aboutsummaryrefslogtreecommitdiffhomepage
path: root/main/app/sprinkles/account
diff options
context:
space:
mode:
authormarvin-borner@live.com2018-04-16 21:09:05 +0200
committermarvin-borner@live.com2018-04-16 21:09:05 +0200
commitcf14306c2b3f82a81f8d56669a71633b4d4b5fce (patch)
tree86700651aa180026e89a66064b0364b1e4346f3f /main/app/sprinkles/account
parent619b01b3615458c4ed78bfaeabb6b1a47cc8ad8b (diff)
Main merge to user management system - files are now at /main/public/
Diffstat (limited to 'main/app/sprinkles/account')
-rwxr-xr-xmain/app/sprinkles/account/asset-bundles.json79
-rwxr-xr-xmain/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js29
-rwxr-xr-xmain/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js19
-rwxr-xr-xmain/app/sprinkles/account/assets/userfrosting/js/pages/register.js94
-rwxr-xr-xmain/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js19
-rwxr-xr-xmain/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js19
-rwxr-xr-xmain/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js39
-rwxr-xr-xmain/app/sprinkles/account/bower.json28
-rwxr-xr-xmain/app/sprinkles/account/composer.json24
-rwxr-xr-xmain/app/sprinkles/account/config/default.php79
-rwxr-xr-xmain/app/sprinkles/account/config/production.php67
-rwxr-xr-xmain/app/sprinkles/account/factories/Permissions.php19
-rwxr-xr-xmain/app/sprinkles/account/factories/Roles.php18
-rwxr-xr-xmain/app/sprinkles/account/factories/Users.php23
-rwxr-xr-xmain/app/sprinkles/account/locale/ar/messages.php176
-rwxr-xr-xmain/app/sprinkles/account/locale/ar/validate.php18
-rwxr-xr-xmain/app/sprinkles/account/locale/de_DE/messages.php188
-rwxr-xr-xmain/app/sprinkles/account/locale/de_DE/validate.php21
-rwxr-xr-xmain/app/sprinkles/account/locale/en_US/messages.php183
-rwxr-xr-xmain/app/sprinkles/account/locale/en_US/validate.php19
-rwxr-xr-xmain/app/sprinkles/account/locale/es_ES/messages.php189
-rwxr-xr-xmain/app/sprinkles/account/locale/es_ES/validate.php19
-rwxr-xr-xmain/app/sprinkles/account/locale/fa/messages.php178
-rwxr-xr-xmain/app/sprinkles/account/locale/fa/validate.php20
-rwxr-xr-xmain/app/sprinkles/account/locale/fr_FR/messages.php179
-rwxr-xr-xmain/app/sprinkles/account/locale/fr_FR/validate.php18
-rwxr-xr-xmain/app/sprinkles/account/locale/it_IT/messages.php186
-rwxr-xr-xmain/app/sprinkles/account/locale/it_IT/validate.php21
-rwxr-xr-xmain/app/sprinkles/account/locale/pt_PT/messages.php166
-rwxr-xr-xmain/app/sprinkles/account/locale/pt_PT/validate.php18
-rwxr-xr-xmain/app/sprinkles/account/locale/ru_RU/messages.php183
-rwxr-xr-xmain/app/sprinkles/account/locale/ru_RU/validate.php19
-rwxr-xr-xmain/app/sprinkles/account/locale/th_TH/messages.php164
-rwxr-xr-xmain/app/sprinkles/account/locale/th_TH/validate.php18
-rwxr-xr-xmain/app/sprinkles/account/locale/tr/messages.php183
-rwxr-xr-xmain/app/sprinkles/account/locale/tr/validate.php19
-rwxr-xr-xmain/app/sprinkles/account/locale/zh_CN/messages.php177
-rwxr-xr-xmain/app/sprinkles/account/locale/zh_CN/validate.php19
-rwxr-xr-xmain/app/sprinkles/account/routes/routes.php59
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/account-settings.yaml35
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/account-verify.yaml6
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/check-username.yaml17
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/deny-password.yaml5
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/forgot-password.yaml6
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/login.yaml19
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/profile-settings.yaml24
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/register.yaml75
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/resend-verification.yaml6
-rwxr-xr-xmain/app/sprinkles/account/schema/requests/set-password.yaml29
-rwxr-xr-xmain/app/sprinkles/account/src/Account.php20
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/AuthGuard.php56
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Authenticator.php419
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php21
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php21
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php21
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php20
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php21
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php21
-rwxr-xr-xmain/app/sprinkles/account/src/Authenticate/Hasher.php108
-rwxr-xr-xmain/app/sprinkles/account/src/Authorize/AccessConditionExpression.php139
-rwxr-xr-xmain/app/sprinkles/account/src/Authorize/AuthorizationException.php23
-rwxr-xr-xmain/app/sprinkles/account/src/Authorize/AuthorizationManager.php157
-rwxr-xr-xmain/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php193
-rwxr-xr-xmain/app/sprinkles/account/src/Bakery/CreateAdminUser.php334
-rwxr-xr-xmain/app/sprinkles/account/src/Controller/AccountController.php1293
-rwxr-xr-xmain/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php20
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php54
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php82
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php57
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php55
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php262
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php57
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php55
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php78
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php69
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php57
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/Activity.php86
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/Group.php69
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/PasswordReset.php76
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/Permission.php121
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/Role.php105
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/User.php493
-rwxr-xr-xmain/app/sprinkles/account/src/Database/Models/Verification.php70
-rwxr-xr-xmain/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php34
-rwxr-xr-xmain/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php50
-rwxr-xr-xmain/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php31
-rwxr-xr-xmain/app/sprinkles/account/src/Facades/Password.php28
-rwxr-xr-xmain/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php33
-rwxr-xr-xmain/app/sprinkles/account/src/Log/UserActivityProcessor.php45
-rwxr-xr-xmain/app/sprinkles/account/src/Repository/PasswordResetRepository.php34
-rwxr-xr-xmain/app/sprinkles/account/src/Repository/TokenRepository.php230
-rwxr-xr-xmain/app/sprinkles/account/src/Repository/VerificationRepository.php32
-rwxr-xr-xmain/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php444
-rwxr-xr-xmain/app/sprinkles/account/src/Twig/AccountExtension.php65
-rwxr-xr-xmain/app/sprinkles/account/src/Util/HashFailedException.php21
-rwxr-xr-xmain/app/sprinkles/account/src/Util/Util.php39
-rwxr-xr-xmain/app/sprinkles/account/templates/forms/settings-account.html.twig37
-rwxr-xr-xmain/app/sprinkles/account/templates/forms/settings-profile.html.twig40
-rwxr-xr-xmain/app/sprinkles/account/templates/mail/password-reset.html.twig22
-rwxr-xr-xmain/app/sprinkles/account/templates/mail/resend-verification.html.twig17
-rwxr-xr-xmain/app/sprinkles/account/templates/mail/verify-account.html.twig21
-rwxr-xr-xmain/app/sprinkles/account/templates/modals/tos.html.twig16
-rwxr-xr-xmain/app/sprinkles/account/templates/navigation/main-nav.html.twig13
-rwxr-xr-xmain/app/sprinkles/account/templates/navigation/user-card.html.twig33
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/account-settings.html.twig45
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/error/compromised.html.twig11
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/forgot-password.html.twig46
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/register.html.twig105
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/resend-verification.html.twig46
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/reset-password.html.twig56
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/set-password.html.twig55
-rwxr-xr-xmain/app/sprinkles/account/templates/pages/sign-in.html.twig84
-rwxr-xr-xmain/app/sprinkles/account/tests/Unit/FactoriesTest.php30
-rwxr-xr-xmain/app/sprinkles/account/tests/Unit/HasherTest.php71
114 files changed, 9785 insertions, 0 deletions
diff --git a/main/app/sprinkles/account/asset-bundles.json b/main/app/sprinkles/account/asset-bundles.json
new file mode 100755
index 0000000..77ee559
--- /dev/null
+++ b/main/app/sprinkles/account/asset-bundles.json
@@ -0,0 +1,79 @@
+{
+ "bundle": {
+ "js/pages/account-settings": {
+ "scripts": [
+ "userfrosting/js/pages/account-settings.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/forgot-password": {
+ "scripts": [
+ "userfrosting/js/pages/forgot-password.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/resend-verification": {
+ "scripts": [
+ "userfrosting/js/pages/resend-verification.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/set-or-reset-password": {
+ "scripts": [
+ "userfrosting/js/pages/set-or-reset-password.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/register": {
+ "scripts": [
+ "vendor/speakingurl/speakingurl.min.js",
+ "userfrosting/js/uf-captcha.js",
+ "userfrosting/js/pages/register.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/sign-in": {
+ "scripts": [
+ "vendor/urijs/src/URI.js",
+ "userfrosting/js/pages/sign-in.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/main/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js b/main/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js
new file mode 100755
index 0000000..8d8d2e7
--- /dev/null
+++ b/main/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js
@@ -0,0 +1,29 @@
+/**
+ * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
+ * example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
+ *
+ * This script depends on validation rules specified in pages/partials/page.js.twig.
+ *
+ * Target page: account/settings
+ */
+$(document).ready(function() {
+
+ // Apply select2 to locale field
+ $('.js-select2').select2();
+
+ $("#account-settings").ufForm({
+ validators: page.validators.account_settings,
+ msgTarget: $("#alerts-page")
+ }).on("submitSuccess.ufForm", function() {
+ // Reload the page on success
+ window.location.reload();
+ });
+
+ $("#profile-settings").ufForm({
+ validators: page.validators.profile_settings,
+ msgTarget: $("#alerts-page")
+ }).on("submitSuccess.ufForm", function() {
+ // Reload the page on success
+ window.location.reload();
+ });
+});
diff --git a/main/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js b/main/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js
new file mode 100755
index 0000000..3f24311
--- /dev/null
+++ b/main/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js
@@ -0,0 +1,19 @@
+/**
+ * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
+ * example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
+ *
+ * This script depends on validation rules specified in pages/partials/page.js.twig.
+ *
+ * Target page: account/forgot-password
+ */
+$(document).ready(function() {
+
+ // TODO: Process form
+ $("#request-password-reset").ufForm({
+ validators: page.validators.forgot_password,
+ msgTarget: $("#alerts-page")
+ }).on("submitSuccess.ufForm", function() {
+ // Forward to login page on success
+ window.location.replace(site.uri.public + "/account/sign-in");
+ });
+});
diff --git a/main/app/sprinkles/account/assets/userfrosting/js/pages/register.js b/main/app/sprinkles/account/assets/userfrosting/js/pages/register.js
new file mode 100755
index 0000000..d855bb9
--- /dev/null
+++ b/main/app/sprinkles/account/assets/userfrosting/js/pages/register.js
@@ -0,0 +1,94 @@
+/**
+ * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
+ * example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
+ *
+ * This script depends on validation rules specified in pages/partials/page.js.twig.
+ *
+ * Target page: account/register
+ */
+$(document).ready(function() {
+ // TOS modal
+ $(this).find('.js-show-tos').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/account/tos",
+ msgTarget: $("#alerts-page")
+ });
+ });
+
+ // Auto-generate username when name is filled in
+ var autoGenerate = true;
+ $("#register").find('input[name=first_name], input[name=last_name]').on('input change', function() {
+ if (!autoGenerate) {
+ return;
+ }
+
+ var form = $("#register");
+
+ var firstName = form.find('input[name=first_name]').val().trim();
+ var lastName = form.find('input[name=last_name]').val().trim();
+
+ if (!firstName && !lastName) {
+ return;
+ }
+
+ var userName = getSlug(firstName + ' ' + lastName, {
+ separator: '.'
+ });
+ // Set slug
+ form.find('input[name=user_name]').val(userName);
+ });
+
+ // Autovalidate username field on a delay
+ var timer;
+ $("#register").find('input[name=first_name], input[name=last_name], input[name=user_name]').on('input change', function() {
+ clearTimeout(timer); // Clear the timer so we don't end up with dupes.
+ timer = setTimeout(function() { // assign timer a new timeout
+ $("#register").find('input[name=user_name]').valid();
+ }, 500);
+ });
+
+ // Enable/disable username suggestions in registration page
+ $("#register").find('#form-register-username-suggest').on('click', function(e) {
+ e.preventDefault();
+ var form = $("#register");
+ $.getJSON(site.uri.public + '/account/suggest-username')
+ .done(function (data) {
+ // Set suggestion
+ form.find('input[name=user_name]').val(data.user_name);
+ });
+ });
+
+ // Turn off autogenerate when someone enters stuff manually in user_name
+ $("#register").find('input[name=user_name]').on('input', function() {
+ autoGenerate = false;
+ });
+
+ // Add remote rule for checking usernames on the fly
+ var registrationValidators = $.extend(
+ true, // deep extend
+ page.validators.register,
+ {
+ rules: {
+ user_name: {
+ remote: {
+ url: site.uri.public + '/account/check-username',
+ dataType: 'text'
+ }
+ }
+ }
+ }
+ );
+
+ // Handles form submission
+ $("#register").ufForm({
+ validators: registrationValidators,
+ msgTarget: $("#alerts-page"),
+ keyupDelay: 500
+ }).on("submitSuccess.ufForm", function() {
+ // Reload to clear form and show alerts
+ window.location.reload();
+ }).on("submitError.ufForm", function() {
+ // Reload captcha
+ $("#captcha").captcha();
+ });
+});
diff --git a/main/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js b/main/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js
new file mode 100755
index 0000000..5c3eaf8
--- /dev/null
+++ b/main/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js
@@ -0,0 +1,19 @@
+/**
+ * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
+ * example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
+ *
+ * This script depends on validation rules specified in pages/partials/page.js.twig.
+ *
+ * Target page: account/resend-verification
+ */
+$(document).ready(function() {
+
+ // TODO: Process form
+ $("#request-verification-email").ufForm({
+ validators: page.validators.resend_verification,
+ msgTarget: $("#alerts-page")
+ }).on("submitSuccess.ufForm", function() {
+ // Forward to login page on success
+ window.location.replace(site.uri.public + "/account/sign-in");
+ });
+});
diff --git a/main/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js b/main/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js
new file mode 100755
index 0000000..39cfd16
--- /dev/null
+++ b/main/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js
@@ -0,0 +1,19 @@
+/**
+ * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
+ * example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
+ *
+ * This script depends on validation rules specified in pages/partials/page.js.twig.
+ *
+ * Target pages: account/set-password, account/reset-password
+ */
+$(document).ready(function() {
+
+ $("#set-or-reset-password").ufForm({
+ validators: page.validators.set_password,
+ msgTarget: $("#alerts-page")
+ }).on("submitSuccess.ufForm", function() {
+ // Forward to home page on success
+ // TODO: forward to landing/last page
+ window.location.replace(site.uri.public + "/account/sign-in");
+ });
+});
diff --git a/main/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js b/main/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js
new file mode 100755
index 0000000..40a8628
--- /dev/null
+++ b/main/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js
@@ -0,0 +1,39 @@
+/**
+ * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
+ * example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
+ *
+ * This script depends on validation rules specified in pages/partials/page.js.twig.
+ *
+ * Target page: account/sign-in
+ */
+$(document).ready(function() {
+ /**
+ * If there is a redirect parameter in the query string, redirect to that page.
+ * Otherwise, if there is a UF-Redirect header, redirect to that page.
+ * Otherwise, redirect to the home page.
+ */
+ function redirectOnLogin(jqXHR) {
+ var components = URI.parse(window.location.href);
+ var query = URI.parseQuery(components['query']);
+
+ if (query && query['redirect']) {
+ // Strip leading slashes from redirect strings
+ var redirectString = site.uri.public + '/' + query['redirect'].replace(/^\/+/, "");
+ // Strip excess trailing slashes for clean URLs. e.g. if redirect=%2F
+ redirectString = redirectString.replace(/\/+$/, "/");
+ // Redirect
+ window.location.replace(redirectString);
+ } else if (jqXHR.getResponseHeader('UF-Redirect')) {
+ window.location.replace(jqXHR.getResponseHeader('UF-Redirect'));
+ } else {
+ window.location.replace(site.uri.public);
+ }
+ }
+
+ $("#sign-in").ufForm({
+ validators: page.validators.login,
+ msgTarget: $("#alerts-page")
+ }).on("submitSuccess.ufForm", function(event, data, textStatus, jqXHR) {
+ redirectOnLogin(jqXHR);
+ });
+});
diff --git a/main/app/sprinkles/account/bower.json b/main/app/sprinkles/account/bower.json
new file mode 100755
index 0000000..8e7ef39
--- /dev/null
+++ b/main/app/sprinkles/account/bower.json
@@ -0,0 +1,28 @@
+{
+ "name": "userfrosting-sprinkle-account",
+ "description": "Authentication and account management module for UserFrosting.",
+ "homepage": "https://github.com/userfrosting",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Alexander Weissman",
+ "homepage": "https://alexanderweissman.com"
+ },
+ "ssnukala"
+ ],
+ "dependencies": {},
+ "moduleType": [
+ "node"
+ ],
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "assets/vendor",
+ "examples",
+ "demo-resources",
+ "demo",
+ "test",
+ "tests"
+ ]
+}
diff --git a/main/app/sprinkles/account/composer.json b/main/app/sprinkles/account/composer.json
new file mode 100755
index 0000000..fa2e178
--- /dev/null
+++ b/main/app/sprinkles/account/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "userfrosting/sprinkle-account",
+ "type": "userfrosting-sprinkle",
+ "description": "Authentication and account management module for UserFrosting.",
+ "keywords": ["php user management", "usercake", "bootstrap"],
+ "homepage": "https://github.com/userfrosting/UserFrosting",
+ "license" : "MIT",
+ "authors" : [
+ {
+ "name": "Alexander Weissman",
+ "homepage": "https://alexanderweissman.com"
+ }
+ ],
+ "require": {
+ "birke/rememberme" : "^2.0",
+ "nikic/php-parser" : "^1",
+ "php": ">=5.6"
+ },
+ "autoload": {
+ "psr-4": {
+ "UserFrosting\\Sprinkle\\Account\\": "src/"
+ }
+ }
+}
diff --git a/main/app/sprinkles/account/config/default.php b/main/app/sprinkles/account/config/default.php
new file mode 100755
index 0000000..e154643
--- /dev/null
+++ b/main/app/sprinkles/account/config/default.php
@@ -0,0 +1,79 @@
+<?php
+
+ /**
+ * Account configuration file for UserFrosting.
+ *
+ */
+
+ return [
+ 'debug' => [
+ 'auth' => false
+ ],
+ // configuration for the 'password reset' feature
+ 'password_reset' => [
+ 'algorithm' => 'sha512',
+ 'timeouts' => [
+ 'create' => 86400,
+ 'reset' => 10800
+ ]
+ ],
+ // See https://github.com/gbirke/rememberme for an explanation of these settings
+ 'remember_me' => [
+ 'cookie' => [
+ 'name' => 'rememberme'
+ ],
+ 'expire_time' => 604800,
+ 'session' => [
+ 'path' => '/'
+ ],
+ 'table' => [
+ 'tableName' => 'persistences',
+ 'credentialColumn' => 'user_id',
+ 'tokenColumn' => 'token',
+ 'persistentTokenColumn' => 'persistent_token',
+ 'expiresColumn' => 'expires_at'
+ ]
+ ],
+ 'reserved_user_ids' => [
+ 'guest' => -1,
+ 'master' => 1
+ ],
+ 'session' => [
+ // The keys used in the session to store info about authenticated users
+ 'keys' => [
+ 'current_user_id' => 'account.current_user_id', // the key to use for storing the authenticated user's id
+ 'captcha' => 'account.captcha' // Key used to store a captcha hash during captcha verification
+ ]
+ ],
+ // "Site" settings that are automatically passed to Twig
+ 'site' => [
+ 'login' => [
+ 'enable_email' => true
+ ],
+ 'registration' => [
+ 'enabled' => true,
+ 'captcha' => true,
+ 'require_email_verification' => true,
+ 'user_defaults' => [
+ 'locale' => 'en_US',
+ 'group' => 'terran',
+ // Default roles for newly registered users
+ 'roles' => [
+ 'user' => true
+ ]
+ ]
+ ]
+ ],
+ 'throttles' => [
+ 'check_username_request' => null,
+ 'password_reset_request' => null,
+ 'registration_attempt' => null,
+ 'sign_in_attempt' => null,
+ 'verification_request' => null
+ ],
+ // configuration for the 'email verification' feature
+ 'verification' => [
+ 'algorithm' => 'sha512',
+ 'timeout' => 10800
+ ]
+ ];
diff --git a/main/app/sprinkles/account/config/production.php b/main/app/sprinkles/account/config/production.php
new file mode 100755
index 0000000..b7c3288
--- /dev/null
+++ b/main/app/sprinkles/account/config/production.php
@@ -0,0 +1,67 @@
+<?php
+
+ /**
+ * Account production config file for UserFrosting. You may override/extend this in your site's configuration file to customize deploy settings.
+ *
+ */
+
+ return [
+ // See http://security.stackexchange.com/a/59550/74909 for the inspiration for our throttling system
+ 'throttles' => [
+ 'check_username_request' => [
+ 'method' => 'ip',
+ 'interval' => 3600,
+ 'delays' => [
+ 40 => 1000
+ ]
+ ],
+ 'password_reset_request' => [
+ 'method' => 'ip',
+ 'interval' => 3600,
+ 'delays' => [
+ 2 => 5,
+ 3 => 10,
+ 4 => 20,
+ 5 => 40,
+ 6 => 80,
+ 7 => 600
+ ]
+ ],
+ 'registration_attempt' => [
+ 'method' => 'ip',
+ 'interval' => 3600,
+ 'delays' => [
+ 2 => 5,
+ 3 => 10,
+ 4 => 20,
+ 5 => 40,
+ 6 => 80,
+ 7 => 600
+ ]
+ ],
+ 'sign_in_attempt' => [
+ 'method' => 'ip',
+ 'interval' => 3600,
+ 'delays' => [
+ 4 => 5,
+ 5 => 10,
+ 6 => 20,
+ 7 => 40,
+ 8 => 80,
+ 9 => 600
+ ]
+ ],
+ 'verification_request' => [
+ 'method' => 'ip',
+ 'interval' => 3600,
+ 'delays' => [
+ 2 => 5,
+ 3 => 10,
+ 4 => 20,
+ 5 => 40,
+ 6 => 80,
+ 7 => 600
+ ]
+ ]
+ ]
+ ];
diff --git a/main/app/sprinkles/account/factories/Permissions.php b/main/app/sprinkles/account/factories/Permissions.php
new file mode 100755
index 0000000..591f5fd
--- /dev/null
+++ b/main/app/sprinkles/account/factories/Permissions.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+use League\FactoryMuffin\Faker\Facade as Faker;
+
+/**
+ * General factory for the Permission Model
+ */
+$fm->define('UserFrosting\Sprinkle\Account\Database\Models\Permission')->setDefinitions([
+ 'slug' => Faker::word(),
+ 'name' => Faker::word(),
+ 'description' => Faker::paragraph(),
+ 'conditions' => Faker::word()
+]);
diff --git a/main/app/sprinkles/account/factories/Roles.php b/main/app/sprinkles/account/factories/Roles.php
new file mode 100755
index 0000000..cdbb5a3
--- /dev/null
+++ b/main/app/sprinkles/account/factories/Roles.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+use League\FactoryMuffin\Faker\Facade as Faker;
+
+/**
+ * General factory for the Role Model
+ */
+$fm->define('UserFrosting\Sprinkle\Account\Database\Models\Role')->setDefinitions([
+ 'slug' => Faker::unique()->word(),
+ 'name' => Faker::word(),
+ 'description' => Faker::paragraph()
+]);
diff --git a/main/app/sprinkles/account/factories/Users.php b/main/app/sprinkles/account/factories/Users.php
new file mode 100755
index 0000000..7390c44
--- /dev/null
+++ b/main/app/sprinkles/account/factories/Users.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+use League\FactoryMuffin\Faker\Facade as Faker;
+
+/**
+ * General factory for the User Model
+ */
+$fm->define('UserFrosting\Sprinkle\Account\Database\Models\User')->setDefinitions([
+ 'user_name' => Faker::unique()->firstNameMale(),
+ 'first_name' => Faker::firstNameMale(),
+ 'last_name' => Faker::firstNameMale(),
+ 'email' => Faker::unique()->email(),
+ 'locale' => 'en_US',
+ 'flag_verified' => 1,
+ 'flag_enabled' => 1,
+ 'password' => Faker::password()
+]);
diff --git a/main/app/sprinkles/account/locale/ar/messages.php b/main/app/sprinkles/account/locale/ar/messages.php
new file mode 100755
index 0000000..7203904
--- /dev/null
+++ b/main/app/sprinkles/account/locale/ar/messages.php
@@ -0,0 +1,176 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Modern Standard Arabic message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\ar
+ * @author Alexander Weissman and Abdullah Seba
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "الحساب",
+
+ "ACCESS_DENIED" => "يبدو أنك لا تملك صلاحية للقيام بذلك",
+
+ "DISABLED" => "هذا الحساب معطل يمكنك الاتصال بنا للحصول على مزيد من المعلومات",
+
+ "EMAIL_UPDATED" => "تم تجديد البريد الإلكتروني بالحساب",
+
+ "INVALID" => "هذا الحساب غير موجود قد تم حذفه يمكنك الاتصا بنا للحصول على مزيد من المعلومات",
+
+ "MASTER_NOT_EXISTS" => "لا يمكنك تسجيل حساب جديد حتى تم إنشاء الحساب الرئيسي",
+ "MY" => "حسابي",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "تم اختراق جلسنك يجب عليك الخروج على كافة الأجهزة، ثم تسجيل الدخول مرة أخرى والتأكد من أن المعلومات الخاصة بك لم يعبث بها",
+ "TITLE" => "من الممكن أن حسابك قد اخترق",
+ "TEXT" => "ربما استخدم شخص معلومات التسجيل الدخول للدخول إلى هذه الصفحة. لسلامتك، تم انتهاء جميع الجلسات يرجا <a href=\"{{url}}\">التسجيل مرة اخرى</a> وتحقق من حسابك بسبب النشاط الغريب قد ترغب في تغيير كلمة المرور"
+ ],
+
+ "SESSION_EXPIRED" => "انتهت جلستك تستطيع تسجيل الدخول مرة أخرى",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "إعدادات الحساب",
+ "DESCRIPTION" => "غير إعدادات حسابك، بما في ذلك البريد الإلكتروني، واسم وكلمة المرور
+",
+ "UPDATED" => "تم تجديد إعدادات الحساب"
+ ],
+
+ "TOOLS" => "أدوات الحساب",
+
+ "UNVERIFIED" => "لم يتم التحقق من حسابك بعد افحص في رسائل البريد الإلكتروني و ملف البريد المزعج للحصول على تعليمات تفعيل الحساب",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "لقد أرسلنا رابط جديدا لتحقق عبر البريد الإلكتروني إلى {{email}} افحص في رسائل البريد الإلكتروني و ملف البريد المزعج",
+ "RESEND" => "إعادة ارسال بريد التحقق",
+ "COMPLETE" => "لقد تم التحقق من حسابك بنجاح يمكنك الآن تسجيل الدخول",
+ "EMAIL" => "ادخل عنوان البريد الإلكتروني الذي استخدمته للتسجيل، و سوف نرسل البريد الإلكتروني لتحقق مرة أخرى",
+ "PAGE" => "إعادة إرسال البريد الإلكتروني التحقق من حسابك الجديد",
+ "SEND" => "ارسل رابط للتحقق عبر البريد الالكتروني",
+ "TOKEN_NOT_FOUND" => "رمز التحقق غير موجود أو تم تحقق الحساب من قبل",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "لا يوجد حساب ل <strong>{{email}}</strong>",
+ "IN_USE" => "البريد الإلكتروني <strong>{{email}}</strong> قيد الاستخدام"
+ ],
+
+ "FIRST_NAME" => "الاسم الاول",
+
+ "HEADER_MESSAGE_ROOT" => "تسجيل الدخول باسم المستخدم ROOT",
+
+ "LAST_NAME" => "اسم العائلة",
+
+ "LOCALEACCOUNT" => "اللغة التي تستخدم لحسابك",
+
+ "LOGIN" => [
+ "@TRANSLATION" => "تسجيل الدخول",
+
+ "ALREADY_COMPLETE" => "انت بالفعل داخل",
+ "SOCIAL" => "أو الدخول مع",
+ "REQUIRED" => "عذرا، يجب عليك تسجيل الدخول للوصول إلى هذا المكان"
+ ],
+
+ "LOGOUT" => "تسجيل الخروج",
+
+ "NAME" => "اسم",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "سجل الدخول إلى حسابك في {{site_name}} أو سجيل للحصول على حساب جديد",
+ "SUBTITLE" => "التسجيل مجانا أو قم بتسجيل الدخول باستخدام حساب موجود",
+ "TITLE" => "هيا نبدأ",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "كلمه المرور",
+
+ "BETWEEN" => "ما بين {{min}}-{{max}} حروف",
+
+ "CONFIRM" => "تأكيد كلمة المرور",
+ "CONFIRM_CURRENT" => "تأكيد كلمه المرور الحالي",
+ "CONFIRM_NEW" => "تأكيد كلمة المرور الجديدة",
+ "CONFIRM_NEW_EXPLAIN" => "إعادة إدخال كلمة المرور الجديدة",
+ "CONFIRM_NEW_HELP" => "لازم إذا كان المطلوب اختيار كلمة مرور جديدة",
+ "CURRENT" => "كلمة المرور الحالية",
+ "CURRENT_EXPLAIN" => "يجب عليك تأكيد كلمة المرور الحالية لإجراء التغييرات",
+
+ "FORGOTTEN" => "كلمه المرور منسية",
+ "FORGET" => [
+ "@TRANSLATION" => "لقد نسيت كلمة المرور",
+
+ "COULD_NOT_UPDATE" => "لا يمكن تحديث كلمة المرور",
+ "EMAIL" => "ادخل عنوان البريد الإلكتروني الذي استخدمته للتسجيل وسوف نرسل تعليمات لإعادة تعيين كلمة المرور",
+ "EMAIL_SEND" => "أرسل رابط تعيين كلمة المرور عبر البريد الالكتروني",
+ "INVALID" => "لم يتم العثور على إعادة تعيين كلمة المرور، أو انتهت صلاحية رابط حاول <a href=\"{{url}}\">إعادة تقديم طلبك<a>",
+ "PAGE" => "الحصول على رابط لإعادة تعيين كلمة المرور",
+ "REQUEST_CANNED" => "إلغاء طلب كلمة المرور",
+ "REQUEST_SENT" => "إذا تطابق البريد الإلكتروني <strong>{{email}}</strong> حسابا في نظامنا، فسيتم إرسال رابط إعادة تعيين كلمة المرور إلى <strong>{{email}}</strong>."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "إعادة تعيين كلمة المرور",
+ "CHOOSE" => "اختيار كلمة مرور جديدة للتواصل",
+ "PAGE" => "اختيار كلمة مرور جديدة لحسابك",
+ "SEND" => "تعيين كلمة المرور الجديدة وتسجيل الدخول"
+ ],
+
+ "HASH_FAILED" => "فشلت التجزئة كلمة المرور يرجى الاتصال بمسؤول الموقع",
+ "INVALID" => "كلمة مرور الحالية لا تتطابق مع ما لدينا",
+ "NEW" => "كلمة مرور الجديدة",
+ "NOTHING_TO_UPDATE" => "لا يمكنك تحديث مع نفس كلمة مرور",
+ "UPDATED" => "جدد كلمة مرور",
+
+ "CREATE" => [
+ "@TRANSLATION" => "إنشاء كلمة مرور",
+ "PAGE" => "اختر كلمة مرور لحسابك الجديد",
+ "SET" => "تعيين كلمة المرور وتسجيل الدخول"
+ ]
+ ],
+
+ "REGISTER" => "تسجيل",
+ "REGISTER_ME" => "سجلني",
+ "SIGN_IN_HERE" => "هل لديك حساب؟ <a href=\"{{url}}\">تسجيل الدخول هنا</a>",
+
+ "REGISTRATION" => [
+ "BROKEN" => "نحن آسفون، هناك مشكلة مع عملية تسجيل الحساب يرجى الاتصال بنا مباشرة للحصول على المساعدة",
+ "COMPLETE_TYPE1" => "لقد سجلت بنجاح يمكنك الآن تسجيل الدخول",
+ "COMPLETE_TYPE2" => "لقد سجلت بنجاح سوف تتلقى قريبا رسالة التحقق تحتوي على رابط لتفعيل حسابك لن تكون قادرا على تسجيل الدخول حتى الانتهاء من هذه الخطوة",
+ "DISABLED" => "عذرا، لقد تم تعطيل تسجيل اي حساب",
+ "LOGOUT" => "لا يمكنك التسجيل للحصول على حساب أثناء تسجيل الدخول",
+ "WELCOME" => "التسجيل سريع وبسيط"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "تم تجاوز الحد عددا لهذا الإجراء يجب الانتظار {{delay}} ثواني قبل القيام بمحاولة أخرى",
+ "REMEMBER_ME" => "تذكرنى",
+ "REMEMBER_ME_ON_COMPUTER" => "تذكرني على هذا الحاسوب (غير مستحسن للحواسب العامة)",
+
+ "SIGNIN" => "تسجيل الدخول",
+ "SIGNIN_OR_REGISTER" => "تسجيل الدخول أو التسجيل",
+ "SIGNUP" => "تسجيل",
+
+ "TOS" => "الأحكام والشروط",
+ "TOS_AGREEMENT" => "من خلال تسجيل حساب جديد في {{site_title}}, انت تقبل <a {{link_attributes | raw}}>الأحكام والشروط</a>",
+ "TOS_FOR" => "الأحكام والشروط ل {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "اسم المستخدم",
+
+ "CHOOSE" => "اختيار اسم مستخدم فريد",
+ "INVALID" => "اسم المستخدم غير صالح",
+ "IN_USE" => "اسم المستخدم <strong>{{user_name}}</strong> قيد الاستخدام"
+ ],
+
+ "USER_ID_INVALID" => "عدم وجود هوية المستخدم المطلوب",
+ "USER_OR_EMAIL_INVALID" => "اسم المستخدم أو عنوان البريد الإلكتروني غير صالح",
+ "USER_OR_PASS_INVALID" => "اسم المستخدم أو كلمة المرور غير صالحة",
+
+ "WELCOME" => "مرحبا بعودتك, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/ar/validate.php b/main/app/sprinkles/account/locale/ar/validate.php
new file mode 100755
index 0000000..37693fb
--- /dev/null
+++ b/main/app/sprinkles/account/locale/ar/validate.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Modern Standard Arabic message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\ar
+ * @author Alexander Weissman and Abdullah Seba
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "يجب أن تكون كلمة المرور وكلمة المرور التأكيدية نفس"
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/de_DE/messages.php b/main/app/sprinkles/account/locale/de_DE/messages.php
new file mode 100755
index 0000000..b331552
--- /dev/null
+++ b/main/app/sprinkles/account/locale/de_DE/messages.php
@@ -0,0 +1,188 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * German message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\de
+ * @author X-Anonymous-Y
+ * @author kevinrombach
+ * @author splitt3r
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Konto",
+
+ "ACCESS_DENIED" => "Hmm, sieht aus als hätten Sie keine Berechtigung, um dies zu tun.",
+
+ "DISABLED" => "Dieses Konto wurde deaktiviert. Bitte Kontaktieren Sie uns für weitere Informationen.",
+
+ "EMAIL_UPDATED" => "E-Mail-Adresse aktualisiert.",
+
+ "INVALID" => "Dieses Konto existiert nicht. Es wurde möglicherweise gelöscht. Bitte kontaktieren Sie uns für weitere Informationen.",
+
+ "MASTER_NOT_EXISTS" => "Sie können kein neues Konto anlegen solange kein Root-Konto angelegt wurde!",
+ "MY" => "Mein Konto",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Ihre Sitzung wurde beeinträchtigt. Sie sollten sich auf allen Geräten abmelden, sich dann wieder anmelden und sicherstellen, dass Ihre Daten nicht manipuliert wurden.",
+ "TITLE" => "Ihr Konto wurde möglicherweise beeinträchtigt",
+ "TEXT" => "Möglicherweise ist es jemandem gelungen, Ihren Zugang zu dieser Seite zu übernehmen. Aus Sicherheitsgründen wurden Sie überall abgemeldet. Bitte <a href=\"{{url}}\">melden Sie sich neu an</a> und untersuchen Sie das Konto nach verdächtigen Aktivitäten. Außerdem sollten Sie Ihr Passwort ändern."
+ ],
+ "SESSION_EXPIRED" => "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Kontoeinstellungen",
+ "DESCRIPTION" => "Aktualisieren Sie Ihre Kontoeinstellungen, einschließlich E-Mail, Name und Passwort.",
+ "UPDATED" => "Kontoeinstellungen aktualisiert"
+ ],
+
+ "TOOLS" => "Konto-Werkzeuge",
+
+ "UNVERIFIED" => "Ihr Konto wurde noch nicht bestätigt. Überprüfen Sie Ihr E-Mails/Spam-Ordner für die Konto-Aktivierungsanleitung.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "Wir haben einen neuen Bestätigungslink an {{email}} gesendet. Überprüfen Sie Ihr E-Mail/Spam-Ordner oder versuchen Sie es später noch einmal.",
+ "RESEND" => "Bestätigungsmail erneut senden",
+ "COMPLETE" => "Sie haben Ihr Konto erfolgreich Verifiziert. Sie können sich jetzt anmelden.",
+ "EMAIL" => "Bitte geben Sie die E-Mail-Adresse ein, mit der Sie sich registriert haben, Überprüfen Sie Ihr E-Mails/Spam-Ordner für die Bestätigungs-E-Mail.",
+ "PAGE" => "Senden Sie die Bestätigungs-E-Mail erneut für Ihr neues Konto.",
+ "SEND" => "Bestätigungslink erneut per E-Mail zusenden",
+ "TOKEN_NOT_FOUND" => "Verifizierungstoken existiert nicht / Konto wurde bereits verifiziert"
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "Es gibt kein Konto für <strong>{{email}}</strong>.",
+ "IN_USE" => "Die E-Mail Adresse <strong>{{email}}</strong> wird bereits verwendet.",
+ "VERIFICATION_REQUIRED" => "E-Mail (Bestätigung benötigt - Benutzen Sie eine echte E-Mail Adresse!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Benutzername oder E-mail Adresse",
+
+ "FIRST_NAME" => "Vorname",
+
+ "HEADER_MESSAGE_ROOT" => "Sie sind als Root-Benutzer angemeldet.",
+
+ "LAST_NAME" => "Nachname",
+
+ "LOCALE" => [
+ "ACCOUNT" => "Die Sprache und das Gebietsschema für Ihr Konto",
+ "INVALID" => "<strong>{{locale}}</strong> ist kein gültiges Gebietsschema."
+ ],
+
+ "LOGIN" => [
+ "@TRANSLATION" => "Anmelden",
+ "ALREADY_COMPLETE" => "Sie sind bereits eingeloggt!",
+ "SOCIAL" => "Oder loggen Sie sich ein mit",
+ "REQUIRED" => "Sorry, Sie müssen angemeldet sein. Um auf diese Ressource zugreifen zu können."
+ ],
+
+ "LOGOUT" => "Ausloggen",
+
+ "NAME" => "Name",
+
+ "NAME_AND_EMAIL" => "Name und E-Mail",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Melden Sie sich in Ihr {{site_name}} Konto an oder registrieren Sie sich für ein neues Konto.",
+ "SUBTITLE" => "Registrieren Sie sich kostenlos oder melden Sie sich mit einem bestehenden Konto an.",
+ "TITLE" => "Lass uns anfangen!"
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Passwort",
+
+ "BETWEEN" => "Zwischen {{min}}-{{max}} Zeichen",
+
+ "CONFIRM" => "Bestätige das Passwort",
+ "CONFIRM_CURRENT" => "Bitte bestätige dein jetziges Passwort",
+ "CONFIRM_NEW" => "Neues Passwort bestätigen",
+ "CONFIRM_NEW_EXPLAIN" => "Geben Sie Ihr neues Passwort erneut ein",
+ "CONFIRM_NEW_HELP" => "Erforderlich, wenn Sie ein neues Passwort wählen",
+ "CREATE" => [
+ "@TRANSLATION" => "Passwort setzen",
+ "PAGE" => "Setzen Sie ein Passwort für den Account.",
+ "SET" => "Passwort setzen und anmelden"
+ ],
+ "CURRENT" => "Aktuelles Passwort",
+ "CURRENT_EXPLAIN" => "Sie müssen Ihr aktuelles Passwort bestätigen, um Änderungen vorzunehmen",
+
+ "FORGOTTEN" => "Passwort vergessen",
+ "FORGET" => [
+ "@TRANSLATION" => "Ich habe mein Passwort vergessen",
+
+ "COULD_NOT_UPDATE" => "Das Passwort konnte nicht aktualisiert werden.",
+ "EMAIL" => "Bitte geben Sie die E-Mail-Adresse ein, mit der Sie sich registriert haben. Ein Link mit der Anweisungen zum Zurücksetzen Ihres Passworts wird Ihnen per E-Mail zugeschickt.",
+ "EMAIL_SEND" => "Neue Passwort zurücksetzen E-Mail senden",
+ "INVALID" => "Diese Anforderung zum Zurücksetzen des Passworts wurde nicht gefunden oder ist abgelaufen.Bitte versuchen Sie <a href=\'{{url}}\'>Ihre Anfrage erneut einzureichen<a>.",
+ "PAGE" => "Holen Sie sich einen Link, um Ihr Passwort zurückzusetzen.",
+ "REQUEST_CANNED" => "Verlorene Passwortanforderung abgebrochen.",
+ "REQUEST_SENT" => "Wenn die E-Mail <strong>{{email}}</strong> mit einem Account in unserem System übereinstimmt, wird ein Passwort-Reset-Link an <strong>{{email}}</strong> gesendet."
+ ],
+
+ "HASH_FAILED" => "Passwort Hashing fehlgeschlagen. Bitte kontaktieren Sie einen Administrator.",
+ "INVALID" => "Das aktuelle Passwort stimmt nicht mit dem Datensatz überein",
+ "NEW" => "Neues Passwort",
+ "NOTHING_TO_UPDATE" => "Sie können nicht das gleiche Passwort zum Aktualisieren verwenden",
+
+ "RESET" => [
+ "@TRANSLATION" => "Passwort zurücksetzen",
+ "CHOOSE" => "Bitte wählen Sie ein neues Passwort, um fortzufahren.",
+ "PAGE" => "Wählen Sie ein neues Passwort für Ihr Konto.",
+ "SEND" => "Neues Passwort festlegen und anmelden"
+ ],
+
+ "UPDATED" => "Konto Passwort aktualisiert"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Profileinstellungen",
+ "UPDATED" => "Profileinstellungen aktualisiert"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "Die grenze für diese Maßnahme wurde überschritten. Sie müssen weitere {{delay}} Sekunden warten, bevor Sie einen weiteren Versuch machen dürfen.",
+
+ "REGISTER" => "Registrieren",
+ "REGISTER_ME" => "Melden Sie mich an",
+ "REGISTRATION" => [
+ "BROKEN" => "Es tut uns leid, es gibt ein Problem mit unserer Registrierung. Bitte kontaktieren Sie uns direkt für Hilfe.",
+ "COMPLETE_TYPE1" => "Sie haben sich erfolgreich registriert. Sie können sich jetzt anmelden.",
+ "COMPLETE_TYPE2" => "Sie haben sich erfolgreich registriert. Sie erhalten in Kürze eine Bestätigungs-E-Mail mit einem Link zur Aktivierung Ihres Kontos. Sie können sich nicht anmelden, bis Sie diesen Schritt abgeschlossen haben.",
+ "DISABLED" => "Es tut uns leid, Die Registrierung des Kontos ist deaktiviert.",
+ "LOGOUT" => "Es tut uns leid, Sie können kein neues Konto registrieren, während Sie angemeldet sind. Bitte melden Sie sich zuerst ab.",
+ "WELCOME" => "Die Registrierung ist schnell und einfach."
+ ],
+ "REMEMBER_ME" => "Erinnere dich an mich!",
+ "REMEMBER_ME_ON_COMPUTER" => "Erinnere dich an mich auf diesem Computer (nicht für öffentliche Computer empfohlen)",
+
+ "SIGN_IN_HERE" => "Sie haben bereits einen Account? <a href=\"{{url}}\">Melden Sie sich hier an.</a>",
+ "SIGNIN" => "Anmelden",
+ "SIGNIN_OR_REGISTER" => "Anmelden oder registrieren",
+ "SIGNUP" => "Anmelden",
+
+ "TOS" => "Geschäftsbedingungen",
+ "TOS_AGREEMENT" => "Durch die Registrierung eines Kontos auf {{site_title}} akzeptieren Sie die <a {{link_attributes | raw}}> Bedingungen </a>.",
+ "TOS_FOR" => "Allgemeine Geschäftsbedingungen für {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Benutzername",
+
+ "CHOOSE" => "Wählen Sie einen eindeutigen Benutzernamen",
+ "INVALID" => "Ungültiger Benutzername",
+ "IN_USE" => "Benutzername <strong>{{user_name}}</strong> wird bereits verwendet.",
+ "NOT_AVAILABLE" => "Benutzername <strong>{{user_name}}</strong> ist nicht verfügbar. Wähle einen anderen Namen, der klicken Sie auf 'vorschlagen'."
+ ],
+
+ "USER_ID_INVALID" => "Die angeforderte Benutzer-ID existiert nicht.",
+ "USER_OR_EMAIL_INVALID" => "Benutzername oder E-Mail-Adresse ist ungültig.",
+ "USER_OR_PASS_INVALID" => "Benutzername oder Passwort ist ungültig.",
+
+ "WELCOME" => "Willkommen zurück, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/de_DE/validate.php b/main/app/sprinkles/account/locale/de_DE/validate.php
new file mode 100755
index 0000000..30cf98b
--- /dev/null
+++ b/main/app/sprinkles/account/locale/de_DE/validate.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * German message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\de
+ * @author X-Anonymous-Y
+ * @author kevinrombach
+ * @author splitt3r
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "Ihr Passwort und das Bestätigungspasswort müssen übereinstimmen.",
+ "USERNAME" => "Benutzernamen dürfen nur aus Kleinbuchstaben, Zahlen, '.', '-' und '_' bestehen."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/en_US/messages.php b/main/app/sprinkles/account/locale/en_US/messages.php
new file mode 100755
index 0000000..17d7582
--- /dev/null
+++ b/main/app/sprinkles/account/locale/en_US/messages.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * US English message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\en_US
+ * @author Alexander Weissman
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Account",
+
+ "ACCESS_DENIED" => "Hmm, looks like you don't have permission to do that.",
+
+ "DISABLED" => "This account has been disabled. Please contact us for more information.",
+
+ "EMAIL_UPDATED" => "Account email updated",
+
+ "INVALID" => "This account does not exist. It may have been deleted. Please contact us for more information.",
+
+ "MASTER_NOT_EXISTS" => "You cannot register an account until the master account has been created!",
+ "MY" => "My Account",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Your session has been compromised. You should log out on all devices, then log back in and make sure that your data has not been tampered with.",
+ "TITLE" => "Your account may have been compromised",
+ "TEXT" => "Someone may have used your login information to acccess this page. For your safety, all sessions were logged out. Please <a href=\"{{url}}\">log in</a> and check your account for suspicious activity. You may also wish to change your password."
+ ],
+ "SESSION_EXPIRED" => "Your session has expired. Please sign in again.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Account settings",
+ "DESCRIPTION" => "Update your account settings, including email, name, and password.",
+ "UPDATED" => "Account settings updated"
+ ],
+
+ "TOOLS" => "Account tools",
+
+ "UNVERIFIED" => "Your account has not yet been verified. Check your emails / spam folder for account activation instructions.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "We have emailed a new verification link to {{email}}. Please check your inbox and spam folders for this email.",
+ "RESEND" => "Resend verification email",
+ "COMPLETE" => "You have successfully verified your account. You can now login.",
+ "EMAIL" => "Please enter the email address you used to sign up, and your verification email will be resent.",
+ "PAGE" => "Resend the verification email for your new account.",
+ "SEND" => "Email the verification link for my account",
+ "TOKEN_NOT_FOUND" => "Verification token does not exist / Account is already verified",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "There is no account for <strong>{{email}}</strong>.",
+ "IN_USE" => "Email <strong>{{email}}</strong> is already in use.",
+ "VERIFICATION_REQUIRED" => "Email (verification required - use a real address!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Username or email address",
+
+ "FIRST_NAME" => "First name",
+
+ "HEADER_MESSAGE_ROOT" => "YOU ARE SIGNED IN AS THE ROOT USER",
+
+ "LAST_NAME" => "Last name",
+ "LOCALE" => [
+ "ACCOUNT" => "The language and locale to use for your account",
+ "INVALID" => "<strong>{{locale}}</strong> is not a valid locale."
+ ],
+ "LOGIN" => [
+ "@TRANSLATION" => "Login",
+ "ALREADY_COMPLETE" => "You are already logged in!",
+ "SOCIAL" => "Or login with",
+ "REQUIRED" => "Sorry, you must be logged in to access this resource."
+ ],
+ "LOGOUT" => "Logout",
+
+ "NAME" => "Name",
+
+ "NAME_AND_EMAIL" => "Name and email",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Sign in to your {{site_name}} account, or register for a new account.",
+ "SUBTITLE" => "Register for free, or sign in with an existing account.",
+ "TITLE" => "Let's get started!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Password",
+
+ "BETWEEN" => "Between {{min}}-{{max}} characters",
+
+ "CONFIRM" => "Confirm password",
+ "CONFIRM_CURRENT" => "Please confirm your current password",
+ "CONFIRM_NEW" => "Confirm New Password",
+ "CONFIRM_NEW_EXPLAIN" => "Re-enter your new password",
+ "CONFIRM_NEW_HELP" => "Required only if selecting a new password",
+ "CREATE" => [
+ "@TRANSLATION" => "Create Password",
+ "PAGE" => "Choose a password for your new account.",
+ "SET" => "Set Password and Sign In"
+ ],
+ "CURRENT" => "Current Password",
+ "CURRENT_EXPLAIN" => "You must confirm your current password to make changes",
+
+ "FORGOTTEN" => "Forgotten Password",
+ "FORGET" => [
+ "@TRANSLATION" => "I forgot my password",
+
+ "COULD_NOT_UPDATE" => "Couldn't update password.",
+ "EMAIL" => "Please enter the email address you used to sign up. A link with instructions to reset your password will be emailed to you.",
+ "EMAIL_SEND" => "Email Password Reset Link",
+ "INVALID" => "This password reset request could not be found, or has expired. Please try <a href=\"{{url}}\">resubmitting your request<a>.",
+ "PAGE" => "Get a link to reset your password.",
+ "REQUEST_CANNED" => "Lost password request cancelled.",
+ "REQUEST_SENT" => "If the email <strong>{{email}}</strong> matches an account in our system, a password reset link will be sent to <strong>{{email}}</strong>."
+ ],
+
+ "HASH_FAILED" => "Password hashing failed. Please contact a site administrator.",
+ "INVALID" => "Current password doesn't match the one we have on record",
+ "NEW" => "New Password",
+ "NOTHING_TO_UPDATE" => "You cannot update with the same password",
+
+ "RESET" => [
+ "@TRANSLATION" => "Reset Password",
+ "CHOOSE" => "Please choose a new password to continue.",
+ "PAGE" => "Choose a new password for your account.",
+ "SEND" => "Set New Password and Sign In"
+ ],
+
+ "UPDATED" => "Account password updated"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Profile settings",
+ "UPDATED" => "Profile settings updated"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "The rate limit for this action has been exceeded. You must wait another {{delay}} seconds before you will be allowed to make another attempt.",
+
+ "REGISTER" => "Register",
+ "REGISTER_ME" => "Sign me up",
+ "REGISTRATION" => [
+ "BROKEN" => "We're sorry, there is a problem with our account registration process. Please contact us directly for assistance.",
+ "COMPLETE_TYPE1" => "You have successfully registered. You can now sign in.",
+ "COMPLETE_TYPE2" => "You have successfully registered. A link to activate your account has been sent to <strong>{{email}}</strong>. You will not be able to sign in until you complete this step.",
+ "DISABLED" => "We're sorry, account registration has been disabled.",
+ "LOGOUT" => "I'm sorry, you cannot register for an account while logged in. Please log out first.",
+ "WELCOME" => "Registration is fast and simple."
+ ],
+ "REMEMBER_ME" => "Keep me signed in",
+ "REMEMBER_ME_ON_COMPUTER" => "Remember me on this computer (not recommended for public computers)",
+
+ "SIGN_IN_HERE" => "Already have an account? <a href=\"{{url}}\">Sign in here.</a>",
+ "SIGNIN" => "Sign in",
+ "SIGNIN_OR_REGISTER" => "Sign in or register",
+ "SIGNUP" => "Sign Up",
+
+ "TOS" => "Terms and Conditions",
+ "TOS_AGREEMENT" => "By registering an account with {{site_title}}, you accept the <a {{link_attributes | raw}}>terms and conditions</a>.",
+ "TOS_FOR" => "Terms and Conditions for {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Username",
+
+ "CHOOSE" => "Choose a unique username",
+ "INVALID" => "Invalid username",
+ "IN_USE" => "Username <strong>{{user_name}}</strong> is already in use.",
+ "NOT_AVAILABLE" => "Username <strong>{{user_name}}</strong> is not available. Choose a different name, or click 'suggest'."
+ ],
+
+ "USER_ID_INVALID" => "The requested user id does not exist.",
+ "USER_OR_EMAIL_INVALID" => "Username or email address is invalid.",
+ "USER_OR_PASS_INVALID" => "User not found or password is invalid.",
+
+ "WELCOME" => "Welcome back, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/en_US/validate.php b/main/app/sprinkles/account/locale/en_US/validate.php
new file mode 100755
index 0000000..00c0aef
--- /dev/null
+++ b/main/app/sprinkles/account/locale/en_US/validate.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * US English message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\en_US
+ * @author Alexander Weissman
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "Your password and confirmation password must match.",
+ "USERNAME" => "Username may consist only of lowercase letters, numbers, '.', '-', and '_'."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/es_ES/messages.php b/main/app/sprinkles/account/locale/es_ES/messages.php
new file mode 100755
index 0000000..aa8b8ed
--- /dev/null
+++ b/main/app/sprinkles/account/locale/es_ES/messages.php
@@ -0,0 +1,189 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Spanish message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\es_ES
+ * @author rafa31gz
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Perfil",
+
+ "ACCESS_DENIED" => "Hmm, parece que no tienes permiso para hacer eso.",
+
+ "DISABLED" => "Esta cuenta se ha inhabilitado. Por favor contáctenos para más información.",
+
+ "EMAIL_UPDATED" => "Correo electrónico de la cuenta actualizado",
+
+ "INVALID" => "Esta cuenta no existe. Puede haber sido eliminado. Por favor contáctenos para más información.",
+
+ "MASTER_NOT_EXISTS" => "No puede registrar una cuenta hasta que se haya creado la cuenta principal.",
+ "MY" => "Mi Perfil",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Su sesión ha sido comprometida. Debe desconectarse de todos los dispositivos y, a continuación, volver a iniciar sesión y asegurarse de que sus datos no han sido manipulados.",
+ "TITLE" => "Es posible que su cuenta se haya visto comprometida.",
+ "TEXT" => "Alguien puede haber utilizado su información de acceso para acceder a esta página. Para su seguridad, todas las sesiones se cerraron. <a href=\"{{url}}\"> ingrese </a> y compruebe si su actividad es sospechosa en su cuenta. También puede cambiar su contraseña."
+ ],
+ "SESSION_EXPIRED" => "Su sesión ha caducado. Inicie sesión nuevamente.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Configuraciones de la cuenta",
+ "DESCRIPTION" => "Actualice la configuración de su cuenta, incluido el correo electrónico, el nombre y la contraseña.",
+ "UPDATED" => "Configuración de la cuenta actualizada"
+ ],
+
+ "TOOLS" => "Herramientas de la cuenta",
+
+ "UNVERIFIED" => "Tu cuenta aún no se ha verificado. Revise sus correos electrónicos / carpeta de spam para obtener instrucciones sobre la activación de la cuenta.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "Hemos enviado por correo electrónico un nuevo enlace de verificación a {{email}}. Comprueba tu bandeja de entrada y las carpetas de spam para este correo electrónico.",
+ "RESEND" => "Reenviar correo electrónico de verificación",
+ "COMPLETE" => "Ha verificado correctamente su cuenta. Ahora puede iniciar sesión.",
+ "EMAIL" => "Ingrese la dirección de correo electrónico que utilizó para registrarse y su correo electrónico de verificación será enviado de nuevo.",
+ "PAGE" => "Vuelva a enviar el correo electrónico de verificación de su nueva cuenta.",
+ "SEND" => "Reenviar correo de verificación",
+ "TOKEN_NOT_FOUND" => "El token de verificación no existe / La cuenta ya está verificada",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "No hay cuenta para <strong> {{email}} </strong>.",
+ "IN_USE" => "El correo electrónico <strong> {{email}} </strong> ya está en uso.",
+ "VERIFICATION_REQUIRED" => "Correo electrónico (se requiere verificación - ¡use una dirección real!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Nombre de usuario o dirección de correo electrónico",
+
+ "FIRST_NAME" => "Nombre",
+
+ "HEADER_MESSAGE_ROOT" => "USTED HA INGRESADO COMO USUARIO ROOT",
+
+ "LAST_NAME" => "Apellidos",
+
+ "LOCALE" => [
+ "ACCOUNT" => "El idioma y la configuración regional para utilizar en su cuenta",
+ "INVALID" => "<strong>{{locale}}</strong> no es un idioma válido."
+ ],
+
+ "LOGIN" => [
+ "@TRANSLATION" => "Acceder",
+ "ALREADY_COMPLETE" => "¡Ya se ha autentificado!",
+ "SOCIAL" => "O ingrese con",
+ "REQUIRED" => "Lo sentimos, debes iniciar sesión para acceder a este recurso."
+ ],
+
+ "LOGOUT" => "Cerrar sesión",
+
+ "NAME" => "Nombre",
+
+ "NAME_AND_EMAIL" => "Nombre y correo electrónico",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Inicie sesión en su cuenta de {{site_name}} o regístrese para obtener una nueva cuenta.",
+ "SUBTITLE" => "Regístrese gratis o inicie sesión con una cuenta existente.",
+ "TITLE" => "¡Empecemos!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Contraseña",
+
+ "BETWEEN" => "Entre {{min}} - {{max}} (recomendado 12)",
+
+ "CONFIRM" => "Confirmar contraseña",
+ "CONFIRM_CURRENT" => "Por favor, confirma tu contraseña actual",
+ "CONFIRM_NEW" => "Confirmar nueva contraseña",
+ "CONFIRM_NEW_EXPLAIN" => "Vuelve a ingresar tu nueva contraseña",
+ "CONFIRM_NEW_HELP" => "Sólo se requiere si se selecciona una nueva contraseña",
+ "CREATE" => [
+ "@TRANSLATION" => "Crear contraseña",
+ "PAGE" => "Elija una contraseña para su nueva cuenta.",
+ "SET" => "Establecer contraseña e iniciar sesión"
+ ],
+ "CURRENT" => "Contraseña actual",
+ "CURRENT_EXPLAIN" => "Debe confirmar su contraseña actual para realizar cambios",
+
+ "FORGOTTEN" => "Contraseña olvidada",
+ "FORGET" => [
+ "@TRANSLATION" => "Olvidé mi contraseña",
+
+ "COULD_NOT_UPDATE" => "No se pudo actualizar la contraseña.",
+ "EMAIL" => "Introduce la dirección de correo electrónico que utilizaste para registrarte. Se le enviará por correo electrónico un enlace con las instrucciones para restablecer su contraseña.",
+ "EMAIL_SEND" => "Contraseña de correo electrónico Restablecer enlace",
+ "INVALID" => "No se pudo encontrar esta solicitud de restablecimiento de contraseña o ha caducado. Intenta <a href=\"{{url}}\"> volver a enviar tu solicitud <a>.",
+ "PAGE" => "Obtenga un enlace para restablecer su contraseña.",
+ "REQUEST_CANNED" => "Se ha cancelado la solicitud de contraseña perdida.",
+ "REQUEST_SENT" => "Se ha enviado un enlace de restablecimiento de contraseña a <strong> {{email}} </strong>."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "Restablecer la contraseña",
+ "CHOOSE" => "Por favor, elija una nueva contraseña para continuar.",
+ "PAGE" => "Elige una nueva contraseña para tu cuenta.",
+ "SEND" => "Establecer nueva contraseña e iniciar sesión"
+ ],
+
+ "HASH_FAILED" => "El hash de la contraseña ha fallado. Póngase en contacto con un administrador del sitio.",
+ "INVALID" => "La contraseña actual no coincide con la que tenemos registrada",
+ "NEW" => "Nueva contraseña",
+ "NOTHING_TO_UPDATE" => "No se puede actualizar con la misma contraseña",
+ "UPDATED" => "Contraseña de la cuenta actualizada"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Configuración de perfil",
+ "UPDATED" => "Configuración del perfil actualizada"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "Se ha superado el límite de velocidad para esta acción. Debe esperar otro {{delay}} segundos antes de que se le permita hacer otro intento.",
+
+ "REGISTER" => "Registro",
+ "REGISTER_ME" => "Inscríbeme",
+ "REGISTRATION" => [
+ "BROKEN" => "Lo sentimos, hay un problema con nuestro proceso de registro de cuenta. Póngase en contacto con nosotros directamente para obtener ayuda.",
+ "COMPLETE_TYPE1" => "Se ha registrado exitosamente. Ahora puede iniciar sesión.",
+ "COMPLETE_TYPE2" => "Se ha registrado exitosamente. Se ha enviado un enlace para activar tu cuenta a <strong> {{email}} </strong>. No podrá iniciar sesión hasta que complete este paso.",
+ "DISABLED" => "Lo sentimos, el registro de cuenta se ha deshabilitado.",
+ "LOGOUT" => "Lo siento, no puede registrarse para una cuenta mientras está conectado. Por favor, cierra la sesión primero.",
+ "WELCOME" => "El registro es rápido y sencillo."
+ ],
+
+ "REMEMBER_ME" => "¡Recuérdame!",
+ "REMEMBER_ME_ON_COMPUTER" => "Recuérdeme en este ordenador (no se recomienda para ordenadores públicos)",
+
+ "SIGNIN" => "Iniciar sesión",
+ "SIGNIN_OR_REGISTER" => "Ingresa o Registro",
+ "SIGNUP" => "Regístrate",
+ "SUGGEST" => "Sugerencia",
+ "HAVE_ACCOUNT" => "¿Ya tienes una cuenta?",
+ "SIGN_IN_HERE"=> "¿Ya tienes una cuenta? <a href=\"{{url}}\"> Acceda aquí. </a>",
+
+
+ "TOS" => "Términos y Condiciones",
+ "TOS_AGREEMENT" => "Al registrar una cuenta con {{site_title}}, acepta los <a {{link_attributes | raw}}> términos y condiciones </a>.",
+ "TOS_FOR" => "Términos y condiciones para {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Nombre de usuario",
+
+ "CHOOSE" => "Elige un nombre de usuario único",
+ "INVALID" => "Nombre de usuario no válido",
+ "IN_USE" => "El nombre de usuario <strong> {{user_name}} </strong> ya está en uso.",
+ "NOT_AVAILABLE" => "El nombre de usuario <strong> {{user_name}} </strong> no está disponible. Elija otro nombre o haga clic en \"sugerir\"."
+ ],
+
+ "USER_ID_INVALID" => "El ID de usuario solicitado no existe.",
+ "USER_OR_EMAIL_INVALID" => "El nombre de usuario o la dirección de correo electrónico no son válidos.",
+ "USER_OR_PASS_INVALID" => "Usuario no encontrado o la contraseña no es válida.",
+
+ "WELCOME" => "Bienvenido de nuevo, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/es_ES/validate.php b/main/app/sprinkles/account/locale/es_ES/validate.php
new file mode 100755
index 0000000..c8ea0a4
--- /dev/null
+++ b/main/app/sprinkles/account/locale/es_ES/validate.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Spanish message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\es_ES
+ * @author rafa31gz
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "Su contraseña y contraseña de confirmación deben coincidir.",
+ "USERNAME" => "El nombre de usuario puede consistir sólo en letras minúsculas, números, '.', '-' y '_'."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/fa/messages.php b/main/app/sprinkles/account/locale/fa/messages.php
new file mode 100755
index 0000000..22623ba
--- /dev/null
+++ b/main/app/sprinkles/account/locale/fa/messages.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Standard Farsi/Persian message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\fa
+ * @author aminakbari
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "حساب",
+
+ "ACCESS_DENIED" => "به نظر می آید که شما اجازه انجام این کار را ندارید",
+
+ "DISABLED" => "این حساب کاربری غیر فعال شده است. برای اطلاعات بیشتر، لطفا با ما تماس برقرار کنید.",
+
+ "EMAIL_UPDATED" => "آدرس پست الکترونیکی حساب، به روز رسانی شد",
+
+ "INVALID" => "این اکانت موجود نیست. ممکن است که حذف شده باشد. برای اطلاعات بیشتر، لطفا با ما تماس برقرار کنید.",
+
+ "MASTER_NOT_EXISTS" => "تا زمانی که حساب اصلی ساخته نشده است نمیتوانید حساب کاربری جدیدی بسازید.",
+ "MY" => "حساب من",
+
+ "SESSION_COMPROMISED" => "ممکن است سژن شما مورد حمله واقع شده باشد. بهتر است با همه دستگاه های خود از وب سایت خارج شوید و دوباره وارد شوید. همچنین توجه بفرمایید که اطلاعات حسابتان، مورد حمله واقع نشده باشد. ",
+ "SESSION_COMPROMISED_TITLE" => "ممکن است که اکانت شما مورد حمله واقع شده باشد",
+ "SESSION_EXPIRED" => "سژن شما به پایان رسیده است. لطفا دوباره وارد شوید.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "تنظیمات حساب",
+ "DESCRIPTION" => "اطلاعات حسابتان یعنی پست الکترونیکی،نام و گذرواژه خود را به روز رسانی کنید",
+ "UPDATED" => "تنظیمات حساب به روز رسانی شد"
+ ],
+
+ "TOOLS" => "ابزار حساب",
+
+ "UNVERIFIED" => "شما هنوز آدرس پست الکترونیکی خود را فعال نکرده اید. برای فعال سازی لطفا ایمیل خود را چک کنید.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "لینک فعال سازی برای ایمیل {{email}} ارسال شد. لطفا ایمیل خود را چک کنید.",
+ "RESEND" => "ارسال دوباره ایمیل فعال سازی",
+ "COMPLETE" => "شما پست الکترونیکی خود را با موفقیت فعال سازی کردید. حالا می توانید وارد شوید.",
+ "EMAIL" => "لطفا آدرس پست الکترونیکی که با آن ثبت نام کردید وارد کنید تا ایمیل فعال سازی دوباره برایتان ارسال شود.",
+ "PAGE" => "ارسال دوباره ایمیل فعال سازی برای حساب جدید شما",
+ "SEND" => "ارسال ایمیل فعال سازی برای حساب کاربری",
+ "TOKEN_NOT_FOUND" => "این حساب کاربری یا قبلا فعال شده است و یا کد فعال سازی موجود نیست.",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "حساب کاربری با <strong>{{email}}</strong> ثبت نشده است.",
+ "IN_USE" => "ایمیل <strong>{{email}}</strong> قبلا استفاده شده است",
+ "VERIFICATION_REQUIRED" => "آدرس پست الکترونیکی را بصورت صحیح وارد کنید"
+ ],
+
+ "EMAIL_OR_USERNAME" => "نام کاربری یا آدرس پست الکترونیکی",
+
+ "FIRST_NAME" => "نام",
+
+ "HEADER_MESSAGE_ROOT" => "شما بعنوان کاربر اصلی وارد شده اید",
+
+ "LAST_NAME" => "نام خانوادگی",
+
+ "LOCALE" => [
+ "ACCOUNT" => "زبان انتخابی برای حساب شما",
+ "INVALID" => "<strong>{{locale}}</strong> زبان صحیحی نیست"
+ ],
+
+ "LOGIN" => [
+ "@TRANSLATION" => "ورود",
+ "ALREADY_COMPLETE" => "شما قبلا وارد شده اید.",
+ "SOCIAL" => "یا با روش های زیر وارد شوید",
+ "REQUIRED" => "برای دیدن این صفحه لازم است که وارد شوید"
+ ],
+
+ "LOGOUT" => "خروج",
+
+ "NAME" => "نام",
+
+ "NAME_AND_EMAIL" => "نام و پست الکترونیکی",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "به حساب کاربری خود در {{site_name}} وارد شوید و یا حساب کاربری جدیدی بسازید",
+ "SUBTITLE" => "ثبت نام کنید و یا با حساب کاربری خود وارد شوید",
+ "TITLE" => "بیایید شروع کنیم!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "گذرواژه",
+
+ "BETWEEN" => "بین {{min}}-{{max}} حرف",
+
+ "CONFIRM" => "رمز عبور را وارد کنید",
+ "CONFIRM_CURRENT" => "لطفا رمز عبور فعلی را تایید کنید",
+ "CONFIRM_NEW" => "رمز عبور جدید را وارد کنید",
+ "CONFIRM_NEW_EXPLAIN" => "رمز عبور جدید را تکرار کنید",
+ "CONFIRM_NEW_HELP" => "فقط زمانی لازم است که می خواهید گذرواژه جدیدی انتخاب کنید",
+ "CURRENT" => "گذرواژه فعلی",
+ "CURRENT_EXPLAIN" => "شما باید گذرواژه فعلی خود را وارد کنید تا بتوانید اطلاعات را به روز رسانی کنید",
+
+ "FORGOTTEN" => "فراموشی گذرواژه",
+ "FORGET" => [
+ "@TRANSLATION" => "گذرواژه خود را فراموش کرده ام",
+
+ "COULD_NOT_UPDATE" => "گذرواژه به روز رسانی نشد",
+ "EMAIL" => "لطفا آدرس پست الکترونیکی که در زمان ثبت نام استفاده کردید، وارد کنید. لینک بازیابی گذرواژه برای شما ایمیل خواهد شد.",
+ "EMAIL_SEND" => "لینک بازیابی گذرواژه ایمیل شود",
+ "INVALID" => "درخواست بازیابی کذرواژه پیدا نشد و یا منقضی شده است. لطفا درخواست را <a href=\"{{url}}\">دوباره ارسال کنید<a>",
+ "PAGE" => "دریافت لینک بازیابی گذرواژه",
+ "REQUEST_CANNED" => "درخواست فراموشی گذرواژه، حذف شد.",
+ "REQUEST_SENT" => "ایمیل بازیابی گذرواژه به <strong>{{email}}</strong> ارسال شد."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "تغییر گذرواژه",
+ "CHOOSE" => "لطفا گذرواژه جدید را انتخاب کنید",
+ "PAGE" => "برای حساب خود، گذرواژه جدیدی انتخاب کنید.",
+ "SEND" => "گذرواژه جدید را انتخاب کرده و وارد شوید"
+ ],
+
+ "HASH_FAILED" => "هشینگ گذرواژه با مشکل روبرو شد. لطفا با مسولین وب سایت تماس برقرار کنید",
+ "INVALID" => "گذرواژه فعلی درست وارد نشده است",
+ "NEW" => "گذرواژه جدید",
+ "NOTHING_TO_UPDATE" => "شما نمیتوانید همان گذرواژه را دوباره وارد کنید",
+ "UPDATED" => "گذرواژه به روز رسانی شد"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "تنظیمات شخصی حساب",
+ "UPDATED" => "تنظیمات شخصی حساب به روز رسانی شد"
+ ],
+
+ "REGISTER" => "ثبت نام",
+ "REGISTER_ME" => "ثبت نام کن",
+
+ "REGISTRATION" => [
+ "BROKEN" => "متاسفانه پروسه ثبت نام با مشکلی روبرو شد. برای دریافت کمک لطفا با ما تماس بگیرید.",
+ "COMPLETE_TYPE1" => "شما با موفقیت ثبت نام کردید. حالا میتوانید وارد شوید.",
+ "COMPLETE_TYPE2" => "شما با موفقیت ثبت نام کردید. لینک فعال سازی حساب به آدرس پست الکترونیکیتان <strong>{{email}}</strong> ارسال شد. بدون فعال سازی نمیتوانید وارد شوید.",
+ "DISABLED" => "با عرض تاسف، امکان ثبت در وب سایت، غیر فعال شده است.",
+ "LOGOUT" => "شما همزمان این که وارد شده اید نمیتوانید حساب کاربری جدیدی بسازید. لطفا ابتدا خارج شوید.",
+ "WELCOME" => "سریع و ساده ثبت نام کنید"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "شما محدودیت تعداد انجام این کار را پشت سر گذاشتید. لطفا {{delay}} ثانیه دیگر صبر کرده و دوباره تلاش کنید.",
+ "REMEMBER_ME" => "من را به خاطر بسپار!",
+ "REMEMBER_ME_ON_COMPUTER" => "من را در این دستگاه به خاطر بسپار (برای دستگاه های عمومی پیشنهاد نمی شود)",
+
+ "SIGNIN" => "ورود",
+ "SIGNIN_OR_REGISTER" => "ثبت نام کنید و یا وارد شوید",
+ "SIGNUP" => "ثبت نام",
+
+ "TOS" => "شرایط و مقررات",
+ "TOS_AGREEMENT" => "با ثبت نام در {{site_title}} موافقت خود با <a {{link_attributes | raw}}>شرایط و مقررات</a> را نشان میدهید.",
+ "TOS_FOR" => "شرایط و مقررات {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "نام کاربری",
+
+ "CHOOSE" => "یک نام کاربری منحصر به فرد انتخاب کنید",
+ "INVALID" => "نام کاربری معتبر نیست",
+ "IN_USE" => "نام کاربری <strong>{{user_name}}</strong> قبلا استفاده شده است",
+ "NOT_AVAILABLE" => "نام کاربری <strong>{{user_name}}</strong> موجود نیست. لطفا نام کاربری دیگری انتخاب کنید"
+ ],
+
+ "USER_ID_INVALID" => "آی دی کاربری مد نظر شما موجود نیست",
+ "USER_OR_EMAIL_INVALID" => "نام کاربری و یا آدرس پست الکترونیکی معتبر نیست",
+ "USER_OR_PASS_INVALID" => "کاربری یافت نشد و یا گذرواژه صحیح نیست",
+
+ "WELCOME" => "خوش آمدید {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/fa/validate.php b/main/app/sprinkles/account/locale/fa/validate.php
new file mode 100755
index 0000000..a63cae1
--- /dev/null
+++ b/main/app/sprinkles/account/locale/fa/validate.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Standard Farsi/Persian message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\fa
+ * @author aminakbari
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "گذرواژه و تکرار آن باید با یکدیگر تطبیق پیدا کنند",
+ "USERNAME" => "نام کاربری فقط میتواند از حروف کوچک، اعداد، '.'، '-' و '_' متشکل شوند."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/fr_FR/messages.php b/main/app/sprinkles/account/locale/fr_FR/messages.php
new file mode 100755
index 0000000..6e5a032
--- /dev/null
+++ b/main/app/sprinkles/account/locale/fr_FR/messages.php
@@ -0,0 +1,179 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * French message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\fr
+ * @author Louis Charette
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Compte d'utilisateur",
+
+ "ACCESS_DENIED" => "Hmm, on dirait que vous n'avez pas la permission de faire ceci.",
+
+ "DISABLED" => "Ce compte a été désactivé. Veuillez nous contacter pour plus d'informations.",
+
+ "EMAIL_UPDATED" => "Adresse email mise à jour",
+
+ "INVALID" => "Ce compte n'existe pas. Il a peut-être été supprimé. Veuillez nous contacter pour plus d'informations.",
+
+ "MASTER_NOT_EXISTS" => "Vous ne pouvez pas enregistrer un compte tant que le compte principal n'a pas été créé!",
+ "MY" => "Mon compte",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Votre session a été compromise. Vous devez vous déconnecter de tous les périphériques, puis vous reconnecter et vous assurer que vos données n'ont pas été altérées.",
+ "TITLE" => "Votre compte peut avoir été compromis"
+ ],
+ "SESSION_EXPIRED" => "Votre session a expiré. Veuillez vous connecter à nouveau.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Paramètres du compte",
+ "DESCRIPTION" => "Mettez à jour les paramètres de votre compte, y compris votre adresse e-mail, votre nom et votre mot de passe.",
+ "UPDATED" => "Paramètres du compte mis à jour"
+ ],
+
+ "TOOLS" => "Outils du compte",
+
+ "UNVERIFIED" => "Votre compte n'a pas encore été vérifié. Vérifiez vos emails / dossier spam pour les instructions d'activation du compte.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "Nous avons envoyé un nouveau lien de vérification à {{email}}. Veuillez vérifier vos dossiers de boîte de réception et de spam pour ce courriel.",
+ "RESEND" => "Renvoyer le courriel de validation",
+ "COMPLETE" => "Votre compte a été validé. Vous pouvez maintenant vous connecter.",
+ "EMAIL" => "Veuillez saisir l'adresse email que vous avez utilisée pour vous inscrire et votre courriel de vérification sera renvoyé.",
+ "PAGE" => "Renvoyer l'email de validation de votre nouveau compte.",
+ "SEND" => "Envoyer le lien de validation de mon compte",
+ "TOKEN_NOT_FOUND" => "Le jeton de vérification n'existe pas / Le compte est déjà vérifié",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "Il n'y a aucun compte pour <strong>{{email}}</strong>.",
+ "IN_USE" => "Le email <strong>{{email}}</strong> est déjà utilisé.",
+ "VERIFICATION_REQUIRED" => "Email (vérification requise - utiliser une adresse réelle!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Nom d'utilisateur ou adresse email",
+
+ "FIRST_NAME" => "Prénom",
+
+ "HEADER_MESSAGE_ROOT" => "VOUS ÊTES CONNECTÉ EN TANT QUE L'UTILISATEUR ROOT",
+
+ "LAST_NAME" => "Nom de famille",
+
+ "LOCALE" => [
+ "ACCOUNT" => "La langue utilisé pour votre compte d'utilisateur",
+ "INVALID" => "<strong>{{locale}}</strong> n'est pas une langue valide."
+ ],
+
+ "LOGIN" => [
+ "@TRANSLATION" => "Connexion",
+ "ALREADY_COMPLETE" => "Vous êtes déjà connecté!",
+ "SOCIAL" => "Ou se connecter avec",
+ "REQUIRED" => "Désolé, vous devez être connecté pour accéder à cette ressource."
+ ],
+
+ "LOGOUT" => "Déconnexion",
+
+ "NAME" => "Nom",
+
+ "NAME_AND_EMAIL" => "Nom et email",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Connectez-vous à votre compte {{site_name}} ou enregistrez-vous pour un nouveau compte.",
+ "SUBTITLE" => "Inscrivez-vous gratuitement ou connectez-vous avec un compte existant.",
+ "TITLE" => "Commençons!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Mot de passe",
+
+ "BETWEEN" => "Entre {{min}} et {{max}} charactères",
+
+ "CONFIRM" => "Confirmer le mot de passe",
+ "CONFIRM_CURRENT" => "Veuillez confirmer votre mot de passe actuel",
+ "CONFIRM_NEW" => "Confirmer le nouveau mot de passe",
+ "CONFIRM_NEW_EXPLAIN" => "Confirmer le mot de passe",
+ "CONFIRM_NEW_HELP" => "Obligatoire uniquement si vous sélectionnez un nouveau mot de passe",
+ "CURRENT" => "Mot de passe actuel",
+ "CURRENT_EXPLAIN" => "Vous devez confirmer votre mot de passe actuel pour apporter des modifications",
+
+ "FORGOTTEN" => "Mot de passe oublié",
+ "FORGET" => [
+ "@TRANSLATION" => "J'ai oublié mon mot de passe",
+
+ "COULD_NOT_UPDATE" => "Impossible de mettre à jour le mot de passe.",
+ "EMAIL" => "Veuillez saisir l'adresse e-mail que vous avez utilisée pour vous inscrire. Un lien avec les instructions pour réinitialiser votre mot de passe vous sera envoyé par email.",
+ "EMAIL_SEND" => "Envoyer le lien de réinitialisation",
+ "INVALID" => "Cette requête de réinitialisation de mot de passe n'a pas pu être trouvée ou a expiré. Veuillez réessayer <a href=\"{{url}}\"> de soumettre votre demande <a>.",
+ "PAGE" => "Obtenir un lien pour réinitialiser votre mot de passe.",
+ "REQUEST_CANNED" => "Demande de mot de passe perdu annulée.",
+ "REQUEST_SENT" => "Si l'adresse e-mail <strong>{{email}}</strong> correspond à un compte dans notre système, un lien de réinitialisation de mot de passe sera envoyé à <strong>{{email}}</strong>."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "Réinitialiser le mot de passe",
+ "CHOOSE" => "Veuillez choisir un nouveau mot de passe pour continuer.",
+ "PAGE" => "Choisissez un nouveau mot de passe pour votre compte.",
+ "SEND" => "Définir un nouveau mot de passe"
+ ],
+
+ "HASH_FAILED" => "Le hachage du mot de passe a échoué. Veuillez contacter un administrateur de site.",
+ "INVALID" => "Le mot de passe actuel ne correspond pas à celui que nous avons au dossier",
+ "NEW" => "Nouveau mot de passe",
+ "NOTHING_TO_UPDATE" => "Vous ne pouvez pas mettre à jour avec le même mot de passe",
+ "UPDATED" => "Mot de passe du compte mis à jour"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Paramètres du profil",
+ "UPDATED" => "Paramètres du profil mis à jour"
+ ],
+
+ "REGISTER" => "S'inscrire",
+ "REGISTER_ME" => "S'inscrire",
+
+ "REGISTRATION" => [
+ "BROKEN" => "Nous sommes désolés, il ya un problème avec notre processus d'enregistrement de compte. Veuillez nous contacter directement pour obtenir de l'aide.",
+ "COMPLETE_TYPE1" => "Vous êtes inscrit avec succès. Vous pouvez maintenant vous connecter.",
+ "COMPLETE_TYPE2" => "Vous êtes inscrit avec succès. Vous recevrez bientôt un e-mail de validation contenant un lien pour activer votre compte. Vous ne pourrez pas vous connecter avant d'avoir terminé cette étape.",
+ "DISABLED" => "Désolé, l'enregistrement de compte a été désactivé.",
+ "LOGOUT" => "Désolé, vous ne pouvez pas vous inscrire tout en étant connecté. Veuillez vous déconnecter en premier.",
+ "WELCOME" => "L'inscription est rapide et simple."
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "La limite de tentatives pour cette action a été dépassée. Vous devez attendre {{delay}} secondes avant de pouvoir effectuer une autre tentative.",
+ "REMEMBER_ME" => "Se souvenir de moi!",
+ "REMEMBER_ME_ON_COMPUTER" => "Se souvenir de moi sur cet ordinateur (non recommandé pour les ordinateurs publics)",
+
+ "SIGNIN" => "Se connecter",
+ "SIGNIN_OR_REGISTER" => "Se connecter ou s'inscrire",
+ "SIGNUP" => "S'inscrire",
+
+ "TOS" => "Termes et conditions",
+ "TOS_AGREEMENT" => "En créant un compte avec {{site_title}}, vous acceptez les <a {{link_attributes | raw}}>termes et conditions</a>.",
+ "TOS_FOR" => "Termes et conditions pour {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Nom d'utilisateur",
+
+ "CHOOSE" => "Choisissez un nom d'utilisateur unique",
+ "INVALID" => "Nom d'utilisateur invalide",
+ "IN_USE" => "Le nom d'utilisateur '{{username}}' est déjà utilisé.",
+ "NOT_AVAILABLE" => "Le nom d'utilisateur <strong>{{user_name}}</strong> n'est pas disponible. Choisissez un autre nom, ou cliquez sur « suggérer »."
+ ],
+
+ "USER_ID_INVALID" => "L'identifiant d'utilisateur demandé n'existe pas.",
+ "USER_OR_EMAIL_INVALID" => "Nom d'utilisateur ou adresse e-mail non valide.",
+ "USER_OR_PASS_INVALID" => "Nom d'utilisateur ou mot de passe incorrect.",
+
+ "WELCOME" => "Bienvenue {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/fr_FR/validate.php b/main/app/sprinkles/account/locale/fr_FR/validate.php
new file mode 100755
index 0000000..44b1bc1
--- /dev/null
+++ b/main/app/sprinkles/account/locale/fr_FR/validate.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * French message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\fr
+ * @author Louis Charette
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "Votre mot de passe et votre mot de passe de confirmation doivent correspondre."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/it_IT/messages.php b/main/app/sprinkles/account/locale/it_IT/messages.php
new file mode 100755
index 0000000..fee2e8c
--- /dev/null
+++ b/main/app/sprinkles/account/locale/it_IT/messages.php
@@ -0,0 +1,186 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Italian message token translations for the 'account' sprinkle.
+ * This translation was generated with Google translate. Please contribute if you are a native speaker.
+ *
+ * @package userfrosting\i18n\it
+ * @author Alexander Weissman
+ * @author Pietro Marangon (@Pe46dro)
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Account",
+
+ "ACCESS_DENIED" => "Sembra tu non abbiamo il permesso di fare questo.",
+
+ "DISABLED" => "Questo account è stato disattivato, contattaci per maggiori informazioni",
+
+ "EMAIL_UPDATED" => "Email aggiornata",
+
+ "INVALID" => "Questo account non esiste. Può essere stato cancellato. Vi preghiamo di contattarci per ulteriori informazioni.",
+
+ "MASTER_NOT_EXISTS" => "Non puoi registrare un account finche l'account primario non sarà creato!",
+ "MY" => "Il mio account",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "La tua sessione è stata compromessa. Devi eseguire il logout su tutti i dispositivi, quindi riaccenderti e assicurati che i tuoi dati non siano stati manomessi.",
+ "TITLE" => "Il tuo account potrebbe essere stato compromesso",
+ "TEXT" => "Qualcuno potrebbe aver utilizzato le tue informazioni di accesso per accedere a questa pagina. Per la tua sicurezza tutte le sessioni sono state disconnesse. <a href=\"{{url}}\">Accedi</a> e controlla l'account per attività sospette. Potresti anche desiderare di cambiare la tua password."
+ ],
+ "SESSION_EXPIRED" => "La tua sessione è scaduta. Accedi nuovamente.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Impostazioni dell 'account",
+ "DESCRIPTION" => "Aggiorna le impostazioni del tuo account, tra cui email, nome e password.",
+ "UPDATED" => "Impostazioni account aggiornate"
+ ],
+
+ "TOOLS" => "Account tools",
+
+ "UNVERIFIED" => "Il tuo account non è stato attivato. Controlla nella tua mail ( anche nella cartella dello spam ) per riceve le instruzioni per attivare il tuo account",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "Ti è stato inviato un nuovo codice di attivazione, controlla la tua email ({{email}}).",
+ "RESEND" => "Invia nuovamente email di verifica.",
+ "COMPLETE" => "Hai verificato con successo il tuo account. È ora possibile accedere.",
+ "EMAIL" => "Inserisci l'indirizzo email che hai utilizzato per registrarti e la tua email di verifica sarà resentata.",
+ "PAGE" => "Ripeti l'email di verifica per il tuo nuovo account.",
+ "SEND" => "Inviilo il collegamento di verifica per il mio account",
+ "TOKEN_NOT_FOUND" => "Il token non esiste / l'account è già stato attivato"
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "Non esiste alcun account per <strong>{{email}}</strong>.",
+ "IN_USE" => "L'email '{{email}}' è già in uso",
+ "VERIFICATION_REQUIRED" => "Email (verifica richiesta - utilizzare un indirizzo reale!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Username o Indirizzo Email",
+
+ "FIRST_NAME" => "Nome",
+
+ "HEADER_MESSAGE_ROOT" => "LOGGATO COME ROOT",
+
+ "LAST_NAME" => "Cognome",
+ "LOCALE" => [
+ "ACCOUNT" => "La lingua e la località da utilizzare per il tuo account",
+ "INVALID" => "<strong>{{locale}}</strong> non è una località valida.",
+
+
+ ],
+ "LOGIN" => [
+ "@TRANSLATION" => "Accesso",
+ "ALREADY_COMPLETE" => "Sei già loggato!",
+ "SOCIAL" => "O accedi con",
+ "REQUIRED" => "Devi essere loggato per accedere a questa risorsa"
+ ],
+ "LOGOUT" => "Logout",
+
+ "NAME" => "Nome",
+
+ "NAME_AND_EMAIL" => "Nome e email",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Accedi al tuo account {{site_name}} o registrati per un nuovo account.",
+ "SUBTITLE" => "Registrati gratuitamente o accedi con un account esistente.",
+ "TITLE" => "Iniziamo!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Password",
+
+ "BETWEEN" => "La password deve essere tra {{min}} e i {{max}} caratteri",
+
+ "CONFIRM" => "Conferma la password",
+ "CONFIRM_CURRENT" => "Conferma la password attuale",
+ "CONFIRM_NEW" => "Conferma la tua nuova password",
+ "CONFIRM_NEW_EXPLAIN" => "Inserisci nuovamente la nuova password",
+ "CONFIRM_NEW_HELP" => "Richiesto solo se si seleziona una nuova password",
+ "CREATE" => [
+ "@TRANSLATION" => "Crea password",
+ "PAGE" => "Scegli una password per il tuo nuovo account.",
+ "SET" => "Imposta password e accedi"
+ ],
+ "CURRENT" => "Password attuale",
+ "CURRENT_EXPLAIN" => "Devi confermare la tua password corrente per apportare modifiche",
+
+ "FORGOTTEN" => "Password dimenticata",
+ "FORGET" => [
+ "@TRANSLATION" => "Ho dimenticato la mia password",
+
+ "COULD_NOT_UPDATE" => "Password non aggiornata",
+ "EMAIL" => "Inserisci l'indirizzo email che hai utilizzato per iscriverti. Un collegamento con le istruzioni per reimpostare la tua password verrà inviata via email.",
+ "EMAIL_SEND" => "Email link di resetta password",
+ "INVALID" => "Questa richiesta di ripristino della password non è stata trovata o è scaduta. Prova a <a href=\"{{url}}\">riprovare</a> la tua richiesta.",
+ "PAGE" => "Ottieni un collegamento per reimpostare la tua password.",
+ "REQUEST_CANNED" => "Richiesta di recupero password cancellata.",
+ "REQUEST_SENT" => "Se l'email <strong>{{email}}</strong> corrisponde a un account nel nostro sistema, verrà inviato un collegamento per la reimpostazione della password a <strong>{{email}}</strong>."
+ ],
+
+ "HASH_FAILED" => "Hash della password fallito. Contatta l'amministratore di sistema.",
+ "INVALID" => "La password corrente non corrisponde con quella in memoria",
+ "NEW" => "Nuova Password",
+ "NOTHING_TO_UPDATE" => "Non puoi aggiornare con la stessa password",
+
+ "RESET" => [
+ "@TRANSLATION" => "Resetta la Password",
+ "CHOOSE" => "Inserisci la tua nuova password",
+ "PAGE" => "Scegli una nuova password per il tuo account.",
+ "SEND" => "Impostare nuova password e accedere"
+ ],
+
+ "UPDATED" => "Password aggiornata"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Impostazioni del profilo",
+ "UPDATED" => "Le impostazioni del profilo sono aggiornate"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "Il limite di velocità per questa azione è stato superato. Devi aspettare un altro {{delay}} secondi prima che ti sia permesso di fare un altro tentativo.",
+ "REGISTER" => "Registrare",
+ "REGISTER_ME" => "Iscrivimi",
+ "REGISTRATION" => [
+ "BROKEN" => "Ci dispiace, c'è un problema con il nostro processo di registrazione dell'account. Vi preghiamo di contattarci direttamente per assistenza.",
+ "COMPLETE_TYPE1" => "Sei stato registrato con successo ora puoi eseguire il login",
+ "COMPLETE_TYPE2" => "Sei stato registrato con successo. Riceverai presto una mail a <strong>{{email}}</strong> per l'attivazione. Devi attivare il tuo account prima di eseguire il login.",
+ "DISABLED" => "La registrazione di nuovi account è stata bloccata",
+ "LOGOUT" => "Non è possibile registrare un account mentre si è loggati",
+ "WELCOME" => "La registrazione è semplice e veloce"
+ ],
+ "REMEMBER_ME" => "Ricordami!",
+ "REMEMBER_ME_ON_COMPUTER" => "Ricordami su questo computer (non consigliato per i computer pubblici)",
+
+ "SIGN_IN_HERE" => "Hai già un account? <a href=\"{{url}}\">Accedi qui</a>",
+ "SIGNIN" => "Accedi",
+ "SIGNIN_OR_REGISTER" => "Accedi o registri",
+ "SIGNUP" => "Registrazione",
+
+ "TOS" => "Termini e condizioni",
+ "TOS_AGREEMENT" => "Registrando un account con {{site_title}}, accetti il <a {{link_attributes | raw}}>termini e condizioni</a>.",
+ "TOS_FOR" => "Termini e condizioni per {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Username",
+
+ "CHOOSE" => "Inserisci il tuo username",
+ "INVALID" => "Username non valido",
+ "IN_USE" => "Il nome utente '{{user_name}}' è già in uso",
+ "NOT_AVAILABLE" => "Il nome utente <strong>{{user_name}}</strong> non è disponibile. Scegli un nome diverso, oppure fai clic su \"suggerisci\"."
+ ],
+
+ "USER_ID_INVALID" => "User ID richiesto non è valido",
+ "USER_OR_EMAIL_INVALID" => "L'indirizzo mail o il nome utente non sono validi",
+ "USER_OR_PASS_INVALID" => "Il nome utente o la password non sono validi",
+
+ "WELCOME" => "Bentornato, {{display_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/it_IT/validate.php b/main/app/sprinkles/account/locale/it_IT/validate.php
new file mode 100755
index 0000000..713ccba
--- /dev/null
+++ b/main/app/sprinkles/account/locale/it_IT/validate.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Italian message token translations for the 'account' sprinkle.
+ * This translation was generated with Google translate. Please contribute if you are a native speaker.
+ *
+ * @package userfrosting\i18n\it
+ * @author Alexander Weissman
+ * @author Pietro Marangon (@Pe46dro)
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "I due campi devono combaciare",
+ "USERNAME" => "L'username può essere composto da caratteri alfanumerici, '.', '-', e '_'."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/pt_PT/messages.php b/main/app/sprinkles/account/locale/pt_PT/messages.php
new file mode 100755
index 0000000..3db4200
--- /dev/null
+++ b/main/app/sprinkles/account/locale/pt_PT/messages.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Portuguese message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\pt
+ * @author Bruno Silva (brunomnsilva@gmail.com)
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Conta",
+
+ "ACCESS_DENIED" => "Hmm, parece que não tem permissões para fazer isso.",
+
+ "DISABLED" => "Esta conta foi desativada. Por favor contacte-nos para mais informações.",
+
+ "EMAIL_UPDATED" => "Email da conta atualizado",
+
+ "INVALID" => "Esta conta não existe. Pode ter sido removida. Por favor contacte-nos para mais informações.",
+
+ "MASTER_NOT_EXISTS" => "Não pode registrar uma conta enquanto a conta principal não for criada!",
+ "MY" => "A minha conta",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "A sua sessão foi comprometida. Deverá fechar todas as sessões, voltar a iniciar sessão e verificar que os seus dados não foram alterados por alheios.",
+ "TITLE" => "A sua sessão pode ter sido comprometida"
+ ],
+ "SESSION_EXPIRED" => "A sua sessão expirou. Por favor inicie nova sessão.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Definições de conta",
+ "DESCRIPTION" => "Atualize as suas definições, incluindo email, nome e password.",
+ "UPDATED" => "Definições de conta atualizadas"
+ ],
+
+ "TOOLS" => "Ferramentas de conta",
+
+ "UNVERIFIED" => "A sua conta ainda não foi verificada. Consulte o seu email (incluindo a pasta de spam) para instruções de ativação.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "Enviámos um link de verificação para o endereço {{email}}. Por favor consulte o seu email (incluindo a pasta de spam).",
+ "RESEND" => "Enviar novamente email de verificação",
+ "COMPLETE" => "Verificou com sucesso a sua conta. Pode iniciar sessão.",
+ "EMAIL" => "Por favor introduza o endereço de email que utilizou no registro e um email de verificação será enviado.",
+ "PAGE" => "Reenviar email de verificação para a sua nova conta.",
+ "SEND" => "Enviar email com link de verificação",
+ "TOKEN_NOT_FOUND" => "Token de verificação inexistente / Conta já verificada",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "Não existe nenhuma conta para <strong>{{email}}</strong>.",
+ "IN_USE" => "O email <strong>{{email}}</strong> já se encontra em uso."
+ ],
+
+ "FIRST_NAME" => "Primeiro nome",
+
+ "HEADER_MESSAGE_ROOT" => "INICIOU SESSÃO COM A CONTA ROOT",
+
+ "LAST_NAME" => "Último nome",
+
+ "LOCALE.ACCOUNT" => "Linguagem e localização a utilizar na sua conta",
+
+ "LOGIN" => [
+ "@TRANSLATION" => "Entrar",
+
+ "ALREADY_COMPLETE" => "Sessão já iniciada!",
+ "SOCIAL" => "Ou inicie sessão com",
+ "REQUIRED" => "Lamentamos, tem de iniciar sessão para aceder a este recurso."
+ ],
+
+ "LOGOUT" => "Sair",
+
+ "NAME" => "Nome",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Inicie sessão na sua conta {{site_name}}, ou registre-se para uma nova conta.",
+ "SUBTITLE" => "Registre-se gratuitamente, ou inicie sessão com uma conta existente.",
+ "TITLE" => "Vamos começar!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Password",
+
+ "BETWEEN" => "Entre {{min}}-{{max}} carateres",
+
+ "CONFIRM" => "Confirme a password",
+ "CONFIRM_CURRENT" => "Por favor confirme a sua password atual",
+ "CONFIRM_NEW" => "Confirmar Nova Password",
+ "CONFIRM_NEW_EXPLAIN" => "Re-introduza a sua nova password",
+ "CONFIRM_NEW_HELP" => "Apenas necessário se escolher uma nova password",
+ "CURRENT" => "Password Atual",
+ "CURRENT_EXPLAIN" => "Tem de confirmar a sua password atual para efetuar alterações",
+
+ "FORGOTTEN" => "Password Esquecida",
+ "FORGET" => [
+ "@TRANSLATION" => "Esqueci a minha password",
+
+ "COULD_NOT_UPDATE" => "Não foi possível atualizar a password.",
+ "EMAIL" => "Por favor introduza o endereço de email que utilizou no registro. Enviaremos um email com instruções para efetuar o reset à sua password.",
+ "EMAIL_SEND" => "Enviar email com link de reset da password",
+ "INVALID" => "This password reset request could not be found, or has expired. Please try <a href=\"{{url}}\">resubmitting your request<a>.",
+ "PAGE" => "Obtenha um link para fazer reset à sua password.",
+ "REQUEST_CANNED" => "Pedido de password esquecida foi cancelado.",
+ "REQUEST_SENT" => "Se o email <strong>{{email}}</strong> corresponder a uma conta em nosso sistema, um link de redefinição de senha será enviado para <strong>{{email}}</strong>."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "Reset Password",
+ "CHOOSE" => "Por favor escolha uma nova password para continuar.",
+ "PAGE" => "Escolha uma nova password para a sua conta.",
+ "SEND" => "Definir nova password e registrar"
+ ],
+
+ "HASH_FAILED" => "Falhou o hashing da password. Por favor contacte um administrador do site.",
+ "INVALID" => "A password atual não coincide com a que temos em sistema",
+ "NEW" => "Nova Password",
+ "NOTHING_TO_UPDATE" => "Não pode atualizar para a mesma password",
+ "UPDATED" => "Password da conta foi atualizada"
+ ],
+
+ "REGISTER" => "Registrar",
+ "REGISTER_ME" => "Registrar-me",
+
+ "REGISTRATION" => [
+ "BROKEN" => "Lamentamos, existe um problema com o nosso processo de registro. Contacte-nos diretamente para assistência.",
+ "COMPLETE_TYPE1" => "Registrou-se com sucesso. Pode iniciar sessão.",
+ "COMPLETE_TYPE2" => "Registrou-se com sucesso. Receberá em breve um email de verificação contendo um link para verificar a sua conta. Não será possível iniciar sessão até completar este passo.",
+ "DISABLED" => "Lamentamos, o registro de novas contas foi desativado.",
+ "LOGOUT" => "Não pode registrar uma nova conta enquanto tiver sessão iniciada. Por favor feche a sua sessão primeiro.",
+ "WELCOME" => "O registro é rápido e simples."
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "Excedeu o número de tentativas para esta ação. Tem de aguardar {{delay}} segundos até lhe ser permitida nova tentativa.",
+ "REMEMBER_ME" => "Lembrar de mim!",
+ "REMEMBER_ME_ON_COMPUTER" => "Lembrar de mim neste computador (não recomendado em computadores públicos)",
+
+ "SIGNIN" => "Iniciar Sessão",
+ "SIGNIN_OR_REGISTER" => "Iniciar sessão ou registrar",
+ "SIGNUP" => "Registrar",
+
+ "TOS" => "Termos e Condições",
+ "TOS_AGREEMENT" => "Ao registrar uma conta em {{site_title}}, está a aceitar os <a {{link_attributes | raw}}>termos e condições</a>.",
+ "TOS_FOR" => "Termos e Condições para {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Nome de utilizador",
+
+ "CHOOSE" => "Escolha um nome de utilizador único",
+ "INVALID" => "Nome de utilizador inválido",
+ "IN_USE" => "O nome de utilizador <strong>{{user_name}}</strong> já se encontra em uso."
+ ],
+
+ "USER_ID_INVALID" => "O id de utilizador solicitado não existe.",
+ "USER_OR_EMAIL_INVALID" => "Nome de utilizador ou endereço de email inválidos.",
+ "USER_OR_PASS_INVALID" => "Nome de utilizador ou password inválidos.",
+
+ "WELCOME" => "Bem-vindo, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/pt_PT/validate.php b/main/app/sprinkles/account/locale/pt_PT/validate.php
new file mode 100755
index 0000000..c05f14c
--- /dev/null
+++ b/main/app/sprinkles/account/locale/pt_PT/validate.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Portuguese message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\pt
+ * @author Bruno Silva (brunomnsilva@gmail.com)
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "A password e respetiva confirmação têm de coincidir."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/ru_RU/messages.php b/main/app/sprinkles/account/locale/ru_RU/messages.php
new file mode 100755
index 0000000..328db13
--- /dev/null
+++ b/main/app/sprinkles/account/locale/ru_RU/messages.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Russian message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\ru_RU
+ * @author @rendername
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Аккаунт",
+
+ "ACCESS_DENIED" => "Для получения доступа у вас недостаточно прав.",
+
+ "DISABLED" => "Аккаунт отключен. Пожалуйста, свяжитесь с нами для получения дополнительной информации.",
+
+ "EMAIL_UPDATED" => "Email аккаунта обновлён",
+
+ "INVALID" => "Этот аккаунт не существует. Возможно, он удалён. Пожалуйста, свяжитесь с нами для получения дополнительной информации.",
+
+ "MASTER_NOT_EXISTS" => "Вы не можете зарегистрировать аккаунт до тех пор, пока основная учётная запись не будет создана!",
+ "MY" => "Мой профиль",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Ваша сессия была скомпрометирована. Вы должны выйти на всех устройствах, а затем снова войти и убедиться, что ваши данные не были изменены.",
+ "TITLE" => "Возможно, ваш аккаунт был скомпрометированн",
+ "TEXT" => "Возможно, кто-то использовал ваши данные для входа на эту страницу. В целях безопасности все сеансы были завершены. Пожалуйста, повторно <a href=\"{{url}}\"> войдите </a> и проверьте свой аккаунт на подозрительную активность. Рекомендуем сменить пароль."
+ ],
+ "SESSION_EXPIRED" => "Срок вашей сессии истек. Пожалуйста войдите еще раз.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Настройки аккаунта",
+ "DESCRIPTION" => "Обновите настройки своего аккаунта, включая адрес электронной почты, имя и пароль.",
+ "UPDATED" => "Данные аккаунта обновлены"
+ ],
+
+ "TOOLS" => "Инструменты аккаунта",
+
+ "UNVERIFIED" => "Ваш аккаунт ещё не подтверждён. Проверьте вашу email почту, в том числе папку спам и следуйте инструкциям.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "Мы отправили на ваш email новую ссылку для активации {{email}}. Пожалуйста, проверьте папку \"Входящие\" и \"Спам\".",
+ "RESEND" => "Повторно отправить письмо с подтверждением",
+ "COMPLETE" => "Вы успешно подтвердили свой аккаунт. Теперь вы можете войти.",
+ "EMAIL" => "Введите email, который вы использовали для регистрации, вам будет повторно отправлено письмо с подтверждением.",
+ "PAGE" => "Повторно оправить письмо подтверждения на email для нового аккаунта.",
+ "SEND" => "Проверка по электронной почте для аккаунта",
+ "TOKEN_NOT_FOUND" => "Код подтверждения не действителен либо аккаунт уже подтверждён",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "Нет не одного аккаунта с <strong> {{email}} </strong>.",
+ "IN_USE" => "Email <strong>{{email}}</strong> уже используется.",
+ "VERIFICATION_REQUIRED" => "Email (указывайте верный - необходим для активации!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Имя пользователя или Email",
+
+ "FIRST_NAME" => "Имя",
+
+ "HEADER_MESSAGE_ROOT" => "ВЫ АВТОРИЗОВАНЫ С СУПЕР-ПРАВАМИ",
+
+ "LAST_NAME" => "Фамилия",
+ "LOCALE" => [
+ "ACCOUNT" => "Основной язык для вашего аккаунта",
+ "INVALID" => "<strong>{{locale}}</strong> язык недопустим."
+ ],
+ "LOGIN" => [
+ "@TRANSLATION" => "Вход",
+ "ALREADY_COMPLETE" => "Вы уже выполнили вход!",
+ "SOCIAL" => "Или войти через",
+ "REQUIRED" => "Извините, Вы должны авторизоваться для доступа к этому ресурсу."
+ ],
+ "LOGOUT" => "Выход",
+
+ "NAME" => "Имя",
+
+ "NAME_AND_EMAIL" => "Имя и email",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "Войдите в свой аккаунт {{site_name}}, или Зарегистрируйтесь.",
+ "SUBTITLE" => "Зарегистрироваться или войти в существующий аккаунт.",
+ "TITLE" => "Приступим!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Пароль",
+
+ "BETWEEN" => "Кол-во {{min}}-{{max}} символов",
+
+ "CONFIRM" => "Подтверждение пароля",
+ "CONFIRM_CURRENT" => "Пожалуйста, введите ваш текущий пароль",
+ "CONFIRM_NEW" => "Подтвердите новый пароль",
+ "CONFIRM_NEW_EXPLAIN" => "Повторно введите Ваш новый пароль",
+ "CONFIRM_NEW_HELP" => "Требуется только при выборе нового пароля",
+ "CREATE" => [
+ "@TRANSLATION" => "Создать пароль",
+ "PAGE" => "Выберите пароль для вашего аккаунта.",
+ "SET" => "Установить пароль и войти"
+ ],
+ "CURRENT" => "Текущий пароль",
+ "CURRENT_EXPLAIN" => "Для продолжения вы должны ввести текущий пароль",
+
+ "FORGOTTEN" => "Забытый пароль?",
+ "FORGET" => [
+ "@TRANSLATION" => "Я забыл свой пароль",
+
+ "COULD_NOT_UPDATE" => "Не удалось обновить пароль.",
+ "EMAIL" => "Пожалуйста, введите адрес электронной почты, который Вы использовали при регистрации. Ссылка с инструкцией по сбросу пароля будет отправлена вам по электронной почте.",
+ "EMAIL_SEND" => "Ссылка сброса пароля по Email",
+ "INVALID" => "Этот запрос сброса пароля не может быть найден, или истек. Пожалуйста, попробуйте <a href=\"{{url}}\"> повторно сбросить пароль<a>.",
+ "PAGE" => "Получите ссылку для сброса пароля.",
+ "REQUEST_CANNED" => "Запрос на сброс пароля отменен.",
+ "REQUEST_SENT" => "Если email <strong>{{email}}</strong> существует в нашей системе у какого-либо аккаунта, ссылка на сброс пароля будет направлена на <strong>{{email}}</strong>."
+ ],
+
+ "HASH_FAILED" => "Хэширование пароля не удалось. Пожалуйста, попробуйте другой пароль, либо свяжитесь с администратором сайта.",
+ "INVALID" => "Текущий пароль не соответствует тому, который задан в системе.",
+ "NEW" => "Новый пароль",
+ "NOTHING_TO_UPDATE" => "Невозможно обновить с тем же паролем",
+
+ "RESET" => [
+ "@TRANSLATION" => "Сбросить пароль",
+ "CHOOSE" => "Пожалуйста, выберите новый пароль, чтобы продолжить.",
+ "PAGE" => "Выберите новый пароль для вашего аккаунта.",
+ "SEND" => "Задать новый пароль и войти"
+ ],
+
+ "UPDATED" => "Пароль аккаунта обновлён"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Настройки профиля",
+ "UPDATED" => "Настройки профиля обновлены"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "Превышен лимит попыток для этого действия. Вы должны подождать еще {{delay}} секунд, прежде чем вам вам будет разрешено сделать ещё попытку.",
+
+ "REGISTER" => "Регистрация",
+ "REGISTER_ME" => "Зарегистрируйте меня",
+ "REGISTRATION" => [
+ "BROKEN" => "К сожалению, есть проблема с регистрации аккаунта. Свяжитесь с нами напрямую для получения помощи.",
+ "COMPLETE_TYPE1" => "Вы успешно зарегистрировались. Теперь вы можете войти.",
+ "COMPLETE_TYPE2" => "Вы успешно зарегистрировались. Ссылка для активации вашего аккаунта была отправлена на <strong>{{email}}</strong>. Вы сможете войти в систему только после активации аккаунта.",
+ "DISABLED" => "Извините, регистрация аккаунта была отключена.",
+ "LOGOUT" => "Извините, вы не можете зарегистрироваться когда уже авторизовались в системе. Сначала выйдите из системы.",
+ "WELCOME" => "Быстрая и простая регистрация."
+ ],
+ "REMEMBER_ME" => "Запомнить",
+ "REMEMBER_ME_ON_COMPUTER" => "Запомнить меня на этом компьютере (не рекомендуется для общедоступных компьютеров)",
+
+ "SIGN_IN_HERE" => "Уже есть аккаунт? <a href=\"{{url}}\"> войти.</a>",
+ "SIGNIN" => "Вход",
+ "SIGNIN_OR_REGISTER" => "Регистрация или вход",
+ "SIGNUP" => "Вход",
+
+ "TOS" => "Пользовательское соглашение",
+ "TOS_AGREEMENT" => "Регистрируя аккаунт на {{site_title}}, вы принимаете <a {{link_attributes | raw}}> условия и положения</a>.",
+ "TOS_FOR" => "Правила и условия для {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Пользователь",
+
+ "CHOOSE" => "Выберите имя пользователя",
+ "INVALID" => "Недопустимое имя пользователя",
+ "IN_USE" => "<strong>{{user_name}}</strong> имя пользователя уже используется.",
+ "NOT_AVAILABLE" => "Имя пользователя <strong>{{user_name}}</strong> не доступно. Выберите другое имя или нажмите кнопку «предложить»."
+ ],
+
+ "USER_ID_INVALID" => "ID запрашиваемого пользователя не существует.",
+ "USER_OR_EMAIL_INVALID" => "Имя пользователя или email не верный.",
+ "USER_OR_PASS_INVALID" => "Пользователь не найден или пароль является недействительным.",
+
+ "WELCOME" => "Добро пожаловать, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/ru_RU/validate.php b/main/app/sprinkles/account/locale/ru_RU/validate.php
new file mode 100755
index 0000000..8ede5d8
--- /dev/null
+++ b/main/app/sprinkles/account/locale/ru_RU/validate.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Russian message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\ru_RU
+ * @author @rendername
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "Пароли не совпадают.",
+ "USERNAME" => "Имя может состоять только из строчных букв, цифр, '.', '-' и «_»."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/th_TH/messages.php b/main/app/sprinkles/account/locale/th_TH/messages.php
new file mode 100755
index 0000000..642a7c5
--- /dev/null
+++ b/main/app/sprinkles/account/locale/th_TH/messages.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "บัญชี",
+
+ "ACCESS_DENIED" => "หืมม ดูเหมือนว่าคุณไม่ได้รับอนุญาตให้ทำเช่นนั้น",
+
+ "DISABLED" => "บัญชีนี้ถูกปิดการใช้งานไปแล้ว กรุณาติดต่อเราสำหรับข้อมูลเพิ่มเติม",
+
+ "EMAIL_UPDATED" => "ปรับปรุงบัญชีอีเมลแล้ว",
+
+ "INVALID" => "ไม่พบบัญชีนี้ มันอาจถูกลบไปแล้ว กรุณาติดต่อเราสำหรับข้อมูลเพิ่มเติม",
+
+ "MASTER_NOT_EXISTS" => "คุณไม่สามารถสมัครสมาชิกได้จนกว่าจะสร้างบัญชีหลัก!",
+ "MY" => "บัญชีของฉัน",
+
+ "SESSION_COMPROMISED" => "เซสชันของคุณถูกลักลอบใช้ คุณควรจะออกจากระบบบนอุปกรณ์ทั้งหมดแล้วกลับเข้าสู่ระบบและตรวจสอบให้แน่ใจว่าไม่มีการแก้ไขข้อมูลของคุณ",
+ "SESSION_COMPROMISED_TITLE" => "บัญชีของคุณอาจถูกบุกรุก",
+ "SESSION_EXPIRED" => "เซสชันของคุณหมดอายุ กรุณาเข้าสู่ระบบอีกครั้ง",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "การตั้งค่าบัญชี",
+ "DESCRIPTION" => "ปรับปรุงการตั้งค่าบัญชีของคุณ รวมไปถึงอีเมล ชื่อ และรหัสผ่าน",
+ "UPDATED" => "ปรับปรุงการตั้งค่าบัญชีของคุณแล้ว"
+ ],
+
+ "TOOLS" => "เครื่องมือบัญชี",
+
+ "UNVERIFIED" => "บัญชีของคุณยังไม่ได้รับการยืนยัน กรุณาตรวจสอบกล่องอีเมลและจดหมายขยะของคุณสำหรับขั้นตอนการเปิดใช้งานบัญชี",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "เราได้ส่งลิงก์สำหรับการยืนยันใหม่ไปยังอีเมล {{email}} กรุณาตรวจสอบอีเมลนี้ในกล่องอีเมลและจดหมายขยะของคุณ",
+ "RESEND" => "ส่งอีเมลยืนยันอีกครั้ง",
+ "COMPLETE" => "คุณได้ยืนยันอีเมลของคุณเรียบร้อยแล้ว คุณสามารถเข้าสู่ระบบได้ทันที",
+ "EMAIL" => "กรุณากรอกอีเมลที่คุณได้ใช้สมัครไว้แล้วอีเมลยืนยันจะถูกส่งไปให้ใหม่",
+ "PAGE" => "ส่งอีเมลยืนยันสำหรับบัญชีของฉันใหม่",
+ "SEND" => "ส่งอีเมลยืนยันให้บัญชีของฉัน",
+ "TOKEN_NOT_FOUND" => "ไม่พบโทเคนยืนยันอีเมล / บัญชีนี้ได้ยืนยันแล้ว",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "อีเมล <strong>{{email}}</strong> ไม่มีอยู่จริง",
+ "IN_USE" => "อีเมล <strong>{{email}}</strong> ได้ถูกใช้งานแล้ว"
+ ],
+
+ "FIRST_NAME" => "ชื่อจริง",
+
+ "HEADER_MESSAGE_ROOT" => "คุณได้เข้าสู่ระบบเป็นผู้ดูแลสูงสุด",
+
+ "LAST_NAME" => "นามสกุล",
+
+ "LOCALE.ACCOUNT" => "ภาษาและสถานที่ที่จะใช้สำหรับบัญชีของคุณ",
+
+ "LOGIN" => [
+ "@TRANSLATION" => "เข้าสู่ะระบบ",
+
+ "ALREADY_COMPLETE" => "คุณได้เข้าสู่ระบบอยู่แล้ว!",
+ "SOCIAL" => "หรือเข้าสู่ระบบด้วย",
+ "REQUIRED" => "ขออภัย คุณจะต้องเข้าสู่ระบบเพื่อเข้าถึงส่วนนี้"
+ ],
+
+ "LOGOUT" => "ออกจากระบบ",
+
+ "NAME" => "ชื่อ",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "เข้าสู่ระบบไปยังบัญชี {{site_name}} หรือสมัครสมาชิกสำหรับบัญชีใหม่",
+ "SUBTITLE" => "สมัครสมาชิกฟรี หรือเข้าสู่ระบบด้วยบัญชีที่มีอยู่",
+ "TITLE" => "มาเริ่มกันเลย!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "รหัสผ่าน",
+
+ "BETWEEN" => "ระหว่าง {{min}}-{{max}} ตัวอักษร",
+
+ "CONFIRM" => "ยืนยันรหัสผ่าน",
+ "CONFIRM_CURRENT" => "กรุณายืนยันรหัสผ่านปัจจุบันของคุณ",
+ "CONFIRM_NEW" => "ยืนยันรหัสผ่านใหม่",
+ "CONFIRM_NEW_EXPLAIN" => "กรอกรหัสผ่านใหม่ของคุณอีกครั้ง",
+ "CONFIRM_NEW_HELP" => "กรอกเฉพาะเมื่อคุณต้องการตั้งรหัสผ่านใหม่",
+ "CURRENT" => "รหัสผ่านปัจจุบัน",
+ "CURRENT_EXPLAIN" => "คุณจะต้องยืนยันรหัสผ่านปัจจุบันเพื่อแก้ไขข้อมูล",
+
+ "FORGOTTEN" => "ลืมรหัสผ่าน",
+ "FORGET" => [
+ "@TRANSLATION" => "ฉันลืมรหัสผ่านของฉัน",
+
+ "COULD_NOT_UPDATE" => "ไม่สามารถปรับปรุงรหัสผ่าน",
+ "EMAIL" => "กรุณากรอกที่อยู่อีเมลที่คุณเคยใช้เข้าสู่ระบบ ลิงก์ขั้นตอนการรีเซ็ตรหัสผ่านของคุณจะถูกส่งไปให้คุณ",
+ "EMAIL_SEND" => "ลิงก์รีเซ็ตรหัสผ่านจากอีเมล",
+ "INVALID" => "ขอรีเซ็ตรหัสผ่านนี้ไม่มีอยู่ หรือหมดอายุไปแล้ว กรุณาลอง <a href=\"{{url}}\">ส่งคำขอของคุณอีกครั้ง<a>",
+ "PAGE" => "รับลิงก์สำหรับการรีเซ็ตรหัสผ่านของคุณ",
+ "REQUEST_CANNED" => "คำขอลืมรหัสผ่านได้ถูกยกเลิก",
+ "REQUEST_SENT" => "หากอีเมล <strong>{{email}}</strong> ตรงกับบัญชีในระบบของเราลิงก์การรีเซ็ตรหัสผ่านจะถูกส่งไปที่ <strong>{{email}}</strong>"
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "รีเซ็ตรหัสผ่าน",
+ "CHOOSE" => "กรุณาเลือกรหัสผ่านใหม่เพื่อดำเนินการต่อ",
+ "PAGE" => "เลือกรหัสผ่านใหม่สำหรับบัญชีของคุณ",
+ "SEND" => "ตั้งรหัสผ่านใหม่และเข้าสู่ระบบ"
+ ],
+
+ "HASH_FAILED" => "เข้ารหัสรหัสผ่านล้มเหลว กรุณาติดต่อผู้ดูแลระบบของเว็บไซต์",
+ "INVALID" => "รหัสผ่านปัจจุบันไม่ตรงกับรหัสผ่านที่เราบันทึกไว้",
+ "NEW" => "รหัสผ่านใหม่",
+ "NOTHING_TO_UPDATE" => "คุณไม่สามารถปรังปรุงด้วยรหัสผ่านเดียวกัน",
+ "UPDATED" => "ปรังปรุงรหัสผ่านของบัญชีแล้ว"
+ ],
+
+ "REGISTER" => "สมัครสมาชิก",
+ "REGISTER_ME" => "ให้ฉันสมัครสมาชิกด้วย",
+
+ "REGISTRATION" => [
+ "BROKEN" => "เราขออภัย มันมีปัญหาในการดำเนินการสมัครสมาชิกของเรา กรุณาติดต่อเราโดยตรงเพื่อขอความช่วยเหลือ",
+ "COMPLETE_TYPE1" => "คุณได้สมัครสมาชิกเรียบร้อยแล้ว คุณสามารถเข้าสู่ระบบได้ทันที",
+ "COMPLETE_TYPE2" => "คุณได้สมัครสมาชิกเรียบร้อยแล้ว คุณจะได้รับอีเมลยืนยันที่มีลิงก์สำหรับเปิดใช้งานบัญชีของคุณอยู่ คุณจะไม่สามารถเข้าสู่ระบบจนกว่าคุณจะยืนยันอีเมลแล้ว",
+ "DISABLED" => "เราขออภัย ระบบสมัครสมาชิกได้ถูกปิดไว้",
+ "LOGOUT" => "เราขออภัย คุณไม่สามารถสมัครสมาชิกขณะที่เข้าสู่ระบบอยู่ กรุณาออกจากระบบก่อน",
+ "WELCOME" => "การสมัครสมาชิกนั้นรวดเร็ว และง่ายดาย"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "ถึงขีดจำกัดสำหรับการกระทำนี้แล้ว คุณจะต้องรออีก {{delay}} วินาที ก่อนที่คุณจะได้รับอนุญาตให้ลองใหม่อีกครั้ง",
+ "REMEMBER_ME" => "จำฉันไว้ในระบบ!",
+ "REMEMBER_ME_ON_COMPUTER" => "จำฉันไว้ในระบบบนคอมพิวเตอร์นี้ (ไม่แนะนำสำหรับคอมพิวเตอร์สาธารณะ)",
+
+ "SIGNIN" => "เข้าสู่ะระบบ",
+ "SIGNIN_OR_REGISTER" => "เข้าสู่ระบบหรือสมัครสมาชิก",
+ "SIGNUP" => "สมัครสมาชิก",
+
+ "TOS" => "ข้อตกลงและเงื่อนไข",
+ "TOS_AGREEMENT" => "ในการสมัครสมาชิกกับ {{site_title}} หมายถึงคุณยอมรับ <a {{link_attributes}}>ข้อตกลงและเงื่อนไข</a> แล้ว",
+ "TOS_FOR" => "ข้อตกลงและเงื่อนไขสำหรับ {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "ชื่อผู้ใช้",
+
+ "CHOOSE" => "เลือกชื่อผู้ใช้ที่เป็นเป็นเอกลักษณ์",
+ "INVALID" => "ชื่อผู้ใช้ไม่ถูกต้อง",
+ "IN_USE" => "ชื่อผู้ใช้ <strong>{{user_name}}</strong> ถูกใช้งานแล้ว"
+ ],
+
+ "USER_ID_INVALID" => "ไม่พบหมายเลขผู้ใช้ที่ร้องขอมา",
+ "USER_OR_EMAIL_INVALID" => "ชื่อผู้ใช้หรือที่อยู่อีเมลไม่ถูกต้อง",
+ "USER_OR_PASS_INVALID" => "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง",
+
+ "WELCOME" => "ยินดีต้อนรับ {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/th_TH/validate.php b/main/app/sprinkles/account/locale/th_TH/validate.php
new file mode 100755
index 0000000..1a2e90a
--- /dev/null
+++ b/main/app/sprinkles/account/locale/th_TH/validate.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "รหัสผ่านและรหัสผ่านยืนยันของคุณจะต้องตรงกัน"
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/tr/messages.php b/main/app/sprinkles/account/locale/tr/messages.php
new file mode 100755
index 0000000..5213490
--- /dev/null
+++ b/main/app/sprinkles/account/locale/tr/messages.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Turkish message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\tr
+ * @author Dumblledore
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "Hesap",
+
+ "ACCESS_DENIED" => "Hmm. görünüşe göre böyle bir şey için izne sahip değilsiniz.",
+
+ "DISABLED" => "Bu hesap durduruldu. Daha çok bilgi için bizimle iletişime geçin.",
+
+ "EMAIL_UPDATED" => "Hesap maili güncellendi",
+
+ "INVALID" => "Bu hesap bulunamadı. Silinmiş olabilir. Daha çok bilgi için bizimle iletişime geçin.",
+
+ "MASTER_NOT_EXISTS" => "Ana hesap oluşturuluncaya kadar bir hesap oluşturamazsın!",
+ "MY" => "Hesabım",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Oturumunuz tehlikeye atıldı. Tüm cihazlardan çıkmanız, daha sonra giriş yapmanız ve bilgilerinizin değiştirilmediğini kontrol etmeniz gerekir.",
+ "TITLE" => "Hesabınız tehlikeye atılmış olabilir",
+ "TEXT" => "Birisi bu sayfayı ele geçirmek için giriş verilerinizi kullanmış olabilir. Güvenliğiniz için tüm oturumlar günlüğe kaydedildi. Lütfen <a href=\"{{url}}\">giriş yapın</a>ve şüpheli hareketler için hesabınızı kontrol edin. Ayrıca şifrenizi değiştirmek isteyebilirsiniz."
+ ],
+ "SESSION_EXPIRED" => "Oturumunuz sona erdi. Lütfen tekrar oturum açın.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "Hesap ayarları",
+ "DESCRIPTION" => "E-posta, isim ve parolanız da dahil olmak üzere hesap ayarlarınızı güncelleyin.",
+ "UPDATED" => "Hesap ayarları güncellendi"
+ ],
+
+ "TOOLS" => "Hesap araçları",
+
+ "UNVERIFIED" => "Hesap henüz onaylanmadı. Hesap etkinleştirme talimatları için e-postalarınızı ve spam klasörünüzü kontrol edin.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "{{email}} için yeni bir doğrulama bağlantısı e-posta ile gönderildi. Lütfen bu e-postanın gelen kutusunu ve spam klasörlerini kontrol edin.",
+ "RESEND" => "Doğrulama e-postasını tekrar gönder",
+ "COMPLETE" => "Hesabınızı başarıyla doğruladınız. Şimdi giriş yapabilirsiniz.",
+ "EMAIL" => "Kaydolmak için kullandığınız e-posta adresinizi giriniz, ve doğrulama e-postanızı tekrar gönderin.",
+ "PAGE" => "Yeni hesabınız için doğrulama e-postasını tekrar gönder.",
+ "SEND" => "Hesabım için doğrulama bağlantısını e-posta ile gönder",
+ "TOKEN_NOT_FOUND" => "Doğrulama belirteci bulunumadı / Hesap zaten doğrulandı",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "<strong>{{email}}</strong> için hesap yoktur.",
+ "IN_USE" => "E-posta <strong>{{email}}</strong> zaten kullanılıyor.",
+ "VERIFICATION_REQUIRED" => "E-posta (doğrulama gerekli - gerçek bir adres kullanın!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Kullanıcı adı veya e-posta adresi",
+
+ "FIRST_NAME" => "Adınız",
+
+ "HEADER_MESSAGE_ROOT" => "Kök kullanıcı olarak giriş yaptın",
+
+ "LAST_NAME" => "Soyadı",
+ "LOCALE" => [
+ "ACCOUNT" => "Hesabınız için kullanılacak dil ve yerel ayar",
+ "INVALID" => "<strong>{{locale}}</strong> geçersiz bir yerel."
+ ],
+ "LOGIN" => [
+ "@TRANSLATION" => "Oturum Aç",
+ "ALREADY_COMPLETE" => "Zaten oturum açtınız!",
+ "SOCIAL" => "Veya şununla oturum aç",
+ "REQUIRED" => "Üzgünüm, bu sayfaya ulaşmak için oturum açmalısın."
+ ],
+ "LOGOUT" => "Oturumu kapat",
+
+ "NAME" => "Ad",
+
+ "NAME_AND_EMAIL" => "Ad ve e-posta",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "{{site_name}} hesabınız ile giriş yapın ya da yeni bir hesap oluşturun.",
+ "SUBTITLE" => "Ücretsiz üye ol veya mevcut bir hesap ile giriş yapın.",
+ "TITLE" => "Hadi başlayalım!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "Parola",
+
+ "BETWEEN" => "{{min}}-{{max}} karakterler arasında",
+
+ "CONFIRM" => "Şifreyi onayla",
+ "CONFIRM_CURRENT" => "Lütfen şuanki parolanızı giriniz",
+ "CONFIRM_NEW" => "Yeni parolayı onayla",
+ "CONFIRM_NEW_EXPLAIN" => "Yeni parolayı tekrar gir",
+ "CONFIRM_NEW_HELP" => "Sadece yeni bir şifre seçerseniz gerekli",
+ "CREATE" => [
+ "@TRANSLATION" => "Parola Oluştur",
+ "PAGE" => "Yeni hesabınız için bir şifre belirleyin.",
+ "SET" => "Parolayı Ayarla ve Giriş Yap"
+ ],
+ "CURRENT" => "Şimdiki Parola",
+ "CURRENT_EXPLAIN" => "Değişiklikler için şimdiki parolanız ile onaylamalısınız",
+
+ "FORGOTTEN" => "Unutulan Şifre",
+ "FORGET" => [
+ "@TRANSLATION" => "Şifremi unuttum",
+
+ "COULD_NOT_UPDATE" => "Şifre güncellenemedi.",
+ "EMAIL" => "Lütfen kaydolmak için kullandığınız e-posta adresini giriniz. Şifrenizi sıfırlama talimatlarıyla bir bir bağlantı e-postanıza gönderilecektir.",
+ "EMAIL_SEND" => "E-posta şifre sıfırlama bağlantısı",
+ "INVALID" => "Bu şifre sıfırlama isteği bulunamadı ya da süresi bitmiş. Lütfen <a href=\"{{url}}\">isteğinizi yeniden göndermeyi<a>deneyin.",
+ "PAGE" => "Şifrenizi sıfırlamak için bir bağlantı oluşturun.",
+ "REQUEST_CANNED" => "Kayıp parola isteği iptal edildi.",
+ "REQUEST_SENT" => "Eğer e-posta<strong>{{email}}</strong> sistemdeki bir hesap ile eşleşirse, bir şifre yenileme bağlantısı<strong>{{email}}</strong> gönderilir."
+ ],
+
+ "HASH_FAILED" => "Parola karma başarısız oldu. Lütfen bir site yöneticisiyle iletişime geçin.",
+ "INVALID" => "Şimdiki şifre kayıt edilen şifre ile eşleşmiyor",
+ "NEW" => "Yeni Şifre",
+ "NOTHING_TO_UPDATE" => "Aynı şifre ile güncelleyemezsiniz",
+
+ "RESET" => [
+ "@TRANSLATION" => "Şifre sıfırlama",
+ "CHOOSE" => "Lütfen devam etmek için yeni bir şifre belirleyiniz.",
+ "PAGE" => "Hesabınız için yeni bir şifre belirleyiniz.",
+ "SEND" => "Yeni şifre ayarla ve giriş yap"
+ ],
+
+ "UPDATED" => "Hesap şifresi güncellendi"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "Profil ayarları",
+ "UPDATED" => "Profil ayarları güncellendi"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "Bu işlem için belirlenen son oran aşıldı. Başka bir deneme yapmanıza izin verilene kadar {{delay}} bir süre beklemelisiniz.",
+
+ "REGISTER" => "Kaydol",
+ "REGISTER_ME" => "Beni kaydet",
+ "REGISTRATION" => [
+ "BROKEN" => "Üzgünüz, hesap kayıt işlemimizde bir sorun var. Lütfen destek almak için doğrudan bizimle iletişime geçin.",
+ "COMPLETE_TYPE1" => "Kaydınız başarıyla tamamlandı. Şimdi giriş yapabilirsiniz.",
+ "COMPLETE_TYPE2" => "Kaydınız başarıyla tamamlandı. Hesabınızı aktifleştirmek için bir bağlantı gönderildi<strong>{{email}}</strong>. Bu adımı tamamlayana kadar oturum açamazsınız.",
+ "DISABLED" => "Üzgünüz, hesap kaydı devre dışı bırakıldı.",
+ "LOGOUT" => "Üzgünüm, oturumunuz açıkken yeni bir hesap oluşturamazsınız. Lütfen önce oturumunuzdan çıkış yapınız.",
+ "WELCOME" => "Kaydolmak hızlı ve basittir."
+ ],
+ "REMEMBER_ME" => "Beni hatırla!",
+ "REMEMBER_ME_ON_COMPUTER" => "Bu bilgisayarda beni hatırla ( genel bilgisayarlar için önerilmez)",
+
+ "SIGN_IN_HERE" => "Zaten bir hesaba sahip misiniz?<a href=\"{{url}}\">burada giriş yap</a>",
+ "SIGNIN" => "Giriş yap",
+ "SIGNIN_OR_REGISTER" => "Giriş yap veya kayıt ol",
+ "SIGNUP" => "Üye ol",
+
+ "TOS" => "Şartlar ve Koşullar",
+ "TOS_AGREEMENT" => "Bir hesap ile kaydolarak {{site_title}} sen kabul edersin <a {{link_attributes | raw}}>şartlar ve koşulları</a>.",
+ "TOS_FOR" => "{{title}} için şartlar ve koşullar",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Kullanıcı Adı",
+
+ "CHOOSE" => "Benzersiz bir kullanıcı adı seç",
+ "INVALID" => "Geçersiz kullanıcı adı",
+ "IN_USE" => "<strong>{{user_name}}</strong> kullanıcı adı zaten mevcut.",
+ "NOT_AVAILABLE" => "<strong>{{user_name}}</strong> kullanıcı adı kullanılamaz. Farklı bir isim veya 'öneriye' tıklayın."
+ ],
+
+ "USER_ID_INVALID" => "İstenen kullanıcı adı mevcut değil.",
+ "USER_OR_EMAIL_INVALID" => "Kullanıcı adı veya e-posta adresi hatalı.",
+ "USER_OR_PASS_INVALID" => "Kullanıcı bulunamadı ya da şifre hatalı.",
+
+ "WELCOME" => "Tekrar Hoşgeldiniz.{{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/tr/validate.php b/main/app/sprinkles/account/locale/tr/validate.php
new file mode 100755
index 0000000..298bdbc
--- /dev/null
+++ b/main/app/sprinkles/account/locale/tr/validate.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Turkish message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\tr
+ * @author Dumblledore
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "Şifreniz ve onaylama şifreniz eşleşmiyor.",
+ "USERNAME" => "Kullanıcı adınız sadece küçük harfler, sayılar, '.', '-', ve '_' içerebilir."
+ ]
+];
diff --git a/main/app/sprinkles/account/locale/zh_CN/messages.php b/main/app/sprinkles/account/locale/zh_CN/messages.php
new file mode 100755
index 0000000..60adcf0
--- /dev/null
+++ b/main/app/sprinkles/account/locale/zh_CN/messages.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Chinese message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\zh_CN
+ * @author @BruceGui (https://github.com/BruceGui)
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "账户",
+
+ "ACCESS_DENIED" => "噢, 你好像没有权限这么做.",
+
+ "DISABLED" => "这个账户已被禁用. 请联系我们获取更多信息.",
+
+ "EMAIL_UPDATED" => "账户邮箱更新成功",
+
+ "INVALID" => "此账户不存在. 可能已被删除. 请联系我们获取更多信息.",
+
+ "MASTER_NOT_EXISTS" => "在创建超级账户之前你不能注册",
+ "MY" => "我的账户",
+
+ "SESSION_COMPROMISED" => "你的会话已泄露. 你应该在所有的设备上注销, 然后再登陆确保你的数据没被修改.",
+ "SESSION_COMPROMISED_TITLE" => "你的账户可能被盗用",
+ "SESSION_EXPIRED" => "会话已过期. 请重新登陆.",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "账户设置",
+ "DESCRIPTION" => "更新你的账户, 包括邮箱、姓名和密码.",
+ "UPDATED" => "账户更新成功"
+ ],
+
+ "TOOLS" => "账户工具",
+
+ "UNVERIFIED" => "你的账户还没有验证. 检查你的(垃圾)邮箱文件夹进行验证.",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "我们发送了新的验证链接 {{email}}. 请检查你的收件箱或垃圾邮件进行验证.",
+ "RESEND" => "重新发送验证邮件",
+ "COMPLETE" => "你已成功验证. 现在可以登陆了.",
+ "EMAIL" => "请输入你登陆时的邮箱, 然后将会发送验证邮件.",
+ "PAGE" => "重新发送验证邮件给你的新账户.",
+ "SEND" => "为我的账户发送验证邮件",
+ "TOKEN_NOT_FOUND" => "验证令牌不存在 / 账户已经验证",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "<strong>{{email}}</strong> 没有账户注册.",
+ "IN_USE" => "邮箱 <strong>{{email}}</strong> 已被使用.",
+ "VERIFICATION_REQUIRED" => "邮箱 (需要进行验证 - 请使用一个有效的!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "用户名或邮箱地址",
+
+ "FIRST_NAME" => "名字",
+
+ "HEADER_MESSAGE_ROOT" => "你现在以超级用户登陆",
+
+ "LAST_NAME" => "姓氏",
+
+ "LOCALE" => [
+ "ACCOUNT" => "设置你账户的地区和语言",
+ "INVALID" => "<strong>{{locale}}</strong> 不是一个有效的地区."
+ ],
+
+ "LOGIN" => [
+ "@TRANSLATION" => "登陆",
+ "ALREADY_COMPLETE" => "你已经登陆!",
+ "SOCIAL" => "用其他方式登陆",
+ "REQUIRED" => "对不起, 你需要登陆才能获取资源."
+ ],
+
+ "LOGOUT" => "注销",
+
+ "NAME" => "名字",
+
+ "NAME_AND_EMAIL" => "名字和邮箱",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "用 {{site_name}} 账户登陆, 或者创建新账户.",
+ "SUBTITLE" => "免费注册, 或用已有账户登陆.",
+ "TITLE" => "让我们开始吧!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "密码",
+
+ "BETWEEN" => "字符长度 {{min}}-{{max}} ",
+
+ "CONFIRM" => "确认密码",
+ "CONFIRM_CURRENT" => "请确认当前密码",
+ "CONFIRM_NEW" => "确认新密码",
+ "CONFIRM_NEW_EXPLAIN" => "重新输入新密码",
+ "CONFIRM_NEW_HELP" => "选择了新密码时才需要",
+ "CURRENT" => "密码正确",
+ "CURRENT_EXPLAIN" => "你必须要确认密码再进行修改",
+
+ "FORGOTTEN" => "忘记密码",
+ "FORGET" => [
+ "@TRANSLATION" => "我忘记了密码",
+
+ "COULD_NOT_UPDATE" => "无法更新密码.",
+ "EMAIL" => "请输入你登陆时的邮箱. 重置密码的链接将会发送给你.",
+ "EMAIL_SEND" => "发送重置密码链接",
+ "INVALID" => "这个重置密码请求无法使用, 或已过期. 请 <a href=\"{{url}}\">重新发送请求<a>.",
+ "PAGE" => "获取重置密码的链接.",
+ "REQUEST_CANNED" => "取消重置请求.",
+ "REQUEST_SENT" => "重置密码的链接已经发送 <strong>{{email}}</strong>."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "重置密码",
+ "CHOOSE" => "请输入新密码.",
+ "PAGE" => "为账户设置新密码.",
+ "SEND" => "设置密码并登陆"
+ ],
+
+ "HASH_FAILED" => "密码验证失败. 请联系网站管理.",
+ "INVALID" => "当前密码无法与记录匹配",
+ "NEW" => "新密码",
+ "NOTHING_TO_UPDATE" => "新密码不能与旧密码相同",
+ "UPDATED" => "账户密码更新成功"
+ ],
+
+ "PROFILE" => [
+ "SETTINGS" => "简介设置",
+ "UPDATED" => "简介设置成功"
+ ],
+
+ "REGISTER" => "注册",
+ "REGISTER_ME" => "注册",
+
+ "REGISTRATION" => [
+ "BROKEN" => "抱歉, 账户注册过程发送错误. 请联系我们寻求帮助.",
+ "COMPLETE_TYPE1" => "你已注册成功. 现在可以登陆了.",
+ "COMPLETE_TYPE2" => "成功注册. 激活链接已经发送给 <strong>{{email}}</strong>. 激活之前无法登陆.",
+ "DISABLED" => "抱歉, 账户注册以禁用.",
+ "LOGOUT" => "抱歉, 登陆时不能注册. 请先注销.",
+ "WELCOME" => "注册简单快速."
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "行动速度过快. 请等 {{delay}} 秒后再尝试新的操作.",
+ "REMEMBER_ME" => "记住我!",
+ "REMEMBER_ME_ON_COMPUTER" => "在此电脑上记住我 (不推荐在公共电脑上)",
+
+ "SIGNIN" => "登陆",
+ "SIGNIN_OR_REGISTER" => "登陆或注册",
+ "SIGNUP" => "注销",
+
+ "TOS" => "条款和说明",
+ "TOS_AGREEMENT" => "在 {{site_title}} 注册, 你需要接收 <a {{link_attributes | raw}}>条款和说明</a>.",
+ "TOS_FOR" => "{{title}}的条款和说明",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "用户名",
+
+ "CHOOSE" => "取一个唯一的用户名",
+ "INVALID" => "无效的用户名",
+ "IN_USE" => "用户名 <strong>{{user_name}}</strong> 已存在.",
+ "NOT_AVAILABLE" => "用户名 <strong>{{user_name}}</strong> 不可用. 重新选择用户名, 或者点击 '建议'."
+ ],
+
+ "USER_ID_INVALID" => "请求的用户不存在.",
+ "USER_OR_EMAIL_INVALID" => "用户名或邮箱无效.",
+ "USER_OR_PASS_INVALID" => "没有发现用户或密码错误.",
+
+ "WELCOME" => "欢迎回来, {{first_name}}"
+];
diff --git a/main/app/sprinkles/account/locale/zh_CN/validate.php b/main/app/sprinkles/account/locale/zh_CN/validate.php
new file mode 100755
index 0000000..3ca368a
--- /dev/null
+++ b/main/app/sprinkles/account/locale/zh_CN/validate.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Chinese message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\zh_CN
+ * @author @BruceGui (https://github.com/BruceGui)
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "密码不一致.",
+ "USERNAME" => "用户名必须以小写字母, 数字, '.', '-', 和 '_'组成."
+ ]
+];
diff --git a/main/app/sprinkles/account/routes/routes.php b/main/app/sprinkles/account/routes/routes.php
new file mode 100755
index 0000000..8198255
--- /dev/null
+++ b/main/app/sprinkles/account/routes/routes.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+$app->group('/account', function () {
+ $this->get('/captcha', 'UserFrosting\Sprinkle\Account\Controller\AccountController:imageCaptcha');
+
+ $this->get('/check-username', 'UserFrosting\Sprinkle\Account\Controller\AccountController:checkUsername');
+
+ $this->get('/forgot-password', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageForgotPassword')
+ ->setName('forgot-password');
+
+ $this->get('/logout', 'UserFrosting\Sprinkle\Account\Controller\AccountController:logout')
+ ->add('authGuard');
+
+ $this->get('/resend-verification', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageResendVerification');
+
+ $this->get('/set-password/confirm', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageResetPassword');
+
+ $this->get('/set-password/deny', 'UserFrosting\Sprinkle\Account\Controller\AccountController:denyResetPassword');
+
+ $this->get('/register', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageRegister')
+ ->add('checkEnvironment')
+ ->setName('register');
+
+ $this->get('/settings', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageSettings')
+ ->add('authGuard');
+
+ $this->get('/sign-in', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageSignIn')
+ ->add('checkEnvironment')
+ ->setName('login');
+
+ $this->get('/suggest-username', 'UserFrosting\Sprinkle\Account\Controller\AccountController:suggestUsername');
+
+ $this->get('/verify', 'UserFrosting\Sprinkle\Account\Controller\AccountController:verify');
+
+ $this->post('/forgot-password', 'UserFrosting\Sprinkle\Account\Controller\AccountController:forgotPassword');
+
+ $this->post('/login', 'UserFrosting\Sprinkle\Account\Controller\AccountController:login');
+
+ $this->post('/register', 'UserFrosting\Sprinkle\Account\Controller\AccountController:register');
+
+ $this->post('/resend-verification', 'UserFrosting\Sprinkle\Account\Controller\AccountController:resendVerification');
+
+ $this->post('/set-password', 'UserFrosting\Sprinkle\Account\Controller\AccountController:setPassword');
+
+ $this->post('/settings', 'UserFrosting\Sprinkle\Account\Controller\AccountController:settings')
+ ->add('authGuard')
+ ->setName('settings');
+
+ $this->post('/settings/profile', 'UserFrosting\Sprinkle\Account\Controller\AccountController:profile')
+ ->add('authGuard');
+});
+
+$app->get('/modals/account/tos', 'UserFrosting\Sprinkle\Account\Controller\AccountController:getModalAccountTos');
diff --git a/main/app/sprinkles/account/schema/requests/account-settings.yaml b/main/app/sprinkles/account/schema/requests/account-settings.yaml
new file mode 100755
index 0000000..4a2d368
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/account-settings.yaml
@@ -0,0 +1,35 @@
+---
+passwordcheck:
+ validators:
+ required:
+ message: PASSWORD.CONFIRM_CURRENT
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&EMAIL"
+ min: 1
+ max: 150
+ message: VALIDATE.LENGTH_RANGE
+ email:
+ message: VALIDATE.INVALID_EMAIL
+password:
+ validators:
+ length:
+ label: "&PASSWORD"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+passwordc:
+ validators:
+ matches:
+ field: password
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.PASSWORD_MISMATCH
+ length:
+ label: "&PASSWORD.CONFIRM"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
diff --git a/main/app/sprinkles/account/schema/requests/account-verify.yaml b/main/app/sprinkles/account/schema/requests/account-verify.yaml
new file mode 100755
index 0000000..01f3155
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/account-verify.yaml
@@ -0,0 +1,6 @@
+---
+token:
+ validators:
+ required:
+ label: validation token
+ message: VALIDATION.REQUIRED
diff --git a/main/app/sprinkles/account/schema/requests/check-username.yaml b/main/app/sprinkles/account/schema/requests/check-username.yaml
new file mode 100755
index 0000000..778b5e5
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/check-username.yaml
@@ -0,0 +1,17 @@
+---
+user_name:
+ validators:
+ length:
+ label: "&USERNAME"
+ min: 1
+ max: 50
+ message: VALIDATE.LENGTH_RANGE
+ no_leading_whitespace:
+ label: "&USERNAME"
+ message: VALIDATE.NO_LEAD_WS
+ no_trailing_whitespace:
+ label: "&USERNAME"
+ message: VALIDATE.NO_TRAIL_WS
+ username:
+ label: "&USERNAME"
+ message: VALIDATE.USERNAME
diff --git a/main/app/sprinkles/account/schema/requests/deny-password.yaml b/main/app/sprinkles/account/schema/requests/deny-password.yaml
new file mode 100755
index 0000000..3b3e919
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/deny-password.yaml
@@ -0,0 +1,5 @@
+---
+token:
+ validators:
+ required:
+ message: PASSWORD.FORGET.INVALID
diff --git a/main/app/sprinkles/account/schema/requests/forgot-password.yaml b/main/app/sprinkles/account/schema/requests/forgot-password.yaml
new file mode 100755
index 0000000..70072b5
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/forgot-password.yaml
@@ -0,0 +1,6 @@
+---
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
diff --git a/main/app/sprinkles/account/schema/requests/login.yaml b/main/app/sprinkles/account/schema/requests/login.yaml
new file mode 100755
index 0000000..b78596a
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/login.yaml
@@ -0,0 +1,19 @@
+---
+user_name:
+ validators:
+ required:
+ label: "&USERNAME"
+ message: VALIDATE.REQUIRED
+ no_leading_whitespace:
+ label: "&USERNAME"
+ message: VALIDATE.NO_LEAD_WS
+ no_trailing_whitespace:
+ label: "&USERNAME"
+ message: VALIDATE.NO_TRAIL_WS
+password:
+ validators:
+ required:
+ label: "&PASSWORD"
+ message: VALIDATE.REQUIRED
+rememberme:
+ default: false
diff --git a/main/app/sprinkles/account/schema/requests/profile-settings.yaml b/main/app/sprinkles/account/schema/requests/profile-settings.yaml
new file mode 100755
index 0000000..c2b5ee8
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/profile-settings.yaml
@@ -0,0 +1,24 @@
+---
+first_name:
+ validators:
+ length:
+ label: "&FIRST_NAME"
+ min: 1
+ max: 20
+ message: VALIDATE.LENGTH_RANGE
+ required:
+ label: "&FIRST_NAME"
+ message: VALIDATE.REQUIRED
+last_name:
+ validators:
+ length:
+ label: "&LAST_NAME"
+ min: 1
+ max: 30
+ message: VALIDATE.LENGTH_RANGE
+locale:
+ validators:
+ required:
+ label: "&LOCALE"
+ domain: server
+ message: VALIDATE.REQUIRED
diff --git a/main/app/sprinkles/account/schema/requests/register.yaml b/main/app/sprinkles/account/schema/requests/register.yaml
new file mode 100755
index 0000000..75dae59
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/register.yaml
@@ -0,0 +1,75 @@
+---
+user_name:
+ validators:
+ length:
+ label: "&USERNAME"
+ min: 1
+ max: 50
+ message: VALIDATE.LENGTH_RANGE
+ no_leading_whitespace:
+ label: "&USERNAME"
+ message: VALIDATE.NO_LEAD_WS
+ no_trailing_whitespace:
+ label: "&USERNAME"
+ message: VALIDATE.NO_TRAIL_WS
+ required:
+ label: "&USERNAME"
+ message: VALIDATE.REQUIRED
+ username:
+ label: "&USERNAME"
+ message: VALIDATE.USERNAME
+first_name:
+ validators:
+ length:
+ label: "&FIRST_NAME"
+ min: 1
+ max: 20
+ message: VALIDATE.LENGTH_RANGE
+ required:
+ label: "&FIRST_NAME"
+ message: VALIDATE.REQUIRED
+last_name:
+ validators:
+ length:
+ label: "&LAST_NAME"
+ min: 1
+ max: 30
+ message: VALIDATE.LENGTH_RANGE
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&EMAIL"
+ min: 1
+ max: 150
+ message: VALIDATE.LENGTH_RANGE
+ email:
+ message: VALIDATE.INVALID_EMAIL
+password:
+ validators:
+ required:
+ label: "&PASSWORD"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&PASSWORD"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+passwordc:
+ validators:
+ required:
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.REQUIRED
+ matches:
+ field: password
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.PASSWORD_MISMATCH
+ length:
+ label: "&PASSWORD.CONFIRM"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+captcha:
+ validators:
diff --git a/main/app/sprinkles/account/schema/requests/resend-verification.yaml b/main/app/sprinkles/account/schema/requests/resend-verification.yaml
new file mode 100755
index 0000000..70072b5
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/resend-verification.yaml
@@ -0,0 +1,6 @@
+---
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
diff --git a/main/app/sprinkles/account/schema/requests/set-password.yaml b/main/app/sprinkles/account/schema/requests/set-password.yaml
new file mode 100755
index 0000000..ae59d1c
--- /dev/null
+++ b/main/app/sprinkles/account/schema/requests/set-password.yaml
@@ -0,0 +1,29 @@
+---
+password:
+ validators:
+ required:
+ label: "&PASSWORD"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&PASSWORD"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+passwordc:
+ validators:
+ required:
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.REQUIRED
+ matches:
+ field: password
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.PASSWORD_MISMATCH
+ length:
+ label: "&PASSWORD.CONFIRM"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+token:
+ validators:
+ required:
+ message: PASSWORD.FORGET.INVALID
diff --git a/main/app/sprinkles/account/src/Account.php b/main/app/sprinkles/account/src/Account.php
new file mode 100755
index 0000000..49c2de9
--- /dev/null
+++ b/main/app/sprinkles/account/src/Account.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account;
+
+use UserFrosting\System\Sprinkle\Sprinkle;
+
+/**
+ * Bootstrapper class for the 'account' sprinkle.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class Account extends Sprinkle
+{
+
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/AuthGuard.php b/main/app/sprinkles/account/src/Authenticate/AuthGuard.php
new file mode 100755
index 0000000..efcfaae
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/AuthGuard.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Slim\Http\Body;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException;
+
+/**
+ * Middleware to catch requests that fail because they require user authentication.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthGuard
+{
+ /**
+ * @var Authenticator
+ */
+ protected $authenticator;
+
+ /**
+ * Constructor.
+ *
+ * @param $authenticator Authenticator The current authentication object.
+ */
+ public function __construct($authenticator)
+ {
+ $this->authenticator = $authenticator;
+ }
+
+ /**
+ * Invoke the AuthGuard middleware, throwing an exception if there is no authenticated user in the session.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
+ * @param \Psr\Http\Message\ResponseInterface $response PSR7 response
+ * @param callable $next Next middleware
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function __invoke($request, $response, $next)
+ {
+ if (!$this->authenticator->check()) {
+ throw new AuthExpiredException();
+ } else {
+ return $next($request, $response);
+ }
+
+ return $response;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Authenticator.php b/main/app/sprinkles/account/src/Authenticate/Authenticator.php
new file mode 100755
index 0000000..5fb8920
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Authenticator.php
@@ -0,0 +1,419 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate;
+
+use Birke\Rememberme\Authenticator as RememberMe;
+use Birke\Rememberme\Storage\PDOStorage as RememberMePDO;
+use Birke\Rememberme\Triplet as RememberMeTriplet;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Session\Session;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountDisabledException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountInvalidException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountNotVerifiedException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\InvalidCredentialsException;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Account\Facades\Password;
+use UserFrosting\Sprinkle\Core\Util\ClassMapper;
+
+/**
+ * Handles authentication tasks.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * Partially inspired by Laravel's Authentication component: https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/SessionGuard.php
+ */
+class Authenticator
+{
+ /**
+ * @var ClassMapper
+ */
+ protected $classMapper;
+
+ /**
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * @var Cache
+ */
+ protected $cache;
+
+ /**
+ * @var bool
+ */
+ protected $loggedOut = false;
+
+ /**
+ * @var RememberMePDO
+ */
+ protected $rememberMeStorage;
+
+ /**
+ * @var RememberMe
+ */
+ protected $rememberMe;
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * Indicates if the user was authenticated via a rememberMe cookie.
+ *
+ * @var bool
+ */
+ protected $viaRemember = false;
+
+ /**
+ * Create a new Authenticator object.
+ *
+ * @param ClassMapper $classMapper Maps generic class identifiers to specific class names.
+ * @param Session $session The session wrapper object that will store the user's id.
+ * @param Config $config Config object that contains authentication settings.
+ * @param mixed $cache Cache service instance
+ */
+ public function __construct(ClassMapper $classMapper, Session $session, $config, $cache)
+ {
+ $this->classMapper = $classMapper;
+ $this->session = $session;
+ $this->config = $config;
+ $this->cache = $cache;
+
+ // Initialize RememberMe storage
+ $this->rememberMeStorage = new RememberMePDO($this->config['remember_me.table']);
+
+ // Get the actual PDO instance from Eloquent
+ $pdo = Capsule::connection()->getPdo();
+
+ $this->rememberMeStorage->setConnection($pdo);
+
+ // Set up RememberMe
+ $this->rememberMe = new RememberMe($this->rememberMeStorage);
+ // Set cookie name
+ $cookieName = $this->config['session.name'] . '-' . $this->config['remember_me.cookie.name'];
+ $this->rememberMe->getCookie()->setName($cookieName);
+
+ // Change cookie path
+ $this->rememberMe->getCookie()->setPath($this->config['remember_me.session.path']);
+
+ // Set expire time, if specified
+ if ($this->config->has('remember_me.expire_time') && ($this->config->has('remember_me.expire_time') != null)) {
+ $this->rememberMe->getCookie()->setExpireTime($this->config['remember_me.expire_time']);
+ }
+
+ $this->user = null;
+
+ $this->viaRemember = false;
+ }
+
+ /**
+ * Attempts to authenticate a user based on a supplied identity and password.
+ *
+ * If successful, the user's id is stored in session.
+ */
+ public function attempt($identityColumn, $identityValue, $password, $rememberMe = false)
+ {
+ // Try to load the user, using the specified conditions
+ $user = $this->classMapper->staticMethod('user', 'where', $identityColumn, $identityValue)->first();
+
+ if (!$user) {
+ throw new InvalidCredentialsException();
+ }
+
+ // Check that the user has a password set (so, rule out newly created accounts without a password)
+ if (!$user->password) {
+ throw new InvalidCredentialsException();
+ }
+
+ // Check that the user's account is enabled
+ if ($user->flag_enabled == 0) {
+ throw new AccountDisabledException();
+ }
+
+ // Check that the user's account is verified (if verification is required)
+ if ($this->config['site.registration.require_email_verification'] && $user->flag_verified == 0) {
+ throw new AccountNotVerifiedException();
+ }
+
+ // Here is my password. May I please assume the identify of this user now?
+ if (Password::verify($password, $user->password)) {
+ $this->login($user, $rememberMe);
+ return $user;
+ } else {
+ // We know the password is at fault here (as opposed to the identity), but lets not give away the combination in case of someone bruteforcing
+ throw new InvalidCredentialsException();
+ }
+ }
+
+ /**
+ * Determine if the current user is authenticated.
+ *
+ * @return bool
+ */
+ public function check()
+ {
+ return !is_null($this->user());
+ }
+
+ /**
+ * Determine if the current user is a guest (unauthenticated).
+ *
+ * @return bool
+ */
+ public function guest()
+ {
+ return !$this->check();
+ }
+
+ /**
+ * Process an account login request.
+ *
+ * This method logs in the specified user, allowing the client to assume the user's identity for the duration of the session.
+ * @param User $user The user to log in.
+ * @param bool $rememberMe Set to true to make this a "persistent session", i.e. one that will re-login even after the session expires.
+ * @todo Figure out a way to update the currentUser service to reflect the logged-in user *immediately* in the service provider.
+ * As it stands, the currentUser service will still reflect a "guest user" for the remainder of the request.
+ */
+ public function login($user, $rememberMe = false)
+ {
+ $oldId = session_id();
+ $this->session->regenerateId(true);
+
+ // Since regenerateId deletes the old session, we'll do the same in cache
+ $this->flushSessionCache($oldId);
+
+ // If the user wants to be remembered, create Rememberme cookie
+ if ($rememberMe) {
+ $this->rememberMe->createCookie($user->id);
+ } else {
+ $this->rememberMe->clearCookie();
+ }
+
+ // Assume identity
+ $key = $this->config['session.keys.current_user_id'];
+ $this->session[$key] = $user->id;
+
+ // Set auth mode
+ $this->viaRemember = false;
+
+ // User login actions
+ $user->onLogin();
+ }
+
+ /**
+ * Processes an account logout request.
+ *
+ * Logs the currently authenticated user out, destroying the PHP session and clearing the persistent session.
+ * This can optionally remove persistent sessions across all browsers/devices, since there can be a "RememberMe" cookie
+ * and corresponding database entries in multiple browsers/devices. See http://jaspan.com/improved_persistent_login_cookie_best_practice.
+ *
+ * @param bool $complete If set to true, will ensure that the user is logged out from *all* browsers on all devices.
+ */
+ public function logout($complete = false)
+ {
+ $currentUserId = $this->session->get($this->config['session.keys.current_user_id']);
+
+ // This removes all of the user's persistent logins from the database
+ if ($complete) {
+ $this->storage->cleanAllTriplets($currentUserId);
+ }
+
+ // Clear the rememberMe cookie
+ $this->rememberMe->clearCookie();
+
+ // User logout actions
+ if ($currentUserId) {
+ $currentUser = $this->classMapper->staticMethod('user', 'find', $currentUserId);
+ if ($currentUser) {
+ $currentUser->onLogout();
+ }
+ }
+
+ $this->user = null;
+ $this->loggedOut = true;
+
+ $oldId = session_id();
+
+ // Completely destroy the session
+ $this->session->destroy();
+
+ // Since regenerateId deletes the old session, we'll do the same in cache
+ $this->flushSessionCache($oldId);
+
+ // Restart the session service
+ $this->session->start();
+ }
+
+ /**
+ * Try to get the currently authenticated user, returning a guest user if none was found.
+ *
+ * Tries to re-establish a session for "remember-me" users who have been logged out due to an expired session.
+ * @return User|null
+ * @throws AuthExpiredException
+ * @throws AuthCompromisedException
+ * @throws AccountInvalidException
+ * @throws AccountDisabledException
+ */
+ public function user()
+ {
+ $user = null;
+
+ if (!$this->loggedOut) {
+
+ // Return any cached user
+ if (!is_null($this->user)) {
+ return $this->user;
+ }
+
+ // If this throws a PDOException we catch it and return null than allowing the exception to propagate.
+ // This is because the error handler relies on Twig, which relies on a Twig Extension, which relies on the global current_user variable.
+ // So, we really don't want this method to throw any database exceptions.
+ try {
+ // Now, check to see if we have a user in session
+ $user = $this->loginSessionUser();
+
+ // If no user was found in the session, try to login via RememberMe cookie
+ if (!$user) {
+ $user = $this->loginRememberedUser();
+ }
+ } catch (\PDOException $e) {
+ $user = null;
+ }
+ }
+
+ return $this->user = $user;
+ }
+
+ /**
+ * Determine whether the current user was authenticated using a remember me cookie.
+ *
+ * This function is useful when users are performing sensitive operations, and you may want to force them to re-authenticate.
+ * @return bool
+ */
+ public function viaRemember()
+ {
+ return $this->viaRemember;
+ }
+
+ /**
+ * Attempt to log in the client from their rememberMe token (in their cookie).
+ *
+ * @return User|bool If successful, the User object of the remembered user. Otherwise, return false.
+ * @throws AuthCompromisedException The client attempted to log in with an invalid rememberMe token.
+ */
+ protected function loginRememberedUser()
+ {
+ /** @var \Birke\Rememberme\LoginResult $loginResult */
+ $loginResult = $this->rememberMe->login();
+
+ if ($loginResult->isSuccess()) {
+ // Update in session
+ $this->session[$this->config['session.keys.current_user_id']] = $loginResult->getCredential();
+ // There is a chance that an attacker has stolen the login token,
+ // so we store the fact that the user was logged in via RememberMe (instead of login form)
+ $this->viaRemember = true;
+ } else {
+ // If $rememberMe->login() was not successfull, check if the token was invalid as well. This means the cookie was stolen.
+ if ($loginResult->hasPossibleManipulation()) {
+ throw new AuthCompromisedException();
+ }
+ }
+
+ return $this->validateUserAccount($loginResult->getCredential());
+ }
+
+ /**
+ * Attempt to log in the client from the session.
+ *
+ * @return User|null If successful, the User object of the user in session. Otherwise, return null.
+ * @throws AuthExpiredException The client attempted to use an expired rememberMe token.
+ */
+ protected function loginSessionUser()
+ {
+ $userId = $this->session->get($this->config['session.keys.current_user_id']);
+
+ // If a user_id was found in the session, check any rememberMe cookie that was submitted.
+ // If they submitted an expired rememberMe cookie, then we need to log them out.
+ if ($userId) {
+ if (!$this->validateRememberMeCookie()) {
+ $this->logout();
+ throw new AuthExpiredException();
+ }
+ }
+
+ return $this->validateUserAccount($userId);
+ }
+
+ /**
+ * Determine if the cookie contains a valid rememberMe token.
+ *
+ * @return bool
+ */
+ protected function validateRememberMeCookie()
+ {
+ $cookieValue = $this->rememberMe->getCookie()->getValue();
+ if (!$cookieValue) {
+ return true;
+ }
+ $triplet = RememberMeTriplet::fromString($cookieValue);
+ if (!$triplet->isValid()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Tries to load the specified user by id from the database.
+ *
+ * Checks that the account is valid and enabled, throwing an exception if not.
+ * @param int $userId
+ * @return User|null
+ * @throws AccountInvalidException
+ * @throws AccountDisabledException
+ */
+ protected function validateUserAccount($userId)
+ {
+ if ($userId) {
+ $user = $this->classMapper->staticMethod('user', 'find', $userId);
+
+ // If the user doesn't exist any more, throw an exception.
+ if (!$user) {
+ throw new AccountInvalidException();
+ }
+
+ // If the user has been disabled since their last request, throw an exception.
+ if (!$user->flag_enabled) {
+ throw new AccountDisabledException();
+ }
+
+ return $user;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Flush the cache associated with a session id
+ *
+ * @param string $id The session id
+ * @return bool
+ */
+ public function flushSessionCache($id)
+ {
+ return $this->cache->tags('_s' . $id)->flush();
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
new file mode 100755
index 0000000..e79ceb5
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Disabled account exception. Used when an account has been disabled.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountDisabledException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.DISABLED';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php
new file mode 100755
index 0000000..607235b
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Invalid account exception. Used when an account has been removed during an active session.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountInvalidException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.INVALID';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php
new file mode 100755
index 0000000..7eb56a6
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Unverified account exception. Used when an account is required to complete email verification, but hasn't done so yet.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountNotVerifiedException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.UNVERIFIED';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php
new file mode 100755
index 0000000..df3efbe
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\ForbiddenException;
+
+/**
+ * Compromised authentication exception. Used when we suspect theft of the rememberMe cookie.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthCompromisedException extends ForbiddenException
+{
+ protected $defaultMessage = 'ACCOUNT.SESSION_COMPROMISED';
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php
new file mode 100755
index 0000000..5583746
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Expired authentication exception. Used when the user needs to authenticate/reauthenticate.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthExpiredException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.SESSION_EXPIRED';
+ protected $httpErrorCode = 401;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php b/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php
new file mode 100755
index 0000000..18d4a5c
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Invalid credentials exception. Used when an account fails authentication for some reason.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class InvalidCredentialsException extends HttpException
+{
+ protected $defaultMessage = 'USER_OR_PASS_INVALID';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Hasher.php b/main/app/sprinkles/account/src/Authenticate/Hasher.php
new file mode 100755
index 0000000..e277eef
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authenticate/Hasher.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authenticate;
+
+/**
+ * Password hashing and validation class
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class Hasher
+{
+ /**
+ * Default crypt cost factor.
+ *
+ * @var int
+ */
+ protected $defaultRounds = 10;
+
+ /**
+ * Returns the hashing type for a specified password hash.
+ *
+ * Automatically detects the hash type: "sha1" (for UserCake legacy accounts), "legacy" (for 0.1.x accounts), and "modern" (used for new accounts).
+ * @param string $password the hashed password.
+ * @return string "sha1"|"legacy"|"modern".
+ */
+ public function getHashType($password)
+ {
+ // If the password in the db is 65 characters long, we have an sha1-hashed password.
+ if (strlen($password) == 65) {
+ return 'sha1';
+ } elseif (strlen($password) == 82) {
+ return 'legacy';
+ }
+
+ return 'modern';
+ }
+
+ /**
+ * Hashes a plaintext password using bcrypt.
+ *
+ * @param string $password the plaintext password.
+ * @param array $options
+ * @return string the hashed password.
+ * @throws HashFailedException
+ */
+ public function hash($password, array $options = [])
+ {
+ $hash = password_hash($password, PASSWORD_BCRYPT, [
+ 'cost' => $this->cost($options),
+ ]);
+
+ if (!$hash) {
+ throw new HashFailedException();
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Verify a plaintext password against the user's hashed password.
+ *
+ * @param string $password The plaintext password to verify.
+ * @param string $hash The hash to compare against.
+ * @param array $options
+ * @return boolean True if the password matches, false otherwise.
+ */
+ public function verify($password, $hash, array $options = [])
+ {
+ $hashType = $this->getHashType($hash);
+
+ if ($hashType == 'sha1') {
+ // Legacy UserCake passwords
+ $salt = substr($hash, 0, 25); // Extract the salt from the hash
+ $inputHash = $salt . sha1($salt . $password);
+
+ return (hash_equals($inputHash, $hash) === true);
+
+ } elseif ($hashType == 'legacy') {
+ // Homegrown implementation (assuming that current install has been using a cost parameter of 12)
+ // Used for manual implementation of bcrypt.
+ // Note that this legacy hashing put the salt at the _end_ for some reason.
+ $salt = substr($hash, 60);
+ $inputHash = crypt($password, '$2y$12$' . $salt);
+ $correctHash = substr($hash, 0, 60);
+
+ return (hash_equals($inputHash, $correctHash) === true);
+ }
+
+ // Modern implementation
+ return password_verify($password, $hash);
+ }
+
+ /**
+ * Extract the cost value from the options array.
+ *
+ * @param array $options
+ * @return int
+ */
+ protected function cost(array $options = [])
+ {
+ return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php b/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php
new file mode 100755
index 0000000..dd5647e
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authorize/AccessConditionExpression.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authorize;
+
+use Monolog\Logger;
+use PhpParser\Lexer\Emulative as EmulativeLexer;
+use PhpParser\Node;
+use PhpParser\NodeTraverser;
+use PhpParser\Parser as Parser;
+use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
+use PhpParser\Error as PhpParserException;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+
+/**
+ * AccessConditionExpression class
+ *
+ * This class models the evaluation of an authorization condition expression, as associated with permissions.
+ * A condition is built as a boolean expression composed of AccessCondition method calls.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccessConditionExpression
+{
+ /**
+ * @var User A user object, which for convenience can be referenced as 'self' in access conditions.
+ */
+ protected $user;
+
+ /**
+ * @var ParserNodeFunctionEvaluator The node visitor, which evaluates access condition callbacks used in a permission condition.
+ */
+ protected $nodeVisitor;
+
+ /**
+ * @var \PhpParser\Parser The PhpParser object to use (initialized in the ctor)
+ */
+ protected $parser;
+
+ /**
+ * @var NodeTraverser The NodeTraverser object to use (initialized in the ctor)
+ */
+ protected $traverser;
+
+ /**
+ * @var StandardPrettyPrinter The PrettyPrinter object to use (initialized in the ctor)
+ */
+ protected $prettyPrinter;
+
+ /**
+ * @var Logger
+ */
+ protected $logger;
+
+ /**
+ * @var bool Set to true if you want debugging information printed to the auth log.
+ */
+ protected $debug;
+
+ /**
+ * Create a new AccessConditionExpression object.
+ *
+ * @param User $user A user object, which for convenience can be referenced as 'self' in access conditions.
+ * @param Logger $logger A Monolog logger, used to dump debugging info for authorization evaluations.
+ * @param bool $debug Set to true if you want debugging information printed to the auth log.
+ */
+ public function __construct(ParserNodeFunctionEvaluator $nodeVisitor, User $user, Logger $logger, $debug = false)
+ {
+ $this->nodeVisitor = $nodeVisitor;
+ $this->user = $user;
+ $this->parser = new Parser(new EmulativeLexer);
+ $this->traverser = new NodeTraverser;
+ $this->traverser->addVisitor($nodeVisitor);
+ $this->prettyPrinter = new StandardPrettyPrinter;
+ $this->logger = $logger;
+ $this->debug = $debug;
+ }
+
+ /**
+ * Evaluates a condition expression, based on the given parameters.
+ *
+ * The special parameter `self` is an array of the current user's data.
+ * This get included automatically, and so does not need to be passed in.
+ * @param string $condition a boolean expression composed of calls to AccessCondition functions.
+ * @param array[mixed] $params the parameters to be used when evaluating the expression.
+ * @return bool true if the condition is passed for the given parameters, otherwise returns false.
+ */
+ public function evaluateCondition($condition, $params)
+ {
+ // Set the reserved `self` parameters.
+ // This replaces any values of `self` specified in the arguments, thus preventing them from being overridden in malicious user input.
+ // (For example, from an unfiltered request body).
+ $params['self'] = $this->user->export();
+
+ $this->nodeVisitor->setParams($params);
+
+ $code = "<?php $condition;";
+
+ if ($this->debug) {
+ $this->logger->debug("Evaluating access condition '$condition' with parameters:", $params);
+ }
+
+ // Traverse the parse tree, and execute any callbacks found using the supplied parameters.
+ // Replace the function node with the return value of the callback.
+ try {
+ // parse
+ $stmts = $this->parser->parse($code);
+
+ // traverse
+ $stmts = $this->traverser->traverse($stmts);
+
+ // Evaluate boolean statement. It is safe to use eval() here, because our expression has been reduced entirely to a boolean expression.
+ $expr = $this->prettyPrinter->prettyPrintExpr($stmts[0]);
+ $expr_eval = "return " . $expr . ";\n";
+ $result = eval($expr_eval);
+
+ if ($this->debug) {
+ $this->logger->debug("Expression '$expr' evaluates to " . ($result == true ? "true" : "false"));
+ }
+
+ return $result;
+ } catch (PhpParserException $e) {
+ if ($this->debug) {
+ $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage());
+ }
+ return false; // Access fails if the access condition can't be parsed.
+ } catch (AuthorizationException $e) {
+ if ($this->debug) {
+ $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage());
+ }
+ return false;
+ }
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authorize/AuthorizationException.php b/main/app/sprinkles/account/src/Authorize/AuthorizationException.php
new file mode 100755
index 0000000..251b67f
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authorize/AuthorizationException.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authorize;
+
+use UserFrosting\Support\Exception\ForbiddenException;
+
+/**
+ * AuthorizationException class
+ *
+ * Exception for AccessConditionExpression.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @see http://www.userfrosting.com/components/#authorization
+ */
+class AuthorizationException extends ForbiddenException
+{
+
+}
diff --git a/main/app/sprinkles/account/src/Authorize/AuthorizationManager.php b/main/app/sprinkles/account/src/Authorize/AuthorizationManager.php
new file mode 100755
index 0000000..def152b
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authorize/AuthorizationManager.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authorize;
+
+use Interop\Container\ContainerInterface;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+
+/**
+ * AuthorizationManager class.
+ *
+ * Manages a collection of access condition callbacks, and uses them to perform access control checks on user objects.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthorizationManager
+{
+ /**
+ * @var ContainerInterface The global container object, which holds all your services.
+ */
+ protected $ci;
+
+ /**
+ * @var array[callable] An array of callbacks that accept some parameters and evaluate to true or false.
+ */
+ protected $callbacks = [];
+
+ /**
+ * Create a new AuthorizationManager object.
+ *
+ * @param ContainerInterface $ci The global container object, which holds all your services.
+ */
+ public function __construct(ContainerInterface $ci, array $callbacks = [])
+ {
+ $this->ci = $ci;
+ $this->callbacks = $callbacks;
+ }
+
+ /**
+ * Register an authorization callback, which can then be used in permission conditions.
+ *
+ * To add additional callbacks, simply extend the `authorizer` service in your Sprinkle's service provider.
+ * @param string $name
+ * @param callable $callback
+ */
+ public function addCallback($name, $callback)
+ {
+ $this->callbacks[$name] = $callback;
+ return $this;
+ }
+
+ /**
+ * Get all authorization callbacks.
+ *
+ * @return callable[]
+ */
+ public function getCallbacks()
+ {
+ return $this->callbacks;
+ }
+
+ /**
+ * Checks whether or not a user has access on a particular permission slug.
+ *
+ * Determine if this user has access to the given $slug under the given $params.
+ *
+ * @param UserFrosting\Sprinkle\Account\Database\Models\User $user
+ * @param string $slug The permission slug to check for access.
+ * @param array $params[optional] An array of field names => values, specifying any additional data to provide the authorization module
+ * when determining whether or not this user has access.
+ * @return boolean True if the user has access, false otherwise.
+ */
+ public function checkAccess(User $user, $slug, array $params = [])
+ {
+ $debug = $this->ci->config['debug.auth'];
+
+ if ($debug) {
+ $trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1);
+ $this->ci->authLogger->debug("Authorization check requested at: ", $trace);
+ $this->ci->authLogger->debug("Checking authorization for user {$user->id} ('{$user->user_name}') on permission '$slug'...");
+ }
+
+ if ($this->ci->authenticator->guest()) {
+ if ($debug) {
+ $this->ci->authLogger->debug("User is not logged in. Access denied.");
+ }
+ return false;
+ }
+
+ // The master (root) account has access to everything.
+ // Need to use loose comparison for now, because some DBs return `id` as a string.
+
+ if ($user->id == $this->ci->config['reserved_user_ids.master']) {
+ if ($debug) {
+ $this->ci->authLogger->debug("User is the master (root) user. Access granted.");
+ }
+ return true;
+ }
+
+ // Find all permissions that apply to this user (via roles), and check if any evaluate to true.
+ $permissions = $user->getCachedPermissions();
+
+ if (empty($permissions) || !isset($permissions[$slug])) {
+ if ($debug) {
+ $this->ci->authLogger->debug("No matching permissions found. Access denied.");
+ }
+ return false;
+ }
+
+ $permissions = $permissions[$slug];
+
+ if ($debug) {
+ $this->ci->authLogger->debug("Found matching permissions: \n" . print_r($this->getPermissionsArrayDebugInfo($permissions), true));
+ }
+
+ $nodeVisitor = new ParserNodeFunctionEvaluator($this->callbacks, $this->ci->authLogger, $debug);
+ $ace = new AccessConditionExpression($nodeVisitor, $user, $this->ci->authLogger, $debug);
+
+ foreach ($permissions as $permission) {
+ $pass = $ace->evaluateCondition($permission->conditions, $params);
+ if ($pass) {
+ if ($debug) {
+ $this->ci->authLogger->debug("User passed conditions '{$permission->conditions}' . Access granted.");
+ }
+ return true;
+ }
+ }
+
+ if ($debug) {
+ $this->ci->authLogger->debug("User failed to pass any of the matched permissions. Access denied.");
+ }
+
+ return false;
+ }
+
+ /**
+ * Remove extraneous information from the permission to reduce verbosity.
+ *
+ * @param array
+ * @return array
+ */
+ protected function getPermissionsArrayDebugInfo($permissions)
+ {
+ $permissionsInfo = [];
+ foreach ($permissions as $permission) {
+ $permissionData = array_only($permission->toArray(), ['id', 'slug', 'name', 'conditions', 'description']);
+ // Remove this until we can find an efficient way to only load these once during debugging
+ //$permissionData['roles_via'] = $permission->roles_via->pluck('id')->all();
+ $permissionsInfo[] = $permissionData;
+ }
+
+ return $permissionsInfo;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php b/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php
new file mode 100755
index 0000000..e8e5cde
--- /dev/null
+++ b/main/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Authorize;
+
+use Monolog\Logger;
+use PhpParser\Node;
+use PhpParser\NodeVisitorAbstract;
+use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
+
+/**
+ * ParserNodeFunctionEvaluator class
+ *
+ * This class parses access control condition expressions.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @see http://www.userfrosting.com/components/#authorization
+ */
+class ParserNodeFunctionEvaluator extends NodeVisitorAbstract
+{
+
+ /**
+ * @var array[callable] An array of callback functions to be used when evaluating a condition expression.
+ */
+ protected $callbacks;
+ /**
+ * @var \PhpParser\PrettyPrinter\Standard The PrettyPrinter object to use (initialized in the ctor)
+ */
+ protected $prettyPrinter;
+ /**
+ * @var array The parameters to be used when evaluating the methods in the condition expression, as an array.
+ */
+ protected $params = [];
+
+ /**
+ * @var Logger
+ */
+ protected $logger;
+
+ /**
+ * @var bool Set to true if you want debugging information printed to the auth log.
+ */
+ protected $debug;
+
+ /**
+ * Create a new ParserNodeFunctionEvaluator object.
+ *
+ * @param array $params The parameters to be used when evaluating the methods in the condition expression, as an array.
+ * @param Logger $logger A Monolog logger, used to dump debugging info for authorization evaluations.
+ * @param bool $debug Set to true if you want debugging information printed to the auth log.
+ */
+ public function __construct($callbacks, $logger, $debug = false)
+ {
+ $this->callbacks = $callbacks;
+ $this->prettyPrinter = new StandardPrettyPrinter;
+ $this->logger = $logger;
+ $this->debug = $debug;
+ $this->params = [];
+ }
+
+ public function leaveNode(Node $node)
+ {
+ // Look for function calls
+ if ($node instanceof \PhpParser\Node\Expr\FuncCall) {
+ $eval = new \PhpParser\Node\Scalar\LNumber;
+
+ // Get the method name
+ $callbackName = $node->name->toString();
+ // Get the method arguments
+ $argNodes = $node->args;
+
+ $args = [];
+ $argsInfo = [];
+ foreach ($argNodes as $arg) {
+ $argString = $this->prettyPrinter->prettyPrintExpr($arg->value);
+
+ // Debugger info
+ $currentArgInfo = [
+ 'expression' => $argString
+ ];
+ // Resolve parameter placeholders ('variable' names (either single-word or array-dot identifiers))
+ if (($arg->value instanceof \PhpParser\Node\Expr\BinaryOp\Concat) || ($arg->value instanceof \PhpParser\Node\Expr\ConstFetch)) {
+ $value = $this->resolveParamPath($argString);
+ $currentArgInfo['type'] = "parameter";
+ $currentArgInfo['resolved_value'] = $value;
+ // Resolve arrays
+ } elseif ($arg->value instanceof \PhpParser\Node\Expr\Array_) {
+ $value = $this->resolveArray($arg);
+ $currentArgInfo['type'] = "array";
+ $currentArgInfo['resolved_value'] = print_r($value, true);
+ // Resolve strings
+ } elseif ($arg->value instanceof \PhpParser\Node\Scalar\String_) {
+ $value = $arg->value->value;
+ $currentArgInfo['type'] = "string";
+ $currentArgInfo['resolved_value'] = $value;
+ // Resolve numbers
+ } elseif ($arg->value instanceof \PhpParser\Node\Scalar\DNumber) {
+ $value = $arg->value->value;
+ $currentArgInfo['type'] = "float";
+ $currentArgInfo['resolved_value'] = $value;
+ } elseif ($arg->value instanceof \PhpParser\Node\Scalar\LNumber) {
+ $value = $arg->value->value;
+ $currentArgInfo['type'] = "integer";
+ $currentArgInfo['resolved_value'] = $value;
+ // Anything else is simply interpreted as its literal string value
+ } else {
+ $value = $argString;
+ $currentArgInfo['type'] = "unknown";
+ $currentArgInfo['resolved_value'] = $value;
+ }
+
+ $args[] = $value;
+ $argsInfo[] = $currentArgInfo;
+ }
+
+ if ($this->debug) {
+ if (count($args)) {
+ $this->logger->debug("Evaluating callback '$callbackName' on: ", $argsInfo);
+ } else {
+ $this->logger->debug("Evaluating callback '$callbackName'...");
+ }
+ }
+
+ // Call the specified access condition callback with the specified arguments.
+ if (isset($this->callbacks[$callbackName]) && is_callable($this->callbacks[$callbackName])) {
+ $result = call_user_func_array($this->callbacks[$callbackName], $args);
+ } else {
+ throw new AuthorizationException("Authorization failed: Access condition method '$callbackName' does not exist.");
+ }
+
+ if ($this->debug) {
+ $this->logger->debug("Result: " . ($result ? "1" : "0"));
+ }
+
+ return new \PhpParser\Node\Scalar\LNumber($result ? "1" : "0");
+ }
+ }
+
+ public function setParams($params)
+ {
+ $this->params = $params;
+ }
+
+ /**
+ * Resolve an array expression in a condition expression into an actual array.
+ *
+ * @param string $arg the array, represented as a string.
+ * @return array[mixed] the array, as a plain ol' PHP array.
+ */
+ private function resolveArray($arg)
+ {
+ $arr = [];
+ $items = (array) $arg->value->items;
+ foreach ($items as $item) {
+ if ($item->key) {
+ $arr[$item->key] = $item->value->value;
+ } else {
+ $arr[] = $item->value->value;
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * Resolve a parameter path (e.g. "user.id", "post", etc) into its value.
+ *
+ * @param string $path the name of the parameter to resolve, based on the parameters set in this object.
+ * @throws Exception the path could not be resolved. Path is malformed or key does not exist.
+ * @return mixed the value of the specified parameter.
+ */
+ private function resolveParamPath($path)
+ {
+ $pathTokens = explode(".", $path);
+ $value = $this->params;
+ foreach ($pathTokens as $token) {
+ $token = trim($token);
+ if (is_array($value) && isset($value[$token])) {
+ $value = $value[$token];
+ continue;
+ } elseif (is_object($value) && isset($value->$token)) {
+ $value = $value->$token;
+ continue;
+ } else {
+ throw new AuthorizationException("Cannot resolve the path \"$path\". Error at token \"$token\".");
+ }
+ }
+ return $value;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php b/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php
new file mode 100755
index 0000000..cfaacef
--- /dev/null
+++ b/main/app/sprinkles/account/src/Bakery/CreateAdminUser.php
@@ -0,0 +1,334 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Bakery;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+use UserFrosting\System\Bakery\BaseCommand;
+use UserFrosting\System\Bakery\DatabaseTest;
+use UserFrosting\System\Database\Model\Migrations;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Account\Database\Models\Role;
+use UserFrosting\Sprinkle\Account\Facades\Password;
+
+/**
+ * Create root user CLI command.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class CreateAdminUser extends BaseCommand
+{
+ use DatabaseTest;
+
+ /**
+ * @var string[] Migration dependencies for this command to work
+ */
+ protected $dependencies = [
+ '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\UsersTable',
+ '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\RolesTable',
+ '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\RoleUsersTable'
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function configure()
+ {
+ $this->setName("create-admin")
+ ->setDescription("Create the initial admin (root) user account");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title("Root account setup");
+
+ // Need the database
+ try {
+ $this->io->writeln("<info>Testing database connection</info>", OutputInterface::VERBOSITY_VERBOSE);
+ $this->testDB();
+ $this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE);
+ } catch (\Exception $e) {
+ $this->io->error($e->getMessage());
+ exit(1);
+ }
+
+ // Need migration table
+ if (!Capsule::schema()->hasColumn('migrations', 'id')) {
+ $this->io->error("Migrations doesn't appear to have been run! Make sure the database is properly migrated by using the `php bakery migrate` command.");
+ exit(1);
+ }
+
+ // Make sure the required mirgations have been run
+ foreach ($this->dependencies as $migration) {
+ if (!Migrations::where('migration', $migration)->exists()) {
+ $this->io->error("Migration `$migration` doesn't appear to have been run! Make sure all migrations are up to date by using the `php bakery migrate` command.");
+ exit(1);
+ }
+ }
+
+ // Make sure that there are no users currently in the user table
+ // We setup the root account here so it can be done independent of the version check
+ if (User::count() > 0) {
+
+ $this->io->note("Table 'users' is not empty. Skipping root account setup. To set up the root account again, please truncate or drop the table and try again.");
+
+ } else {
+
+ $this->io->writeln("Please answer the following questions to create the root account:\n");
+
+ // Get the account details
+ $userName = $this->askUsername();
+ $email = $this->askEmail();
+ $firstName = $this->askFirstName();
+ $lastName = $this->askLastName();
+ $password = $this->askPassword();
+
+ // Ok, now we've got the info and we can create the new user.
+ $this->io->write("\n<info>Saving the root user details...</info>");
+ $rootUser = new User([
+ "user_name" => $userName,
+ "email" => $email,
+ "first_name" => $firstName,
+ "last_name" => $lastName,
+ "password" => Password::hash($password)
+ ]);
+
+ $rootUser->save();
+
+ $defaultRoles = [
+ 'user' => Role::where('slug', 'user')->first(),
+ 'group-admin' => Role::where('slug', 'group-admin')->first(),
+ 'site-admin' => Role::where('slug', 'site-admin')->first()
+ ];
+
+ foreach ($defaultRoles as $slug => $role) {
+ if ($role) {
+ $rootUser->roles()->attach($role->id);
+ }
+ }
+
+ $this->io->success("Root user creation successful!");
+ }
+ }
+
+ /**
+ * Ask for the username
+ *
+ * @access protected
+ * @return void
+ */
+ protected function askUsername()
+ {
+ while (!isset($userName) || !$this->validateUsername($userName)) {
+ $userName = $this->io->ask("Choose a root username (1-50 characters, no leading or trailing whitespace)");
+ }
+ return $userName;
+ }
+
+ /**
+ * Validate the username.
+ *
+ * @access protected
+ * @param mixed $userName
+ * @return void
+ */
+ protected function validateUsername($userName)
+ {
+ // Validate length
+ if (strlen($userName) < 1 || strlen($userName) > 50) {
+ $this->io->error("Username must be between 1-50 characters");
+ return false;
+ }
+
+ // Validate format
+ $options = [
+ 'options' => [
+ 'regexp' => "/^\S((.*\S)|)$/"
+ ]
+ ];
+ $validate = filter_var($userName, FILTER_VALIDATE_REGEXP, $options);
+ if (!$validate) {
+ $this->io->error("Username can't have any leading or trailing whitespace");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Ask for the email
+ *
+ * @access protected
+ * @return void
+ */
+ protected function askEmail()
+ {
+ while (!isset($email) || !$this->validateEmail($email)) {
+ $email = $this->io->ask("Enter a valid email address (1-254 characters, must be compatible with FILTER_VALIDATE_EMAIL)");
+ }
+ return $email;
+ }
+
+ /**
+ * Validate the email.
+ *
+ * @access protected
+ * @param mixed $email
+ * @return void
+ */
+ protected function validateEmail($email)
+ {
+ // Validate length
+ if (strlen($email) < 1 || strlen($email) > 254) {
+ $this->io->error("Email must be between 1-254 characters");
+ return false;
+ }
+
+ // Validate format
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ $this->io->error("Email must be compatible with FILTER_VALIDATE_EMAIL");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Ask for the first name
+ *
+ * @access protected
+ * @return void
+ */
+ protected function askFirstName()
+ {
+ while (!isset($firstName) || !$this->validateFirstName($firstName)) {
+ $firstName = $this->io->ask("Enter the user first name (1-20 characters)");
+ }
+ return $firstName;
+ }
+
+ /**
+ * validateFirstName function.
+ *
+ * @access protected
+ * @param mixed $name
+ * @return void
+ */
+ protected function validateFirstName($firstName)
+ {
+ // Validate length
+ if (strlen($firstName) < 1 || strlen($firstName) > 20) {
+ $this->io->error("First name must be between 1-20 characters");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Ask for the last name
+ *
+ * @access protected
+ * @return void
+ */
+ protected function askLastName()
+ {
+ while (!isset($lastName) || !$this->validateLastName($lastName)) {
+ $lastName = $this->io->ask("Enter the user last name (1-30 characters)");
+ }
+ return $lastName;
+ }
+
+ /**
+ * validateLastName function.
+ *
+ * @access protected
+ * @param mixed $lastName
+ * @return void
+ */
+ protected function validateLastName($lastName)
+ {
+ // Validate length
+ if (strlen($lastName) < 1 || strlen($lastName) > 30) {
+ $this->io->error("Last name must be between 1-30 characters");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Ask for the password
+ *
+ * @access protected
+ * @return void
+ */
+ protected function askPassword()
+ {
+ while (!isset($password) || !$this->validatePassword($password) || !$this->confirmPassword($password)) {
+ $password = $this->io->askHidden("Enter password (12-255 characters)");
+ }
+ return $password;
+ }
+
+ /**
+ * validatePassword function.
+ *
+ * @access protected
+ * @param mixed $password
+ * @return void
+ */
+ protected function validatePassword($password)
+ {
+ if (strlen($password) < 12 || strlen($password) > 255) {
+ $this->io->error("Password must be between 12-255 characters");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * confirmPassword function.
+ *
+ * @access protected
+ * @param mixed $passwordToConfirm
+ * @return void
+ */
+ protected function confirmPassword($passwordToConfirm)
+ {
+ while (!isset($password)) {
+ $password = $this->io->askHidden("Please re-enter the chosen password");
+ }
+ return $this->validatePasswordConfirmation($password, $passwordToConfirm);
+ }
+
+ /**
+ * validatePasswordConfirmation function.
+ *
+ * @access protected
+ * @param mixed $password
+ * @param mixed $passwordToConfirm
+ * @return void
+ */
+ protected function validatePasswordConfirmation($password, $passwordToConfirm)
+ {
+ if ($password != $passwordToConfirm) {
+ $this->io->error("Passwords do not match, please try again.");
+ return false;
+ }
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/main/app/sprinkles/account/src/Controller/AccountController.php b/main/app/sprinkles/account/src/Controller/AccountController.php
new file mode 100755
index 0000000..ce99370
--- /dev/null
+++ b/main/app/sprinkles/account/src/Controller/AccountController.php
@@ -0,0 +1,1293 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Controller;
+
+use Carbon\Carbon;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\NotFoundException as NotFoundException;
+use UserFrosting\Fortress\RequestDataTransformer;
+use UserFrosting\Fortress\RequestSchema;
+use UserFrosting\Fortress\ServerSideValidator;
+use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
+use UserFrosting\Sprinkle\Account\Controller\Exception\SpammyRequestException;
+use UserFrosting\Sprinkle\Account\Facades\Password;
+use UserFrosting\Sprinkle\Account\Util\Util as AccountUtil;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Core\Mail\EmailRecipient;
+use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage;
+use UserFrosting\Sprinkle\Core\Util\Captcha;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\ForbiddenException;
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Controller class for /account/* URLs. Handles account-related activities, including login, registration, password recovery, and account settings.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @see http://www.userfrosting.com/navigating/#structure
+ */
+class AccountController extends SimpleController
+{
+ /**
+ * Check a username for availability.
+ *
+ * This route is throttled by default, to discourage abusing it for account enumeration.
+ * This route is "public access".
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function checkUsername(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ // Load request schema
+ $schema = new RequestSchema('schema://requests/check-username.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException
+ $e = new BadRequestException('Missing or malformed request data!');
+ foreach ($validator->errors() as $idx => $field) {
+ foreach($field as $eidx => $error) {
+ $e->addUserMessage($error);
+ }
+ }
+ throw $e;
+ }
+
+ /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */
+ $throttler = $this->ci->throttler;
+ $delay = $throttler->getDelay('check_username_request');
+
+ // Throttle requests
+ if ($delay > 0) {
+ return $response->withStatus(429);
+ }
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Log throttleable event
+ $throttler->logEvent('check_username_request');
+
+ if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) {
+ $message = $translator->translate('USERNAME.NOT_AVAILABLE', $data);
+ return $response->write($message)->withStatus(200);
+ } else {
+ return $response->write('true')->withStatus(200);
+ }
+ }
+
+ /**
+ * Processes a request to cancel a password reset request.
+ *
+ * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves.
+ * Processes the request from the password reset link, checking that:
+ * 1. The provided token is associated with an existing user account, who has a pending password reset request.
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function denyResetPassword(Request $request, Response $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $loginPage = $this->ci->router->pathFor('login');
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/deny-password.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors. Since this is a GET request, we need to redirect on failure
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel
+ return $response->withRedirect($loginPage, 400);
+ }
+
+ $passwordReset = $this->ci->repoPasswordReset->cancel($data['token']);
+
+ if (!$passwordReset) {
+ $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID');
+ return $response->withRedirect($loginPage, 400);
+ }
+
+ $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_CANNED');
+ return $response->withRedirect($loginPage);
+ }
+
+ /**
+ * Processes a request to email a forgotten password reset link to the user.
+ *
+ * Processes the request from the form on the "forgot password" page, checking that:
+ * 1. The rate limit for this type of request is being observed.
+ * 2. The provided email address belongs to a registered account;
+ * 3. The submitted data is valid.
+ * Note that we have removed the requirement that a password reset request not already be in progress.
+ * This is because we need to allow users to re-request a reset, even if they lose the first reset email.
+ * This route is "public access".
+ * Request type: POST
+ * @todo require additional user information
+ * @todo prevent password reset requests for root account?
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function forgotPassword(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // Get POST parameters
+ $params = $request->getParsedBody();
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/forgot-password.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ return $response->withStatus(400);
+ }
+
+ // Throttle requests
+
+ /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */
+ $throttler = $this->ci->throttler;
+
+ $throttleData = [
+ 'email' => $data['email']
+ ];
+ $delay = $throttler->getDelay('password_reset_request', $throttleData);
+
+ if ($delay > 0) {
+ $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]);
+ return $response->withStatus(429);
+ }
+
+ // All checks passed! log events/activities, update user, and send email
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $throttler, $throttleData, $config) {
+
+ // Log throttleable event
+ $throttler->logEvent('password_reset_request', $throttleData);
+
+ // Load the user, by email address
+ $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first();
+
+ // Check that the email exists.
+ // If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration
+ if ($user) {
+ // Try to generate a new password reset request.
+ // Use timeout for "reset password"
+ $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']);
+
+ // Create and send email
+ $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig');
+ $message->from($config['address_book.admin'])
+ ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
+ ->addParams([
+ 'user' => $user,
+ 'token' => $passwordReset->getToken(),
+ 'request_date' => Carbon::now()->format('Y-m-d H:i:s')
+ ]);
+
+ $this->ci->mailer->send($message);
+ }
+ });
+
+ // TODO: create delay to prevent timing-based attacks
+
+ $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', ['email' => $data['email']]);
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns a modal containing account terms of service.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function getModalAccountTos(Request $request, Response $response, $args)
+ {
+ return $this->ci->view->render($response, 'modals/tos.html.twig');
+ }
+
+ /**
+ * Generate a random captcha, store it to the session, and return the captcha image.
+ *
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function imageCaptcha(Request $request, Response $response, $args)
+ {
+ $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']);
+ $captcha->generateRandomCode();
+
+ return $response->withStatus(200)
+ ->withHeader('Content-Type', 'image/png;base64')
+ ->write($captcha->getImage());
+ }
+
+ /**
+ * Processes an account login request.
+ *
+ * Processes the request from the form on the login page, checking that:
+ * 1. The user is not already logged in.
+ * 2. The rate limit for this type of request is being observed.
+ * 3. Email login is enabled, if an email address was used.
+ * 4. The user account exists.
+ * 5. The user account is enabled and verified.
+ * 6. The user entered a valid username/email and password.
+ * This route, by definition, is "public access".
+ * Request type: POST
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function login(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $this->ci->authenticator;
+
+ // Return 200 success if user is already logged in
+ if ($authenticator->check()) {
+ $ms->addMessageTranslated('warning', 'LOGIN.ALREADY_COMPLETE');
+ return $response->withStatus(200);
+ }
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // Get POST parameters
+ $params = $request->getParsedBody();
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/login.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ return $response->withStatus(400);
+ }
+
+ // Determine whether we are trying to log in with an email address or a username
+ $isEmail = filter_var($data['user_name'], FILTER_VALIDATE_EMAIL);
+
+ // Throttle requests
+
+ /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */
+ $throttler = $this->ci->throttler;
+
+ $userIdentifier = $data['user_name'];
+
+ $throttleData = [
+ 'user_identifier' => $userIdentifier
+ ];
+
+ $delay = $throttler->getDelay('sign_in_attempt', $throttleData);
+ if ($delay > 0) {
+ $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', [
+ 'delay' => $delay
+ ]);
+ return $response->withStatus(429);
+ }
+
+ // Log throttleable event
+ $throttler->logEvent('sign_in_attempt', $throttleData);
+
+ // If credential is an email address, but email login is not enabled, raise an error.
+ // Note that we do this after logging throttle event, so this error counts towards throttling limit.
+ if ($isEmail && !$config['site.login.enable_email']) {
+ $ms->addMessageTranslated('danger', 'USER_OR_PASS_INVALID');
+ return $response->withStatus(403);
+ }
+
+ // Try to authenticate the user. Authenticator will throw an exception on failure.
+ /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $this->ci->authenticator;
+
+ $currentUser = $authenticator->attempt(($isEmail ? 'email' : 'user_name'), $userIdentifier, $data['password'], $data['rememberme']);
+
+ $ms->addMessageTranslated('success', 'WELCOME', $currentUser->export());
+
+ // Set redirect, if relevant
+ $redirectOnLogin = $this->ci->get('redirect.onLogin');
+
+ return $redirectOnLogin($request, $response, $args);
+ }
+
+ /**
+ * Log the user out completely, including destroying any "remember me" token.
+ *
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function logout(Request $request, Response $response, $args)
+ {
+ // Destroy the session
+ $this->ci->authenticator->logout();
+
+ // Return to home page
+ $config = $this->ci->config;
+ return $response->withStatus(302)->withHeader('Location', $config['site.uri.public']);
+ }
+
+ /**
+ * Render the "forgot password" page.
+ *
+ * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them.
+ * By default, this is a "public page" (does not require authentication).
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageForgotPassword(Request $request, Response $response, $args)
+ {
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/forgot-password.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'pages/forgot-password.html.twig', [
+ 'page' => [
+ 'validators' => [
+ 'forgot_password' => $validator->rules('json', false)
+ ]
+ ]
+ ]);
+ }
+
+
+ /**
+ * Render the account registration page for UserFrosting.
+ *
+ * This allows new (non-authenticated) users to create a new account for themselves on your website (if enabled).
+ * By definition, this is a "public page" (does not require authentication).
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageRegister(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ if (!$config['site.registration.enabled']) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $this->ci->authenticator;
+
+ // Redirect if user is already logged in
+ if ($authenticator->check()) {
+ $redirect = $this->ci->get('redirect.onAlreadyLoggedIn');
+
+ return $redirect($request, $response, $args);
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/register.yaml');
+ $validatorRegister = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'pages/register.html.twig', [
+ 'page' => [
+ 'validators' => [
+ 'register' => $validatorRegister->rules('json', false)
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * Render the "resend verification email" page.
+ *
+ * This is a form that allows users who lost their account verification link to have the link resent to their email address.
+ * By default, this is a "public page" (does not require authentication).
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageResendVerification(Request $request, Response $response, $args)
+ {
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/resend-verification.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'pages/resend-verification.html.twig', [
+ 'page' => [
+ 'validators' => [
+ 'resend_verification' => $validator->rules('json', false)
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * Reset password page.
+ *
+ * Renders the new password page for password reset requests.
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageResetPassword(Request $request, Response $response, $args)
+ {
+ // Insert the user's secret token from the link into the password reset form
+ $params = $request->getQueryParams();
+
+ // Load validation rules - note this uses the same schema as "set password"
+ $schema = new RequestSchema('schema://requests/set-password.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'pages/reset-password.html.twig', [
+ 'page' => [
+ 'validators' => [
+ 'set_password' => $validator->rules('json', false)
+ ]
+ ],
+ 'token' => isset($params['token']) ? $params['token'] : '',
+ ]);
+ }
+
+ /**
+ * Render the "set password" page.
+ *
+ * Renders the page where new users who have had accounts created for them by another user, can set their password.
+ * By default, this is a "public page" (does not require authentication).
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageSetPassword(Request $request, Response $response, $args)
+ {
+ // Insert the user's secret token from the link into the password set form
+ $params = $request->getQueryParams();
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/set-password.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'pages/set-password.html.twig', [
+ 'page' => [
+ 'validators' => [
+ 'set_password' => $validator->rules('json', false)
+ ]
+ ],
+ 'token' => isset($params['token']) ? $params['token'] : '',
+ ]);
+ }
+
+ /**
+ * Account settings page.
+ *
+ * Provides a form for users to modify various properties of their account, such as name, email, locale, etc.
+ * Any fields that the user does not have permission to modify will be automatically disabled.
+ * This page requires authentication.
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageSettings(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_account_settings')) {
+ throw new ForbiddenException();
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/account-settings.yaml');
+ $validatorAccountSettings = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ $schema = new RequestSchema('schema://requests/profile-settings.yaml');
+ $validatorProfileSettings = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ return $this->ci->view->render($response, 'pages/account-settings.html.twig', [
+ 'locales' => $locales,
+ 'page' => [
+ 'validators' => [
+ 'account_settings' => $validatorAccountSettings->rules('json', false),
+ 'profile_settings' => $validatorProfileSettings->rules('json', false)
+ ],
+ 'visibility' => ($authorizer->checkAccess($currentUser, 'update_account_settings') ? '' : 'disabled')
+ ]
+ ]);
+ }
+
+ /**
+ * Render the account sign-in page for UserFrosting.
+ *
+ * This allows existing users to sign in.
+ * By definition, this is a "public page" (does not require authentication).
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function pageSignIn(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $this->ci->authenticator;
+
+ // Redirect if user is already logged in
+ if ($authenticator->check()) {
+ $redirect = $this->ci->get('redirect.onAlreadyLoggedIn');
+
+ return $redirect($request, $response, $args);
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/login.yaml');
+ $validatorLogin = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'pages/sign-in.html.twig', [
+ 'page' => [
+ 'validators' => [
+ 'login' => $validatorLogin->rules('json', false)
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * Processes a request to update a user's profile information.
+ *
+ * Processes the request from the user profile settings form, checking that:
+ * 1. They have the necessary permissions to update the posted field(s);
+ * 2. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: POST
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function profile(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access control for entire resource - check that the current user has permission to modify themselves
+ // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify.
+ if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) {
+ $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED');
+ return $response->withStatus(403);
+ }
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // POST parameters
+ $params = $request->getParsedBody();
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/profile-settings.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ $error = false;
+
+ // Validate, and halt on validation errors.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ $error = true;
+ }
+
+ // Check that locale is valid
+ $locales = $config->getDefined('site.locales.available');
+ if (!array_key_exists($data['locale'], $locales)) {
+ $ms->addMessageTranslated('danger', 'LOCALE.INVALID', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Looks good, let's update with new values!
+ // Note that only fields listed in `profile-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB
+ $currentUser->fill($data);
+
+ $currentUser->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their profile settings.", [
+ 'type' => 'update_profile_settings'
+ ]);
+
+ $ms->addMessageTranslated('success', 'PROFILE.UPDATED');
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes an new account registration request.
+ *
+ * This is throttled to prevent account enumeration, since it needs to divulge when a username/email has been used.
+ * Processes the request from the form on the registration page, checking that:
+ * 1. The honeypot was not modified;
+ * 2. The master account has already been created (during installation);
+ * 3. Account registration is enabled;
+ * 4. The user is not already logged in;
+ * 5. Valid information was entered;
+ * 6. The captcha, if enabled, is correct;
+ * 7. The username and email are not already taken.
+ * Automatically sends an activation link upon success, if account activation is enabled.
+ * This route is "public access".
+ * Request type: POST
+ * Returns the User Object for the user record that was created.
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function register(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // Get POST parameters: user_name, first_name, last_name, email, password, passwordc, captcha, spiderbro, csrf_token
+ $params = $request->getParsedBody();
+
+ // Check the honeypot. 'spiderbro' is not a real field, it is hidden on the main page and must be submitted with its default value for this to be processed.
+ if (!isset($params['spiderbro']) || $params['spiderbro'] != 'http://') {
+ throw new SpammyRequestException('Possible spam received:' . print_r($params, true));
+ }
+
+ // Security measure: do not allow registering new users until the master account has been created.
+ if (!$classMapper->staticMethod('user', 'find', $config['reserved_user_ids.master'])) {
+ $ms->addMessageTranslated('danger', 'ACCOUNT.MASTER_NOT_EXISTS');
+ return $response->withStatus(403);
+ }
+
+ // Check if registration is currently enabled
+ if (!$config['site.registration.enabled']) {
+ $ms->addMessageTranslated('danger', 'REGISTRATION.DISABLED');
+ return $response->withStatus(403);
+ }
+
+ /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $this->ci->authenticator;
+
+ // Prevent the user from registering if he/she is already logged in
+ if ($authenticator->check()) {
+ $ms->addMessageTranslated('danger', 'REGISTRATION.LOGOUT');
+ return $response->withStatus(403);
+ }
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/register.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ $error = false;
+
+ // Validate request data
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ $error = true;
+ }
+
+ /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */
+ $throttler = $this->ci->throttler;
+ $delay = $throttler->getDelay('registration_attempt');
+
+ // Throttle requests
+ if ($delay > 0) {
+ return $response->withStatus(429);
+ }
+
+ // Check if username or email already exists
+ if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) {
+ $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) {
+ $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data);
+ $error = true;
+ }
+
+ // Check captcha, if required
+ if ($config['site.registration.captcha']) {
+ $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']);
+ if (!$data['captcha'] || !$captcha->verifyCode($data['captcha'])) {
+ $ms->addMessageTranslated('danger', 'CAPTCHA.FAIL');
+ $error = true;
+ }
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Remove captcha, password confirmation from object data after validation
+ unset($data['captcha']);
+ unset($data['passwordc']);
+
+ if ($config['site.registration.require_email_verification']) {
+ $data['flag_verified'] = false;
+ } else {
+ $data['flag_verified'] = true;
+ }
+
+ // Load default group
+ $groupSlug = $config['site.registration.user_defaults.group'];
+ $defaultGroup = $classMapper->staticMethod('group', 'where', 'slug', $groupSlug)->first();
+
+ if (!$defaultGroup) {
+ $e = new HttpException("Account registration is not working because the default group '$groupSlug' does not exist.");
+ $e->addUserMessage('REGISTRATION.BROKEN');
+ throw $e;
+ }
+
+ // Set default group
+ $data['group_id'] = $defaultGroup->id;
+
+ // Set default locale
+ $data['locale'] = $config['site.registration.user_defaults.locale'];
+
+ // Hash password
+ $data['password'] = Password::hash($data['password']);
+
+ // All checks passed! log events/activities, create user, and send verification email (if required)
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $throttler) {
+ // Log throttleable event
+ $throttler->logEvent('registration_attempt');
+
+ // Create the user
+ $user = $classMapper->createInstance('user', $data);
+
+ // Store new user to database
+ $user->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$user->user_name} registered for a new account.", [
+ 'type' => 'sign_up',
+ 'user_id' => $user->id
+ ]);
+
+ // Load default roles
+ $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs');
+ $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get();
+ $defaultRoleIds = $defaultRoles->pluck('id')->all();
+
+ // Attach default roles
+ $user->roles()->attach($defaultRoleIds);
+
+ // Verification email
+ if ($config['site.registration.require_email_verification']) {
+ // Try to generate a new verification request
+ $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']);
+
+ // Create and send verification email
+ $message = new TwigMailMessage($this->ci->view, 'mail/verify-account.html.twig');
+
+ $message->from($config['address_book.admin'])
+ ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
+ ->addParams([
+ 'user' => $user,
+ 'token' => $verification->getToken()
+ ]);
+
+ $this->ci->mailer->send($message);
+
+ $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE2', $user->toArray());
+ } else {
+ // No verification required
+ $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE1');
+ }
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes a request to resend the verification email for a new user account.
+ *
+ * Processes the request from the resend verification email form, checking that:
+ * 1. The rate limit on this type of request is observed;
+ * 2. The provided email is associated with an existing user account;
+ * 3. The user account is not already verified;
+ * 4. The submitted data is valid.
+ * This route is "public access".
+ * Request type: POST
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function resendVerification(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // Get POST parameters
+ $params = $request->getParsedBody();
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/resend-verification.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ return $response->withStatus(400);
+ }
+
+ // Throttle requests
+
+ /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */
+ $throttler = $this->ci->throttler;
+
+ $throttleData = [
+ 'email' => $data['email']
+ ];
+ $delay = $throttler->getDelay('verification_request', $throttleData);
+
+ if ($delay > 0) {
+ $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]);
+ return $response->withStatus(429);
+ }
+
+ // All checks passed! log events/activities, create user, and send verification email (if required)
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $throttler, $throttleData, $config) {
+ // Log throttleable event
+ $throttler->logEvent('verification_request', $throttleData);
+
+ // Load the user, by email address
+ $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first();
+
+ // Check that the user exists and is not already verified.
+ // If there is no user with that email address, or the user exists and is already verified,
+ // we pretend like we succeeded to prevent account enumeration
+ if ($user && $user->flag_verified != '1') {
+ // We're good to go - record user activity and send the email
+ $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']);
+
+ // Create and send verification email
+ $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig');
+
+ $message->from($config['address_book.admin'])
+ ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
+ ->addParams([
+ 'user' => $user,
+ 'token' => $verification->getToken()
+ ]);
+
+ $this->ci->mailer->send($message);
+ }
+ });
+
+ $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]);
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes a request to set the password for a new or current user.
+ *
+ * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that:
+ * 1. The provided secret token is associated with an existing user account;
+ * 2. The user has a password set/reset request in progress;
+ * 3. The token has not expired;
+ * 4. The submitted data (new password) is valid.
+ * This route is "public access".
+ * Request type: POST
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function setPassword(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // Get POST parameters
+ $params = $request->getParsedBody();
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/set-password.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ return $response->withStatus(400);
+ }
+
+ $forgotPasswordPage = $this->ci->router->pathFor('forgot-password');
+
+ // Ok, try to complete the request with the specified token and new password
+ $passwordReset = $this->ci->repoPasswordReset->complete($data['token'], [
+ 'password' => $data['password']
+ ]);
+
+ if (!$passwordReset) {
+ $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID', ['url' => $forgotPasswordPage]);
+ return $response->withStatus(400);
+ }
+
+ $ms->addMessageTranslated('success', 'PASSWORD.UPDATED');
+
+ /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $this->ci->authenticator;
+
+ // Log out any existing user, and create a new session
+ if ($authenticator->check()) {
+ $authenticator->logout();
+ }
+
+ // Auto-login the user (without "remember me")
+ $user = $passwordReset->user;
+ $authenticator->login($user);
+
+ $ms->addMessageTranslated('success', 'WELCOME', $user->export());
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes a request to update a user's account information.
+ *
+ * Processes the request from the user account settings form, checking that:
+ * 1. The user correctly input their current password;
+ * 2. They have the necessary permissions to update the posted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: POST
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function settings(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access control for entire resource - check that the current user has permission to modify themselves
+ // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify.
+ if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) {
+ $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED');
+ return $response->withStatus(403);
+ }
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ // POST parameters
+ $params = $request->getParsedBody();
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/account-settings.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ $error = false;
+
+ // Validate, and halt on validation errors.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ $error = true;
+ }
+
+ // Confirm current password
+ if (!isset($data['passwordcheck']) || !Password::verify($data['passwordcheck'], $currentUser->password)) {
+ $ms->addMessageTranslated('danger', 'PASSWORD.INVALID');
+ $error = true;
+ }
+
+ // Remove password check, password confirmation from object data after validation
+ unset($data['passwordcheck']);
+ unset($data['passwordc']);
+
+ // If new email was submitted, check that the email address is not in use
+ if (isset($data['email']) && $data['email'] != $currentUser->email && $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) {
+ $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Hash new password, if specified
+ if (isset($data['password']) && !empty($data['password'])) {
+ $data['password'] = Password::hash($data['password']);
+ } else {
+ // Do not pass to model if no password is specified
+ unset($data['password']);
+ }
+
+ // Looks good, let's update with new values!
+ // Note that only fields listed in `account-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB
+ $currentUser->fill($data);
+
+ $currentUser->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their account settings.", [
+ 'type' => 'update_account_settings'
+ ]);
+
+ $ms->addMessageTranslated('success', 'ACCOUNT.SETTINGS.UPDATED');
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Suggest an available username for a specified first/last name.
+ *
+ * This route is "public access".
+ * Request type: GET
+ * @todo Can this route be abused for account enumeration? If so we should throttle it as well.
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function suggestUsername(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $suggestion = AccountUtil::randomUniqueUsername($classMapper, 50, 10);
+
+ // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content.
+ // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating).
+ return $response->withJson([
+ 'user_name' => $suggestion
+ ], 200, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Processes an new email verification request.
+ *
+ * Processes the request from the email verification link that was emailed to the user, checking that:
+ * 1. The token provided matches a user in the database;
+ * 2. The user account is not already verified;
+ * This route is "public access".
+ * Request type: GET
+ *
+ * @param Request $request
+ * @param Response $response
+ * @param array $args
+ * @return void
+ */
+ public function verify(Request $request, Response $response, $args)
+ {
+ /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
+ $ms = $this->ci->alerts;
+
+ /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var \UserFrosting\Support\Repository\Repository $config */
+ $config = $this->ci->config;
+
+ $loginPage = $this->ci->router->pathFor('login');
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ // Load request schema
+ $schema = new RequestSchema('schema://requests/account-verify.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and halt on validation errors. This is a GET request, so we redirect on validation error.
+ $validator = new ServerSideValidator($schema, $this->ci->translator);
+ if (!$validator->validate($data)) {
+ $ms->addValidationErrors($validator);
+ // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel
+ return $response->withRedirect($loginPage, 400);
+ }
+
+ $verification = $this->ci->repoVerification->complete($data['token']);
+
+ if (!$verification) {
+ $ms->addMessageTranslated('danger', 'ACCOUNT.VERIFICATION.TOKEN_NOT_FOUND');
+ return $response->withRedirect($loginPage, 400);
+ }
+
+ $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.COMPLETE');
+
+ // Forward to login page
+ return $response->withRedirect($loginPage);
+ }
+}
diff --git a/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php b/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php
new file mode 100755
index 0000000..9713360
--- /dev/null
+++ b/main/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Controller\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Spammy request exception. Used when a bot has attempted to spam a public form, and fallen into our honeypot.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class SpammyRequestException extends HttpException
+{
+
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php
new file mode 100755
index 0000000..4e55c7c
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use UserFrosting\System\Bakery\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+
+/**
+ * Sessions table migration
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class ActivitiesTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('activities')) {
+ $this->schema->create('activities', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('ip_address', 45)->nullable();
+ $table->integer('user_id')->unsigned();
+ $table->string('type', 255)->comment('An identifier used to track the type of activity.');
+ $table->timestamp('occurred_at')->nullable();
+ $table->text('description')->nullable();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ //$table->foreign('user_id')->references('id')->on('users');
+ $table->index('user_id');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('activities');
+ }
+} \ No newline at end of file
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php
new file mode 100755
index 0000000..c74615f
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\Sprinkle\Account\Database\Models\Group;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Groups table migration
+ * "Group" now replaces the notion of "primary group" in earlier versions of UF. A user can belong to exactly one group.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class GroupsTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('groups')) {
+ $this->schema->create('groups', function(Blueprint $table) {
+ $table->increments('id');
+ $table->string('slug');
+ $table->string('name');
+ $table->text('description')->nullable();
+ $table->string('icon', 100)->nullable(false)->default('fa fa-user')->comment('The icon representing users in this group.');
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ $table->unique('slug');
+ $table->index('slug');
+ });
+
+ // Add default groups
+ $groups = [
+ 'terran' => new Group([
+ 'slug' => 'terran',
+ 'name' => 'Terran',
+ 'description' => 'The terrans are a young species with psionic potential. The terrans of the Koprulu sector descend from the survivors of a disastrous 23rd century colonization mission from Earth.',
+ 'icon' => 'sc sc-terran'
+ ]),
+ 'zerg' => new Group([
+ 'slug' => 'zerg',
+ 'name' => 'Zerg',
+ 'description' => 'Dedicated to the pursuit of genetic perfection, the zerg relentlessly hunt down and assimilate advanced species across the galaxy, incorporating useful genetic code into their own.',
+ 'icon' => 'sc sc-zerg'
+ ]),
+ 'protoss' => new Group([
+ 'slug' => 'protoss',
+ 'name' => 'Protoss',
+ 'description' => 'The protoss, a.k.a. the Firstborn, are a sapient humanoid race native to Aiur. Their advanced technology complements and enhances their psionic mastery.',
+ 'icon' => 'sc sc-protoss'
+ ])
+ ];
+
+ foreach ($groups as $slug => $group) {
+ $group->save();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('groups');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php
new file mode 100755
index 0000000..e785ccc
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * password_resets table migration
+ * Manages requests for password resets.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class passwordResetsTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('password_resets')) {
+ $this->schema->create('password_resets', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('user_id')->unsigned();
+ $table->string('hash');
+ $table->boolean('completed')->default(0);
+ $table->timestamp('expires_at')->nullable();
+ $table->timestamp('completed_at')->nullable();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ //$table->foreign('user_id')->references('id')->on('users');
+ $table->index('user_id');
+ $table->index('hash');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('password_resets');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php
new file mode 100755
index 0000000..2c2990c
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Permission_roles table migration
+ * Many-to-many mapping between permissions and roles.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class PermissionRolesTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('permission_roles')) {
+ $this->schema->create('permission_roles', function (Blueprint $table) {
+ $table->integer('permission_id')->unsigned();
+ $table->integer('role_id')->unsigned();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ $table->primary(['permission_id', 'role_id']);
+ //$table->foreign('permission_id')->references('id')->on('permissions');
+ //$table->foreign('role_id')->references('id')->on('roles');
+ $table->index('permission_id');
+ $table->index('role_id');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('permission_roles');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php
new file mode 100755
index 0000000..684b01a
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php
@@ -0,0 +1,262 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\Sprinkle\Account\Database\Models\Permission;
+use UserFrosting\Sprinkle\Account\Database\Models\Role;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Permissions table migration
+ * Permissions now replace the 'authorize_group' and 'authorize_user' tables.
+ * Also, they now map many-to-many to roles.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class PermissionsTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public $dependencies = [
+ '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\RolesTable',
+ '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\PermissionRolesTable'
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('permissions')) {
+ $this->schema->create('permissions', function(Blueprint $table) {
+ $table->increments('id');
+ $table->string('slug')->comment('A code that references a specific action or URI that an assignee of this permission has access to.');
+ $table->string('name');
+ $table->text('conditions')->comment('The conditions under which members of this group have access to this hook.');
+ $table->text('description')->nullable();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('permissions');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function seed()
+ {
+ // Skip this if table is not empty
+ if (Permission::count() == 0) {
+
+ $defaultRoleIds = [
+ 'user' => Role::where('slug', 'user')->first()->id,
+ 'group-admin' => Role::where('slug', 'group-admin')->first()->id,
+ 'site-admin' => Role::where('slug', 'site-admin')->first()->id
+ ];
+
+ // Add default permissions
+ $permissions = [
+ 'create_group' => new Permission([
+ 'slug' => 'create_group',
+ 'name' => 'Create group',
+ 'conditions' => 'always()',
+ 'description' => 'Create a new group.'
+ ]),
+ 'create_user' => new Permission([
+ 'slug' => 'create_user',
+ 'name' => 'Create user',
+ 'conditions' => 'always()',
+ 'description' => 'Create a new user in your own group and assign default roles.'
+ ]),
+ 'create_user_field' => new Permission([
+ 'slug' => 'create_user_field',
+ 'name' => 'Set new user group',
+ 'conditions' => "subset(fields,['group'])",
+ 'description' => 'Set the group when creating a new user.'
+ ]),
+ 'delete_group' => new Permission([
+ 'slug' => 'delete_group',
+ 'name' => 'Delete group',
+ 'conditions' => "always()",
+ 'description' => 'Delete a group.'
+ ]),
+ 'delete_user' => new Permission([
+ 'slug' => 'delete_user',
+ 'name' => 'Delete user',
+ 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && !is_master(user.id)",
+ 'description' => 'Delete users who are not Site Administrators.'
+ ]),
+ 'update_account_settings' => new Permission([
+ 'slug' => 'update_account_settings',
+ 'name' => 'Edit user',
+ 'conditions' => 'always()',
+ 'description' => 'Edit your own account settings.'
+ ]),
+ 'update_group_field' => new Permission([
+ 'slug' => 'update_group_field',
+ 'name' => 'Edit group',
+ 'conditions' => 'always()',
+ 'description' => 'Edit basic properties of any group.'
+ ]),
+ 'update_user_field' => new Permission([
+ 'slug' => 'update_user_field',
+ 'name' => 'Edit user',
+ 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && subset(fields,['name','email','locale','group','flag_enabled','flag_verified','password'])",
+ 'description' => 'Edit users who are not Site Administrators.'
+ ]),
+ 'update_user_field_group' => new Permission([
+ 'slug' => 'update_user_field',
+ 'name' => 'Edit group user',
+ 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])",
+ 'description' => 'Edit users in your own group who are not Site or Group Administrators, except yourself.'
+ ]),
+ 'uri_account_settings' => new Permission([
+ 'slug' => 'uri_account_settings',
+ 'name' => 'Account settings page',
+ 'conditions' => 'always()',
+ 'description' => 'View the account settings page.'
+ ]),
+ 'uri_activities' => new Permission([
+ 'slug' => 'uri_activities',
+ 'name' => 'Activity monitor',
+ 'conditions' => 'always()',
+ 'description' => 'View a list of all activities for all users.'
+ ]),
+ 'uri_dashboard' => new Permission([
+ 'slug' => 'uri_dashboard',
+ 'name' => 'Admin dashboard',
+ 'conditions' => 'always()',
+ 'description' => 'View the administrative dashboard.'
+ ]),
+ 'uri_group' => new Permission([
+ 'slug' => 'uri_group',
+ 'name' => 'View group',
+ 'conditions' => 'always()',
+ 'description' => 'View the group page of any group.'
+ ]),
+ 'uri_group_own' => new Permission([
+ 'slug' => 'uri_group',
+ 'name' => 'View own group',
+ 'conditions' => 'equals_num(self.group_id,group.id)',
+ 'description' => 'View the group page of your own group.'
+ ]),
+ 'uri_groups' => new Permission([
+ 'slug' => 'uri_groups',
+ 'name' => 'Group management page',
+ 'conditions' => 'always()',
+ 'description' => 'View a page containing a list of groups.'
+ ]),
+ 'uri_user' => new Permission([
+ 'slug' => 'uri_user',
+ 'name' => 'View user',
+ 'conditions' => 'always()',
+ 'description' => 'View the user page of any user.'
+ ]),
+ 'uri_user_in_group' => new Permission([
+ 'slug' => 'uri_user',
+ 'name' => 'View user',
+ 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id))",
+ 'description' => 'View the user page of any user in your group, except the master user and Site and Group Administrators (except yourself).'
+ ]),
+ 'uri_users' => new Permission([
+ 'slug' => 'uri_users',
+ 'name' => 'User management page',
+ 'conditions' => 'always()',
+ 'description' => 'View a page containing a table of users.'
+ ]),
+ 'view_group_field' => new Permission([
+ 'slug' => 'view_group_field',
+ 'name' => 'View group',
+ 'conditions' => "in(property,['name','icon','slug','description','users'])",
+ 'description' => 'View certain properties of any group.'
+ ]),
+ 'view_group_field_own' => new Permission([
+ 'slug' => 'view_group_field',
+ 'name' => 'View group',
+ 'conditions' => "equals_num(self.group_id,group.id) && in(property,['name','icon','slug','description','users'])",
+ 'description' => 'View certain properties of your own group.'
+ ]),
+ 'view_user_field' => new Permission([
+ 'slug' => 'view_user_field',
+ 'name' => 'View user',
+ 'conditions' => "in(property,['user_name','name','email','locale','theme','roles','group','activities'])",
+ 'description' => 'View certain properties of any user.'
+ ]),
+ 'view_user_field_group' => new Permission([
+ 'slug' => 'view_user_field',
+ 'name' => 'View user',
+ 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && in(property,['user_name','name','email','locale','roles','group','activities'])",
+ 'description' => 'View certain properties of any user in your own group, except the master user and Site and Group Administrators (except yourself).'
+ ])
+ ];
+
+ foreach ($permissions as $slug => $permission) {
+ $permission->save();
+ }
+
+ // Add default mappings to permissions
+ $roleUser = Role::where('slug', 'user')->first();
+ if ($roleUser) {
+ $roleUser->permissions()->sync([
+ $permissions['update_account_settings']->id,
+ $permissions['uri_account_settings']->id,
+ $permissions['uri_dashboard']->id
+ ]);
+ }
+
+ $roleSiteAdmin = Role::where('slug', 'site-admin')->first();
+ if ($roleSiteAdmin) {
+ $roleSiteAdmin->permissions()->sync([
+ $permissions['create_group']->id,
+ $permissions['create_user']->id,
+ $permissions['create_user_field']->id,
+ $permissions['delete_group']->id,
+ $permissions['delete_user']->id,
+ $permissions['update_user_field']->id,
+ $permissions['update_group_field']->id,
+ $permissions['uri_activities']->id,
+ $permissions['uri_group']->id,
+ $permissions['uri_groups']->id,
+ $permissions['uri_user']->id,
+ $permissions['uri_users']->id,
+ $permissions['view_group_field']->id,
+ $permissions['view_user_field']->id
+ ]);
+ }
+
+ $roleGroupAdmin = Role::where('slug', 'group-admin')->first();
+ if ($roleGroupAdmin) {
+ $roleGroupAdmin->permissions()->sync([
+ $permissions['create_user']->id,
+ $permissions['update_user_field_group']->id,
+ $permissions['uri_group_own']->id,
+ $permissions['uri_user_in_group']->id,
+ $permissions['view_group_field_own']->id,
+ $permissions['view_user_field_group']->id
+ ]);
+ }
+ }
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php
new file mode 100755
index 0000000..b96e327
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Persistences table migration
+ * Many-to-many mapping between roles and users.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class PersistencesTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('persistences')) {
+ $this->schema->create('persistences', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('user_id')->unsigned();
+ $table->string('token', 40);
+ $table->string('persistent_token', 40);
+ $table->timestamp('expires_at')->nullable();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ //$table->foreign('user_id')->references('id')->on('users');
+ $table->index('user_id');
+ $table->index('token');
+ $table->index('persistent_token');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('persistences');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php
new file mode 100755
index 0000000..7f3648b
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Role_users table migration
+ * Many-to-many mapping between roles and users.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class RoleUsersTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('role_users')) {
+ $this->schema->create('role_users', function (Blueprint $table) {
+ $table->integer('user_id')->unsigned();
+ $table->integer('role_id')->unsigned();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ $table->primary(['user_id', 'role_id']);
+ //$table->foreign('user_id')->references('id')->on('users');
+ //$table->foreign('role_id')->references('id')->on('roles');
+ $table->index('user_id');
+ $table->index('role_id');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('role_users');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php
new file mode 100755
index 0000000..9cef494
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\Sprinkle\Account\Database\Models\Role;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Roles table migration
+ * Roles replace "groups" in UF 0.3.x. Users acquire permissions through roles.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class RolesTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('roles')) {
+ $this->schema->create('roles', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('slug');
+ $table->string('name');
+ $table->text('description')->nullable();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ $table->unique('slug');
+ $table->index('slug');
+ });
+
+ // Add default roles
+ $roles = [
+ 'user' => new Role([
+ 'slug' => 'user',
+ 'name' => 'User',
+ 'description' => 'This role provides basic user functionality.'
+ ]),
+ 'site-admin' => new Role([
+ 'slug' => 'site-admin',
+ 'name' => 'Site Administrator',
+ 'description' => 'This role is meant for "site administrators", who can basically do anything except create, edit, or delete other administrators.'
+ ]),
+ 'group-admin' => new Role([
+ 'slug' => 'group-admin',
+ 'name' => 'Group Administrator',
+ 'description' => 'This role is meant for "group administrators", who can basically do anything with users in their own group, except other administrators of that group.'
+ ])
+ ];
+
+ foreach ($roles as $slug => $role) {
+ $role->save();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('roles');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php
new file mode 100755
index 0000000..a65eeed
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Users table migration
+ * Removed the 'display_name', 'title', 'secret_token', and 'flag_password_reset' fields, and added first and last name and 'last_activity_id'.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class UsersTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('users')) {
+ $this->schema->create('users', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('user_name', 50);
+ $table->string('email', 254);
+ $table->string('first_name', 20);
+ $table->string('last_name', 30);
+ $table->string('locale', 10)->default('en_US')->comment('The language and locale to use for this user.');
+ $table->string('theme', 100)->nullable()->comment("The user theme.");
+ $table->integer('group_id')->unsigned()->default(1)->comment("The id of the user group.");
+ $table->boolean('flag_verified')->default(1)->comment("Set to 1 if the user has verified their account via email, 0 otherwise.");
+ $table->boolean('flag_enabled')->default(1)->comment("Set to 1 if the user account is currently enabled, 0 otherwise. Disabled accounts cannot be logged in to, but they retain all of their data and settings.");
+ $table->integer('last_activity_id')->unsigned()->nullable()->comment("The id of the last activity performed by this user.");
+ $table->string('password', 255);
+ $table->softDeletes();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ //$table->foreign('group_id')->references('id')->on('groups');
+ //$table->foreign('last_activity_id')->references('id')->on('activities');
+ $table->unique('user_name');
+ $table->index('user_name');
+ $table->unique('email');
+ $table->index('email');
+ $table->index('group_id');
+ $table->index('last_activity_id');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('users');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php b/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php
new file mode 100755
index 0000000..fa54da6
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400;
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Schema\Builder;
+use UserFrosting\System\Bakery\Migration;
+
+/**
+ * Verifications table migration
+ * Manages requests for email account verification.
+ * Version 4.0.0
+ *
+ * See https://laravel.com/docs/5.4/migrations#tables
+ * @extends Migration
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class VerificationsTable extends Migration
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function up()
+ {
+ if (!$this->schema->hasTable('verifications')) {
+ $this->schema->create('verifications', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('user_id')->unsigned();
+ $table->string('hash');
+ $table->boolean('completed')->default(0);
+ $table->timestamp('expires_at')->nullable();
+ $table->timestamp('completed_at')->nullable();
+ $table->timestamps();
+
+ $table->engine = 'InnoDB';
+ $table->collation = 'utf8_unicode_ci';
+ $table->charset = 'utf8';
+ //$table->foreign('user_id')->references('id')->on('users');
+ $table->index('user_id');
+ $table->index('hash');
+ });
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function down()
+ {
+ $this->schema->drop('verifications');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/Activity.php b/main/app/sprinkles/account/src/Database/Models/Activity.php
new file mode 100755
index 0000000..d5be589
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/Activity.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+
+/**
+ * Activity Class
+ *
+ * Represents a single user activity at a specified point in time.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @property string ip_address
+ * @property int user_id
+ * @property string type
+ * @property datetime occurred_at
+ * @property string description
+ */
+class Activity extends Model
+{
+ /**
+ * @var string The name of the table for the current model.
+ */
+ protected $table = "activities";
+
+ protected $fillable = [
+ "ip_address",
+ "user_id",
+ "type",
+ "occurred_at",
+ "description"
+ ];
+
+ /**
+ * Joins the activity's user, so we can do things like sort, search, paginate, etc.
+ */
+ public function scopeJoinUser($query)
+ {
+ $query = $query->select('activities.*');
+
+ $query = $query->leftJoin('users', 'activities.user_id', '=', 'users.id');
+
+ return $query;
+ }
+
+ /**
+ * Add clauses to select the most recent event of each type for each user, to the query.
+ *
+ * @return \Illuminate\Database\Query\Builder
+ */
+ public function scopeMostRecentEvents($query)
+ {
+ return $query->select('user_id', 'event_type', Capsule::raw('MAX(occurred_at) as occurred_at'))
+ ->groupBy('user_id')
+ ->groupBy('type');
+ }
+
+ /**
+ * Add clauses to select the most recent event of a given type for each user, to the query.
+ *
+ * @param string $type The type of event, matching the `event_type` field in the user_event table.
+ * @return \Illuminate\Database\Query\Builder
+ */
+ public function scopeMostRecentEventsByType($query, $type)
+ {
+ return $query->select('user_id', Capsule::raw('MAX(occurred_at) as occurred_at'))
+ ->where('type', $type)
+ ->groupBy('user_id');
+ }
+
+ /**
+ * Get the user associated with this activity.
+ */
+ public function user()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/Group.php b/main/app/sprinkles/account/src/Database/Models/Group.php
new file mode 100755
index 0000000..f10e066
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/Group.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+
+/**
+ * Group Class
+ *
+ * Represents a group object as stored in the database.
+ *
+ * @package UserFrosting
+ * @author Alex Weissman
+ * @see http://www.userfrosting.com/tutorials/lesson-3-data-model/
+ *
+ * @property string slug
+ * @property string name
+ * @property string description
+ * @property string icon
+ */
+class Group extends Model
+{
+ /**
+ * @var string The name of the table for the current model.
+ */
+ protected $table = "groups";
+
+ protected $fillable = [
+ "slug",
+ "name",
+ "description",
+ "icon"
+ ];
+
+ /**
+ * @var bool Enable timestamps for this class.
+ */
+ public $timestamps = true;
+
+ /**
+ * Delete this group from the database, along with any user associations
+ *
+ * @todo What do we do with users when their group is deleted? Reassign them? Or, can a user be "groupless"?
+ */
+ public function delete()
+ {
+ // Delete the group
+ $result = parent::delete();
+
+ return $result;
+ }
+
+ /**
+ * Lazily load a collection of Users which belong to this group.
+ */
+ public function users()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->hasMany($classMapper->getClassMapping('user'), 'group_id');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/PasswordReset.php b/main/app/sprinkles/account/src/Database/Models/PasswordReset.php
new file mode 100755
index 0000000..ac8a930
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/PasswordReset.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+
+/**
+ * Password Reset Class
+ *
+ * Represents a password reset request for a specific user.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @property int user_id
+ * @property hash token
+ * @property bool completed
+ * @property datetime expires_at
+ * @property datetime completed_at
+ */
+class PasswordReset extends Model
+{
+ /**
+ * @var string The name of the table for the current model.
+ */
+ protected $table = "password_resets";
+
+ protected $fillable = [
+ "user_id",
+ "hash",
+ "completed",
+ "expires_at",
+ "completed_at"
+ ];
+
+ /**
+ * @var bool Enable timestamps for PasswordResets.
+ */
+ public $timestamps = true;
+
+ /**
+ * Stores the raw (unhashed) token when created, so that it can be emailed out to the user. NOT persisted.
+ */
+ protected $token;
+
+ /**
+ * @return string
+ */
+ public function getToken()
+ {
+ return $this->token;
+ }
+
+ /**
+ * @param string $value
+ */
+ public function setToken($value)
+ {
+ $this->token = $value;
+ return $this;
+ }
+
+ /**
+ * Get the user associated with this reset request.
+ */
+ public function user()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/Permission.php b/main/app/sprinkles/account/src/Database/Models/Permission.php
new file mode 100755
index 0000000..463af8d
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/Permission.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+
+/**
+ * Permission Class.
+ *
+ * Represents a permission for a role or user.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @property string slug
+ * @property string name
+ * @property string conditions
+ * @property string description
+ */
+class Permission extends Model
+{
+ /**
+ * @var string The name of the table for the current model.
+ */
+ protected $table = "permissions";
+
+ protected $fillable = [
+ "slug",
+ "name",
+ "conditions",
+ "description"
+ ];
+
+ /**
+ * @var bool Enable timestamps for this class.
+ */
+ public $timestamps = true;
+
+ /**
+ * Delete this permission from the database, removing associations with roles.
+ *
+ */
+ public function delete()
+ {
+ // Remove all role associations
+ $this->roles()->detach();
+
+ // Delete the permission
+ $result = parent::delete();
+
+ return $result;
+ }
+
+ /**
+ * Get a list of roles to which this permission is assigned.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ */
+ public function roles()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsToMany($classMapper->getClassMapping('role'), 'permission_roles', 'permission_id', 'role_id')->withTimestamps();
+ }
+
+ /**
+ * Query scope to get all permissions assigned to a specific role.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param int $roleId
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeForRole($query, $roleId)
+ {
+ return $query->join('permission_roles', function ($join) use ($roleId) {
+ $join->on('permission_roles.permission_id', 'permissions.id')
+ ->where('role_id', $roleId);
+ });
+ }
+
+ /**
+ * Query scope to get all permissions NOT associated with a specific role.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param int $roleId
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeNotForRole($query, $roleId)
+ {
+ return $query->join('permission_roles', function ($join) use ($roleId) {
+ $join->on('permission_roles.permission_id', 'permissions.id')
+ ->where('role_id', '!=', $roleId);
+ });
+ }
+
+ /**
+ * Get a list of users who have this permission, along with a list of roles through which each user has the permission.
+ *
+ * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough
+ */
+ public function users()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsToManyThrough(
+ $classMapper->getClassMapping('user'),
+ $classMapper->getClassMapping('role'),
+ 'permission_roles',
+ 'permission_id',
+ 'role_id',
+ 'role_users',
+ 'role_id',
+ 'user_id'
+ );
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/Role.php b/main/app/sprinkles/account/src/Database/Models/Role.php
new file mode 100755
index 0000000..ce9cb8c
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/Role.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+
+/**
+ * Role Class
+ *
+ * Represents a role, which aggregates permissions and to which a user can be assigned.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @property string slug
+ * @property string name
+ * @property string description
+ */
+class Role extends Model
+{
+ /**
+ * @var string The name of the table for the current model.
+ */
+ protected $table = "roles";
+
+ protected $fillable = [
+ "slug",
+ "name",
+ "description"
+ ];
+
+ /**
+ * @var bool Enable timestamps for this class.
+ */
+ public $timestamps = true;
+
+ /**
+ * Delete this role from the database, removing associations with permissions and users.
+ *
+ */
+ public function delete()
+ {
+ // Remove all permission associations
+ $this->permissions()->detach();
+
+ // Remove all user associations
+ $this->users()->detach();
+
+ // Delete the role
+ $result = parent::delete();
+
+ return $result;
+ }
+
+ /**
+ * Get a list of default roles.
+ */
+ public static function getDefaultSlugs()
+ {
+ /** @var UserFrosting\Config $config */
+ $config = static::$ci->config;
+
+ return array_map('trim', array_keys($config['site.registration.user_defaults.roles'], true));
+ }
+
+ /**
+ * Get a list of permissions assigned to this role.
+ */
+ public function permissions()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsToMany($classMapper->getClassMapping('permission'), 'permission_roles', 'role_id', 'permission_id')->withTimestamps();
+ }
+
+ /**
+ * Query scope to get all roles assigned to a specific user.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param int $userId
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeForUser($query, $userId)
+ {
+ return $query->join('role_users', function ($join) use ($userId) {
+ $join->on('role_users.role_id', 'roles.id')
+ ->where('user_id', $userId);
+ });
+ }
+
+ /**
+ * Get a list of users who have this role.
+ */
+ public function users()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsToMany($classMapper->getClassMapping('user'), 'role_users', 'role_id', 'user_id');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/User.php b/main/app/sprinkles/account/src/Database/Models/User.php
new file mode 100755
index 0000000..235f2ef
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/User.php
@@ -0,0 +1,493 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Carbon\Carbon;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use UserFrosting\Sprinkle\Account\Facades\Password;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+
+/**
+ * User Class
+ *
+ * Represents a User object as stored in the database.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @property int id
+ * @property string user_name
+ * @property string first_name
+ * @property string last_name
+ * @property string email
+ * @property string locale
+ * @property string theme
+ * @property int group_id
+ * @property bool flag_verified
+ * @property bool flag_enabled
+ * @property int last_activity_id
+ * @property timestamp created_at
+ * @property timestamp updated_at
+ * @property string password
+ * @property timestamp deleted_at
+ */
+class User extends Model
+{
+ use SoftDeletes;
+
+ /**
+ * The name of the table for the current model.
+ *
+ * @var string
+ */
+ protected $table = 'users';
+
+ /**
+ * Fields that should be mass-assignable when creating a new User.
+ *
+ * @var string[]
+ */
+ protected $fillable = [
+ 'user_name',
+ 'first_name',
+ 'last_name',
+ 'email',
+ 'locale',
+ 'theme',
+ 'group_id',
+ 'flag_verified',
+ 'flag_enabled',
+ 'last_activity_id',
+ 'password',
+ 'deleted_at'
+ ];
+
+ /**
+ * A list of attributes to hide by default when using toArray() and toJson().
+ *
+ * @link https://laravel.com/docs/5.4/eloquent-serialization#hiding-attributes-from-json
+ * @var string[]
+ */
+ protected $hidden = [
+ 'password'
+ ];
+
+ /**
+ * The attributes that should be mutated to dates.
+ *
+ * @var string[]
+ */
+ protected $dates = [
+ 'deleted_at'
+ ];
+
+ protected $appends = [
+ 'full_name'
+ ];
+
+ /**
+ * Cached dictionary of permissions for the user.
+ *
+ * @var array
+ */
+ protected $cachedPermissions;
+
+ /**
+ * Enable timestamps for Users.
+ *
+ * @var bool
+ */
+ public $timestamps = true;
+
+ /**
+ * Determine if the property for this object exists.
+ *
+ * We add relations here so that Twig will be able to find them.
+ * See http://stackoverflow.com/questions/29514081/cannot-access-eloquent-attributes-on-twig/35908957#35908957
+ * Every property in __get must also be implemented here for Twig to recognize it.
+ * @param string $name the name of the property to check.
+ * @return bool true if the property is defined, false otherwise.
+ */
+ public function __isset($name)
+ {
+ if (in_array($name, [
+ 'group',
+ 'last_sign_in_time',
+ 'avatar'
+ ])) {
+ return true;
+ } else {
+ return parent::__isset($name);
+ }
+ }
+
+ /**
+ * Get a property for this object.
+ *
+ * @param string $name the name of the property to retrieve.
+ * @throws Exception the property does not exist for this object.
+ * @return string the associated property.
+ */
+ public function __get($name)
+ {
+ if ($name == 'last_sign_in_time') {
+ return $this->lastActivityTime('sign_in');
+ } elseif ($name == 'avatar') {
+ // Use Gravatar as the user avatar
+ $hash = md5(strtolower(trim( $this->email)));
+ return 'https://www.gravatar.com/avatar/' . $hash . '?d=mm';
+ } else {
+ return parent::__get($name);
+ }
+ }
+
+ /**
+ * Get all activities for this user.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function activities()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->hasMany($classMapper->getClassMapping('activity'), 'user_id');
+ }
+
+ /**
+ * Delete this user from the database, along with any linked roles and activities.
+ *
+ * @param bool $hardDelete Set to true to completely remove the user and all associated objects.
+ * @return bool true if the deletion was successful, false otherwise.
+ */
+ public function delete($hardDelete = false)
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ if ($hardDelete) {
+ // Remove all role associations
+ $this->roles()->detach();
+
+ // Remove all user activities
+ $classMapper->staticMethod('activity', 'where', 'user_id', $this->id)->delete();
+
+ // Remove all user tokens
+ $classMapper->staticMethod('password_reset', 'where', 'user_id', $this->id)->delete();
+ $classMapper->staticMethod('verification', 'where', 'user_id', $this->id)->delete();
+
+ // TODO: remove any persistences
+
+ // Delete the user
+ $result = parent::forceDelete();
+ } else {
+ // Soft delete the user, leaving all associated records alone
+ $result = parent::delete();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Determines whether a user exists, including checking soft-deleted records
+ *
+ * @deprecated since 4.1.7 This method conflicts with and overrides the Builder::exists() method. Use Model::findUnique instead.
+ * @param mixed $value
+ * @param string $identifier
+ * @param bool $checkDeleted set to true to include soft-deleted records
+ * @return User|null
+ */
+ public static function exists($value, $identifier = 'user_name', $checkDeleted = true)
+ {
+ return static::findUnique($value, $identifier, $checkDeleted);
+ }
+
+ /**
+ * Return a cache instance specific to that user
+ *
+ * @return \Illuminate\Contracts\Cache\Store
+ */
+ public function getCache()
+ {
+ return static::$ci->cache->tags('_u'.$this->id);
+ }
+
+ /**
+ * Allows you to get the full name of the user using `$user->full_name`
+ *
+ * @return string
+ */
+ public function getFullNameAttribute()
+ {
+ return $this->first_name . ' ' . $this->last_name;
+ }
+
+ /**
+ * Retrieve the cached permissions dictionary for this user.
+ *
+ * @return array
+ */
+ public function getCachedPermissions()
+ {
+ if (!isset($this->cachedPermissions)) {
+ $this->reloadCachedPermissions();
+ }
+
+ return $this->cachedPermissions;
+ }
+
+ /**
+ * Retrieve the cached permissions dictionary for this user.
+ *
+ * @return User
+ */
+ public function reloadCachedPermissions()
+ {
+ $this->cachedPermissions = $this->buildPermissionsDictionary();
+
+ return $this;
+ }
+
+ /**
+ * Get the amount of time, in seconds, that has elapsed since the last activity of a certain time for this user.
+ *
+ * @param string $type The type of activity to search for.
+ * @return int
+ */
+ public function getSecondsSinceLastActivity($type)
+ {
+ $time = $this->lastActivityTime($type);
+ $time = $time ? $time : '0000-00-00 00:00:00';
+ $time = new Carbon($time);
+
+ return $time->diffInSeconds();
+ }
+
+ /**
+ * Return this user's group.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function group()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsTo($classMapper->getClassMapping('group'), 'group_id');
+ }
+
+ /**
+ * Returns whether or not this user is the master user.
+ *
+ * @return bool
+ */
+ public function isMaster()
+ {
+ $masterId = static::$ci->config['reserved_user_ids.master'];
+
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ return ($this->id == $masterId);
+ }
+
+ /**
+ * Get the most recent activity for this user, based on the user's last_activity_id.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function lastActivity()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsTo($classMapper->getClassMapping('activity'), 'last_activity_id');
+ }
+
+ /**
+ * Find the most recent activity for this user of a particular type.
+ *
+ * @param string $type
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function lastActivityOfType($type = null)
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ $query = $this->hasOne($classMapper->getClassMapping('activity'), 'user_id');
+
+ if ($type) {
+ $query = $query->where('type', $type);
+ }
+
+ return $query->latest('occurred_at');
+ }
+
+ /**
+ * Get the most recent time for a specified activity type for this user.
+ *
+ * @param string $type
+ * @return string|null The last activity time, as a SQL formatted time (YYYY-MM-DD HH:MM:SS), or null if an activity of this type doesn't exist.
+ */
+ public function lastActivityTime($type)
+ {
+ $result = $this->activities()
+ ->where('type', $type)
+ ->max('occurred_at');
+ return $result ? $result : null;
+ }
+
+ /**
+ * Performs tasks to be done after this user has been successfully authenticated.
+ *
+ * By default, adds a new sign-in activity and updates any legacy hash.
+ * @param mixed[] $params Optional array of parameters used for this event handler.
+ * @todo Transition to Laravel Event dispatcher to handle this
+ */
+ public function onLogin($params = [])
+ {
+ // Add a sign in activity (time is automatically set by database)
+ static::$ci->userActivityLogger->info("User {$this->user_name} signed in.", [
+ 'type' => 'sign_in'
+ ]);
+
+ // Update password if we had encountered an outdated hash
+ $passwordType = Password::getHashType($this->password);
+
+ if ($passwordType != 'modern') {
+ if (!isset($params['password'])) {
+ Debug::debug('Notice: Unhashed password must be supplied to update to modern password hashing.');
+ } else {
+ // Hash the user's password and update
+ $passwordHash = Password::hash($params['password']);
+ if ($passwordHash === null) {
+ Debug::debug('Notice: outdated password hash could not be updated because the new hashing algorithm is not supported. Are you running PHP >= 5.3.7?');
+ } else {
+ $this->password = $passwordHash;
+ Debug::debug('Notice: outdated password hash has been automatically updated to modern hashing.');
+ }
+ }
+ }
+
+ // Save changes
+ $this->save();
+
+ return $this;
+ }
+
+ /**
+ * Performs tasks to be done after this user has been logged out.
+ *
+ * By default, adds a new sign-out activity.
+ * @param mixed[] $params Optional array of parameters used for this event handler.
+ * @todo Transition to Laravel Event dispatcher to handle this
+ */
+ public function onLogout($params = [])
+ {
+ static::$ci->userActivityLogger->info("User {$this->user_name} signed out.", [
+ 'type' => 'sign_out'
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Get all password reset requests for this user.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function passwordResets()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->hasMany($classMapper->getClassMapping('password_reset'), 'user_id');
+ }
+
+ /**
+ * Get all of the permissions this user has, via its roles.
+ *
+ * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough
+ */
+ public function permissions()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsToManyThrough(
+ $classMapper->getClassMapping('permission'),
+ $classMapper->getClassMapping('role'),
+ 'role_users',
+ 'user_id',
+ 'role_id',
+ 'permission_roles',
+ 'role_id',
+ 'permission_id'
+ );
+ }
+
+ /**
+ * Get all roles to which this user belongs.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ */
+ public function roles()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsToMany($classMapper->getClassMapping('role'), 'role_users', 'user_id', 'role_id')->withTimestamps();
+ }
+
+ /**
+ * Query scope to get all users who have a specific role.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @param int $roleId
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeForRole($query, $roleId)
+ {
+ return $query->join('role_users', function ($join) use ($roleId) {
+ $join->on('role_users.user_id', 'users.id')
+ ->where('role_id', $roleId);
+ });
+ }
+
+ /**
+ * Joins the user's most recent activity directly, so we can do things like sort, search, paginate, etc.
+ *
+ * @param \Illuminate\Database\Eloquent\Builder $query
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ public function scopeJoinLastActivity($query)
+ {
+ $query = $query->select('users.*');
+
+ $query = $query->leftJoin('activities', 'activities.id', '=', 'users.last_activity_id');
+
+ return $query;
+ }
+
+ /**
+ * Loads permissions for this user into a cached dictionary of slugs -> arrays of permissions,
+ * so we don't need to keep requerying the DB for every call of checkAccess.
+ *
+ * @return array
+ */
+ protected function buildPermissionsDictionary()
+ {
+ $permissions = $this->permissions()->get();
+ $cachedPermissions = [];
+
+ foreach ($permissions as $permission) {
+ $cachedPermissions[$permission->slug][] = $permission;
+ }
+
+ return $cachedPermissions;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Database/Models/Verification.php b/main/app/sprinkles/account/src/Database/Models/Verification.php
new file mode 100755
index 0000000..cd5166d
--- /dev/null
+++ b/main/app/sprinkles/account/src/Database/Models/Verification.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Database\Models;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+
+/**
+ * Verification Class
+ *
+ * Represents a pending email verification for a new user account.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @property int user_id
+ * @property hash token
+ * @property bool completed
+ * @property datetime expires_at
+ * @property datetime completed_at
+ */
+class Verification extends Model
+{
+ /**
+ * @var string The name of the table for the current model.
+ */
+ protected $table = "verifications";
+
+ protected $fillable = [
+ "user_id",
+ "hash",
+ "completed",
+ "expires_at",
+ "completed_at"
+ ];
+
+ /**
+ * @var bool Enable timestamps for Verifications.
+ */
+ public $timestamps = true;
+
+ /**
+ * Stores the raw (unhashed) token when created, so that it can be emailed out to the user. NOT persisted.
+ */
+ protected $token;
+
+ public function getToken()
+ {
+ return $this->token;
+ }
+
+ public function setToken($value)
+ {
+ $this->token = $value;
+ return $this;
+ }
+
+ /**
+ * Get the user associated with this verification request.
+ */
+ public function user()
+ {
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = static::$ci->classMapper;
+
+ return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id');
+ }
+}
diff --git a/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php b/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php
new file mode 100755
index 0000000..330ca65
--- /dev/null
+++ b/main/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Error\Handler;
+
+use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler;
+
+/**
+ * Handler for AuthCompromisedExceptions.
+ *
+ * Warns the user that their account may have been compromised due to a stolen "remember me" cookie.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthCompromisedExceptionHandler extends HttpExceptionHandler
+{
+ /**
+ * Render a generic, user-friendly response without sensitive debugging information.
+ *
+ * @return ResponseInterface
+ */
+ public function renderGenericResponse()
+ {
+ $template = $this->ci->view->getEnvironment()->loadTemplate('pages/error/compromised.html.twig');
+
+ return $this->response
+ ->withStatus($this->statusCode)
+ ->withHeader('Content-type', $this->contentType)
+ ->write($template->render());
+ }
+}
diff --git a/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php b/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php
new file mode 100755
index 0000000..c651f77
--- /dev/null
+++ b/main/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Error\Handler;
+
+use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler;
+
+/**
+ * Handler for AuthExpiredExceptions.
+ *
+ * Forwards the user to the login page when their session has expired.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthExpiredExceptionHandler extends HttpExceptionHandler
+{
+ /**
+ * Custom handling for requests that did not pass authentication.
+ */
+ public function handle()
+ {
+ // For auth expired exceptions, we always add messages to the alert stream.
+ $this->writeAlerts();
+
+ $response = $this->response;
+
+ // For non-AJAX requests, we forward the user to the login page.
+ if (!$this->request->isXhr()) {
+ $uri = $this->request->getUri();
+ $path = $uri->getPath();
+ $query = $uri->getQuery();
+ $fragment = $uri->getFragment();
+
+ $path = $path
+ . ($query ? '?' . $query : '')
+ . ($fragment ? '#' . $fragment : '');
+
+ $loginPage = $this->ci->router->pathFor('login', [], [
+ 'redirect' => $path
+ ]);
+
+ $response = $response->withRedirect($loginPage);
+ }
+
+ return $response;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php b/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php
new file mode 100755
index 0000000..e22f02b
--- /dev/null
+++ b/main/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Error\Handler;
+
+use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler;
+use UserFrosting\Support\Message\UserMessage;
+
+/**
+ * Handler for ForbiddenExceptions. Only really needed to override the default error message.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class ForbiddenExceptionHandler extends HttpExceptionHandler
+{
+ /**
+ * Resolve a list of error messages to present to the end user.
+ *
+ * @return array
+ */
+ protected function determineUserMessages()
+ {
+ return [
+ new UserMessage("ACCOUNT.ACCESS_DENIED")
+ ];
+ }
+}
diff --git a/main/app/sprinkles/account/src/Facades/Password.php b/main/app/sprinkles/account/src/Facades/Password.php
new file mode 100755
index 0000000..e5bf967
--- /dev/null
+++ b/main/app/sprinkles/account/src/Facades/Password.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Facades;
+
+use UserFrosting\System\Facade;
+
+/**
+ * Implements facade for the "password" service
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class Password extends Facade
+{
+ /**
+ * Get the registered name of the component.
+ *
+ * @return string
+ */
+ protected static function getFacadeAccessor()
+ {
+ return 'passwordHasher';
+ }
+}
diff --git a/main/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php b/main/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php
new file mode 100755
index 0000000..d7ceeef
--- /dev/null
+++ b/main/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Log;
+
+use UserFrosting\Sprinkle\Core\Log\DatabaseHandler;
+
+/**
+ * Monolog handler for storing user activities to the database.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class UserActivityDatabaseHandler extends DatabaseHandler
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected function write(array $record)
+ {
+ $log = $this->classMapper->createInstance($this->modelName, $record['extra']);
+ $log->save();
+
+ if (isset($record['extra']['user_id'])) {
+ $user = $this->classMapper->staticMethod('user', 'find', $record['extra']['user_id']);
+ $user->last_activity_id = $log->id;
+ $user->save();
+ }
+ }
+}
diff --git a/main/app/sprinkles/account/src/Log/UserActivityProcessor.php b/main/app/sprinkles/account/src/Log/UserActivityProcessor.php
new file mode 100755
index 0000000..2575270
--- /dev/null
+++ b/main/app/sprinkles/account/src/Log/UserActivityProcessor.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Log;
+
+use Monolog\Logger;
+
+/**
+ * Monolog processor for constructing the user activity message.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class UserActivityProcessor
+{
+ /**
+ * @var int
+ */
+ protected $userId;
+
+ /**
+ * @param int $userId The id of the user for whom we will be logging activities.
+ */
+ public function __construct($userId)
+ {
+ $this->userId = $userId;
+ }
+
+ public function __invoke(array $record)
+ {
+ $additionalFields = [
+ 'ip_address' => $_SERVER['REMOTE_ADDR'],
+ 'user_id' => $this->userId,
+ 'occurred_at' => $record['datetime'],
+ 'description' => $record['message']
+ ];
+
+ $record['extra'] = array_replace_recursive($record['extra'], $additionalFields, $record['context']);
+
+ return $record;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php b/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php
new file mode 100755
index 0000000..2dcffd3
--- /dev/null
+++ b/main/app/sprinkles/account/src/Repository/PasswordResetRepository.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Repository;
+
+use UserFrosting\Sprinkle\Account\Facades\Password;
+
+/**
+ * Token repository class for password reset requests.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @see https://learn.userfrosting.com/users/user-accounts
+ */
+class PasswordResetRepository extends TokenRepository
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected $modelIdentifier = 'password_reset';
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function updateUser($user, $args)
+ {
+ $user->password = Password::hash($args['password']);
+ // TODO: generate user activity? or do this in controller?
+ $user->save();
+ }
+}
diff --git a/main/app/sprinkles/account/src/Repository/TokenRepository.php b/main/app/sprinkles/account/src/Repository/TokenRepository.php
new file mode 100755
index 0000000..a299439
--- /dev/null
+++ b/main/app/sprinkles/account/src/Repository/TokenRepository.php
@@ -0,0 +1,230 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Repository;
+
+use Carbon\Carbon;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Core\Database\Models\Model;
+use UserFrosting\Sprinkle\Core\Util\ClassMapper;
+
+/**
+ * An abstract class for interacting with a repository of time-sensitive user tokens.
+ *
+ * User tokens are used, for example, to perform password resets and new account email verifications.
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @see https://learn.userfrosting.com/users/user-accounts
+ */
+abstract class TokenRepository
+{
+
+ /**
+ * @var ClassMapper
+ */
+ protected $classMapper;
+
+ /**
+ * @var string
+ */
+ protected $algorithm;
+
+ /**
+ * @var string
+ */
+ protected $modelIdentifier;
+
+ /**
+ * Create a new TokenRepository object.
+ *
+ * @param ClassMapper $classMapper Maps generic class identifiers to specific class names.
+ * @param string $algorithm The hashing algorithm to use when storing generated tokens.
+ */
+ public function __construct(ClassMapper $classMapper, $algorithm = 'sha512')
+ {
+ $this->classMapper = $classMapper;
+ $this->algorithm = $algorithm;
+ }
+
+ /**
+ * Cancels a specified token by removing it from the database.
+ *
+ * @param int $token The token to remove.
+ * @return Model|false
+ */
+ public function cancel($token)
+ {
+ // Hash the password reset token for the stored version
+ $hash = hash($this->algorithm, $token);
+
+ // Find an incomplete reset request for the specified hash
+ $model = $this->classMapper
+ ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash)
+ ->where('completed', false)
+ ->first();
+
+ if ($model === null) {
+ return false;
+ }
+
+ $model->delete();
+
+ return $model;
+ }
+
+ /**
+ * Completes a token-based process, invoking updateUser() in the child object to do the actual action.
+ *
+ * @param int $token The token to complete.
+ * @param mixed[] $userParams An optional list of parameters to pass to updateUser().
+ * @return Model|false
+ */
+ public function complete($token, $userParams = [])
+ {
+ // Hash the token for the stored version
+ $hash = hash($this->algorithm, $token);
+
+ // Find an unexpired, incomplete token for the specified hash
+ $model = $this->classMapper
+ ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash)
+ ->where('completed', false)
+ ->where('expires_at', '>', Carbon::now())
+ ->first();
+
+ if ($model === null) {
+ return false;
+ }
+
+ // Fetch user for this token
+ $user = $this->classMapper->staticMethod('user', 'find', $model->user_id);
+
+ if (is_null($user)) {
+ return false;
+ }
+
+ $this->updateUser($user, $userParams);
+
+ $model->fill([
+ 'completed' => true,
+ 'completed_at' => Carbon::now()
+ ]);
+
+ $model->save();
+
+ return $model;
+ }
+
+ /**
+ * Create a new token for a specified user.
+ *
+ * @param User $user The user object to associate with this token.
+ * @param int $timeout The time, in seconds, after which this token should expire.
+ * @return Model The model (PasswordReset, Verification, etc) object that stores the token.
+ */
+ public function create(User $user, $timeout)
+ {
+ // Remove any previous tokens for this user
+ $this->removeExisting($user);
+
+ // Compute expiration time
+ $expiresAt = Carbon::now()->addSeconds($timeout);
+
+ $model = $this->classMapper->createInstance($this->modelIdentifier);
+
+ // Generate a random token
+ $model->setToken($this->generateRandomToken());
+
+ // Hash the password reset token for the stored version
+ $hash = hash($this->algorithm, $model->getToken());
+
+ $model->fill([
+ 'hash' => $hash,
+ 'completed' => false,
+ 'expires_at' => $expiresAt
+ ]);
+
+ $model->user_id = $user->id;
+
+ $model->save();
+
+ return $model;
+ }
+
+ /**
+ * Determine if a specified user has an incomplete and unexpired token.
+ *
+ * @param User $user The user object to look up.
+ * @param int $token Optionally, try to match a specific token.
+ * @return Model|false
+ */
+ public function exists(User $user, $token = null)
+ {
+ $model = $this->classMapper
+ ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id)
+ ->where('completed', false)
+ ->where('expires_at', '>', Carbon::now());
+
+ if ($token) {
+ // get token hash
+ $hash = hash($this->algorithm, $token);
+ $model->where('hash', $hash);
+ }
+
+ return $model->first() ?: false;
+ }
+
+ /**
+ * Delete all existing tokens from the database for a particular user.
+ *
+ * @param User $user
+ * @return int
+ */
+ protected function removeExisting(User $user)
+ {
+ return $this->classMapper
+ ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id)
+ ->delete();
+ }
+
+ /**
+ * Remove all expired tokens from the database.
+ *
+ * @return bool|null
+ */
+ public function removeExpired()
+ {
+ return $this->classMapper
+ ->staticMethod($this->modelIdentifier, 'where', 'completed', false)
+ ->where('expires_at', '<', Carbon::now())
+ ->delete();
+ }
+
+ /**
+ * Generate a new random token for this user.
+ *
+ * This generates a token to use for verifying a new account, resetting a lost password, etc.
+ * @param string $gen specify an existing token that, if we happen to generate the same value, we should regenerate on.
+ * @return string
+ */
+ protected function generateRandomToken($gen = null)
+ {
+ do {
+ $gen = md5(uniqid(mt_rand(), false));
+ } while($this->classMapper
+ ->staticMethod($this->modelIdentifier, 'where', 'hash', hash($this->algorithm, $gen))
+ ->first());
+ return $gen;
+ }
+
+ /**
+ * Modify the user during the token completion process.
+ *
+ * This method is called during complete(), and is a way for concrete implementations to modify the user.
+ * @param User $user the user object to modify.
+ * @return mixed[] $args the list of parameters that were supplied to the call to `complete()`
+ */
+ abstract protected function updateUser($user, $args);
+}
diff --git a/main/app/sprinkles/account/src/Repository/VerificationRepository.php b/main/app/sprinkles/account/src/Repository/VerificationRepository.php
new file mode 100755
index 0000000..b0cf048
--- /dev/null
+++ b/main/app/sprinkles/account/src/Repository/VerificationRepository.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Repository;
+
+/**
+ * Token repository class for new account verifications.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * @see https://learn.userfrosting.com/users/user-accounts
+ */
+class VerificationRepository extends TokenRepository
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected $modelIdentifier = 'verification';
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function updateUser($user, $args)
+ {
+ $user->flag_verified = 1;
+ // TODO: generate user activity? or do this in controller?
+ $user->save();
+ }
+}
diff --git a/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php b/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php
new file mode 100755
index 0000000..4c3ab15
--- /dev/null
+++ b/main/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php
@@ -0,0 +1,444 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\ServicesProvider;
+
+use Birke\Rememberme\Authenticator as RememberMe;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Monolog\Formatter\LineFormatter;
+use Monolog\Handler\ErrorLogHandler;
+use Monolog\Handler\StreamHandler;
+use Monolog\Logger;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use UserFrosting\Sprinkle\Account\Authenticate\Authenticator;
+use UserFrosting\Sprinkle\Account\Authenticate\AuthGuard;
+use UserFrosting\Sprinkle\Account\Authenticate\Hasher;
+use UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Account\Log\UserActivityDatabaseHandler;
+use UserFrosting\Sprinkle\Account\Log\UserActivityProcessor;
+use UserFrosting\Sprinkle\Account\Repository\PasswordResetRepository;
+use UserFrosting\Sprinkle\Account\Repository\VerificationRepository;
+use UserFrosting\Sprinkle\Account\Twig\AccountExtension;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Log\MixedFormatter;
+
+/**
+ * Registers services for the account sprinkle, such as currentUser, etc.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class ServicesProvider
+{
+ /**
+ * Register UserFrosting's account services.
+ *
+ * @param Container $container A DI container implementing ArrayAccess and container-interop.
+ */
+ public function register($container)
+ {
+ /**
+ * Extend the asset manager service to see assets for the current user's theme.
+ */
+ $container->extend('assets', function ($assets, $c) {
+
+ // Register paths for user theme, if a user is logged in
+ // We catch any authorization-related exceptions, so that error pages can be rendered.
+ try {
+ /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $c->authenticator;
+ $currentUser = $c->currentUser;
+ } catch (\Exception $e) {
+ return $assets;
+ }
+
+ if ($authenticator->check()) {
+ $c->sprinkleManager->addResource('assets', $currentUser->theme);
+ }
+
+ return $assets;
+ });
+
+ /**
+ * Extend the 'classMapper' service to register model classes.
+ *
+ * Mappings added: User, Group, Role, Permission, Activity, PasswordReset, Verification
+ */
+ $container->extend('classMapper', function ($classMapper, $c) {
+ $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Account\Database\Models\User');
+ $classMapper->setClassMapping('group', 'UserFrosting\Sprinkle\Account\Database\Models\Group');
+ $classMapper->setClassMapping('role', 'UserFrosting\Sprinkle\Account\Database\Models\Role');
+ $classMapper->setClassMapping('permission', 'UserFrosting\Sprinkle\Account\Database\Models\Permission');
+ $classMapper->setClassMapping('activity', 'UserFrosting\Sprinkle\Account\Database\Models\Activity');
+ $classMapper->setClassMapping('password_reset', 'UserFrosting\Sprinkle\Account\Database\Models\PasswordReset');
+ $classMapper->setClassMapping('verification', 'UserFrosting\Sprinkle\Account\Database\Models\Verification');
+ return $classMapper;
+ });
+
+ /**
+ * Extends the 'errorHandler' service with custom exception handlers.
+ *
+ * Custom handlers added: ForbiddenExceptionHandler
+ */
+ $container->extend('errorHandler', function ($handler, $c) {
+ // Register the ForbiddenExceptionHandler.
+ $handler->registerHandler('\UserFrosting\Support\Exception\ForbiddenException', '\UserFrosting\Sprinkle\Account\Error\Handler\ForbiddenExceptionHandler');
+ // Register the AuthExpiredExceptionHandler
+ $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthExpiredExceptionHandler');
+ // Register the AuthCompromisedExceptionHandler.
+ $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthCompromisedExceptionHandler');
+ return $handler;
+ });
+
+ /**
+ * Extends the 'localePathBuilder' service, adding any locale files from the user theme.
+ *
+ */
+ $container->extend('localePathBuilder', function ($pathBuilder, $c) {
+ // Add paths for user theme, if a user is logged in
+ // We catch any authorization-related exceptions, so that error pages can be rendered.
+ try {
+ /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $c->authenticator;
+ $currentUser = $c->currentUser;
+ } catch (\Exception $e) {
+ return $pathBuilder;
+ }
+
+ if ($authenticator->check()) {
+ // Add paths to locale files for user theme
+ $themePath = $c->sprinkleManager->addResource('locale', $currentUser->theme);
+
+ // Add user locale
+ $pathBuilder->addLocales($currentUser->locale);
+ }
+
+ return $pathBuilder;
+ });
+
+ /**
+ * Extends the 'view' service with the AccountExtension for Twig.
+ *
+ * Adds account-specific functions, globals, filters, etc to Twig, and the path to templates for the user theme.
+ */
+ $container->extend('view', function ($view, $c) {
+ $twig = $view->getEnvironment();
+ $extension = new AccountExtension($c);
+ $twig->addExtension($extension);
+
+ // Add paths for user theme, if a user is logged in
+ // We catch any authorization-related exceptions, so that error pages can be rendered.
+ try {
+ /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */
+ $authenticator = $c->authenticator;
+ $currentUser = $c->currentUser;
+ } catch (\Exception $e) {
+ return $view;
+ }
+
+ if ($authenticator->check()) {
+ $theme = $currentUser->theme;
+ $themePath = $c->sprinkleManager->addResource('templates', $theme);
+ if ($themePath) {
+ $loader = $twig->getLoader();
+ $loader->prependPath($themePath);
+ // Add namespaced path as well
+ $loader->addPath($themePath, $theme);
+ }
+ }
+
+ return $view;
+ });
+
+ /**
+ * Authentication service.
+ *
+ * Supports logging in users, remembering their sessions, etc.
+ */
+ $container['authenticator'] = function ($c) {
+ $classMapper = $c->classMapper;
+ $config = $c->config;
+ $session = $c->session;
+ $cache = $c->cache;
+
+ // Force database connection to boot up
+ $c->db;
+
+ // Fix RememberMe table name
+ $config['remember_me.table.tableName'] = Capsule::connection()->getTablePrefix() . $config['remember_me.table.tableName'];
+
+ $authenticator = new Authenticator($classMapper, $session, $config, $cache);
+ return $authenticator;
+ };
+
+ /**
+ * Sets up the AuthGuard middleware, used to limit access to authenticated users for certain routes.
+ */
+ $container['authGuard'] = function ($c) {
+ $authenticator = $c->authenticator;
+ return new AuthGuard($authenticator);
+ };
+
+ /**
+ * Authorization check logging with Monolog.
+ *
+ * Extend this service to push additional handlers onto the 'auth' log stack.
+ */
+ $container['authLogger'] = function ($c) {
+ $logger = new Logger('auth');
+
+ $logFile = $c->get('locator')->findResource('log://userfrosting.log', true, true);
+
+ $handler = new StreamHandler($logFile);
+
+ $formatter = new MixedFormatter(null, null, true);
+
+ $handler->setFormatter($formatter);
+ $logger->pushHandler($handler);
+
+ return $logger;
+ };
+
+ /**
+ * Authorization service.
+ *
+ * Determines permissions for user actions. Extend this service to add additional access condition callbacks.
+ */
+ $container['authorizer'] = function ($c) {
+ $config = $c->config;
+
+ // Default access condition callbacks. Add more in your sprinkle by using $container->extend(...)
+ $callbacks = [
+ /**
+ * Unconditionally grant permission - use carefully!
+ * @return bool returns true no matter what.
+ */
+ 'always' => function () {
+ return true;
+ },
+
+ /**
+ * Check if the specified values are identical to one another (strict comparison).
+ * @param mixed $val1 the first value to compare.
+ * @param mixed $val2 the second value to compare.
+ * @return bool true if the values are strictly equal, false otherwise.
+ */
+ 'equals' => function ($val1, $val2) {
+ return ($val1 === $val2);
+ },
+
+ /**
+ * Check if the specified values are numeric, and if so, if they are equal to each other.
+ * @param mixed $val1 the first value to compare.
+ * @param mixed $val2 the second value to compare.
+ * @return bool true if the values are numeric and equal, false otherwise.
+ */
+ 'equals_num' => function ($val1, $val2) {
+ if (!is_numeric($val1)) {
+ return false;
+ }
+ if (!is_numeric($val2)) {
+ return false;
+ }
+
+ return ($val1 == $val2);
+ },
+
+ /**
+ * Check if the specified user (by user_id) has a particular role.
+ *
+ * @param int $user_id the id of the user.
+ * @param int $role_id the id of the role.
+ * @return bool true if the user has the role, false otherwise.
+ */
+ 'has_role' => function ($user_id, $role_id) {
+ return Capsule::table('role_users')
+ ->where('user_id', $user_id)
+ ->where('role_id', $role_id)
+ ->count() > 0;
+ },
+
+ /**
+ * Check if the specified value $needle is in the values of $haystack.
+ *
+ * @param mixed $needle the value to look for in $haystack
+ * @param array[mixed] $haystack the array of values to search.
+ * @return bool true if $needle is present in the values of $haystack, false otherwise.
+ */
+ 'in' => function ($needle, $haystack) {
+ return in_array($needle, $haystack);
+ },
+
+ /**
+ * Check if the specified user (by user_id) is in a particular group.
+ *
+ * @param int $user_id the id of the user.
+ * @param int $group_id the id of the group.
+ * @return bool true if the user is in the group, false otherwise.
+ */
+ 'in_group' => function ($user_id, $group_id) {
+ $user = User::find($user_id);
+ return ($user->group_id == $group_id);
+ },
+
+ /**
+ * Check if the specified user (by user_id) is the master user.
+ *
+ * @param int $user_id the id of the user.
+ * @return bool true if the user id is equal to the id of the master account, false otherwise.
+ */
+ 'is_master' => function ($user_id) use ($config) {
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ return ($user_id == $config['reserved_user_ids.master']);
+ },
+
+ /**
+ * Check if all values in the array $needle are present in the values of $haystack.
+ *
+ * @param array[mixed] $needle the array whose values we should look for in $haystack
+ * @param array[mixed] $haystack the array of values to search.
+ * @return bool true if every value in $needle is present in the values of $haystack, false otherwise.
+ */
+ 'subset' => function ($needle, $haystack) {
+ return count($needle) == count(array_intersect($needle, $haystack));
+ },
+
+ /**
+ * Check if all keys of the array $needle are present in the values of $haystack.
+ *
+ * This function is useful for whitelisting an array of key-value parameters.
+ * @param array[mixed] $needle the array whose keys we should look for in $haystack
+ * @param array[mixed] $haystack the array of values to search.
+ * @return bool true if every key in $needle is present in the values of $haystack, false otherwise.
+ */
+ 'subset_keys' => function ($needle, $haystack) {
+ return count($needle) == count(array_intersect(array_keys($needle), $haystack));
+ }
+ ];
+
+ $authorizer = new AuthorizationManager($c, $callbacks);
+ return $authorizer;
+ };
+
+ /**
+ * Loads the User object for the currently logged-in user.
+ */
+ $container['currentUser'] = function ($c) {
+ $authenticator = $c->authenticator;
+
+ return $authenticator->user();
+ };
+
+ $container['passwordHasher'] = function ($c) {
+ $hasher = new Hasher();
+ return $hasher;
+ };
+
+ /**
+ * Returns a callback that forwards to dashboard if user is already logged in.
+ */
+ $container['redirect.onAlreadyLoggedIn'] = function ($c) {
+ /**
+ * This method is invoked when a user attempts to perform certain public actions when they are already logged in.
+ *
+ * @todo Forward to user's landing page or last visited page
+ * @param \Psr\Http\Message\ServerRequestInterface $request
+ * @param \Psr\Http\Message\ResponseInterface $response
+ * @param array $args
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ return function (Request $request, Response $response, array $args) use ($c) {
+ $redirect = $c->router->pathFor('dashboard');
+
+ return $response->withRedirect($redirect, 302);
+ };
+ };
+
+ /**
+ * Returns a callback that handles setting the `UF-Redirect` header after a successful login.
+ */
+ $container['redirect.onLogin'] = function ($c) {
+ /**
+ * This method is invoked when a user completes the login process.
+ *
+ * Returns a callback that handles setting the `UF-Redirect` header after a successful login.
+ * @param \Psr\Http\Message\ServerRequestInterface $request
+ * @param \Psr\Http\Message\ResponseInterface $response
+ * @param array $args
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ return function (Request $request, Response $response, array $args) use ($c) {
+ // Backwards compatibility for the deprecated determineRedirectOnLogin service
+ if ($c->has('determineRedirectOnLogin')) {
+ $determineRedirectOnLogin = $c->determineRedirectOnLogin;
+
+ return $determineRedirectOnLogin($response)->withStatus(200);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $c->authorizer;
+
+ $currentUser = $c->authenticator->user();
+
+ if ($authorizer->checkAccess($currentUser, 'uri_account_settings')) {
+ return $response->withHeader('UF-Redirect', $c->router->pathFor('settings'));
+ } else {
+ return $response->withHeader('UF-Redirect', $c->router->pathFor('index'));
+ }
+ };
+ };
+
+ /**
+ * Repository for password reset requests.
+ */
+ $container['repoPasswordReset'] = function ($c) {
+ $classMapper = $c->classMapper;
+ $config = $c->config;
+
+ $repo = new PasswordResetRepository($classMapper, $config['password_reset.algorithm']);
+ return $repo;
+ };
+
+ /**
+ * Repository for verification requests.
+ */
+ $container['repoVerification'] = function ($c) {
+ $classMapper = $c->classMapper;
+ $config = $c->config;
+
+ $repo = new VerificationRepository($classMapper, $config['verification.algorithm']);
+ return $repo;
+ };
+
+ /**
+ * Logger for logging the current user's activities to the database.
+ *
+ * Extend this service to push additional handlers onto the 'userActivity' log stack.
+ */
+ $container['userActivityLogger'] = function ($c) {
+ $classMapper = $c->classMapper;
+ $config = $c->config;
+ $session = $c->session;
+
+ $logger = new Logger('userActivity');
+
+ $handler = new UserActivityDatabaseHandler($classMapper, 'activity');
+
+ // Note that we get the user id from the session, not the currentUser service.
+ // This is because the currentUser service may not reflect the actual user during login/logout requests.
+ $currentUserIdKey = $config['session.keys.current_user_id'];
+ $userId = isset($session[$currentUserIdKey]) ? $session[$currentUserIdKey] : $config['reserved_user_ids.guest'];
+ $processor = new UserActivityProcessor($userId);
+
+ $logger->pushProcessor($processor);
+ $logger->pushHandler($handler);
+
+ return $logger;
+ };
+ }
+}
diff --git a/main/app/sprinkles/account/src/Twig/AccountExtension.php b/main/app/sprinkles/account/src/Twig/AccountExtension.php
new file mode 100755
index 0000000..12bacba
--- /dev/null
+++ b/main/app/sprinkles/account/src/Twig/AccountExtension.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Twig;
+
+use Interop\Container\ContainerInterface;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use Slim\Http\Uri;
+
+/**
+ * Extends Twig functionality for the Account sprinkle.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface
+{
+
+ protected $services;
+ protected $config;
+
+ public function __construct(ContainerInterface $services)
+ {
+ $this->services = $services;
+ $this->config = $services->config;
+ }
+
+ public function getName()
+ {
+ return 'userfrosting/account';
+ }
+
+ public function getFunctions()
+ {
+ return array(
+ // Add Twig function for checking permissions during dynamic menu rendering
+ new \Twig_SimpleFunction('checkAccess', function ($slug, $params = []) {
+ $authorizer = $this->services->authorizer;
+ $currentUser = $this->services->currentUser;
+
+ return $authorizer->checkAccess($currentUser, $slug, $params);
+ }),
+ new \Twig_SimpleFunction('checkAuthenticated', function () {
+ $authenticator = $this->services->authenticator;
+ return $authenticator->check();
+ })
+ );
+ }
+
+ public function getGlobals()
+ {
+ try {
+ $currentUser = $this->services->currentUser;
+ } catch (\Exception $e) {
+ $currentUser = null;
+ }
+
+ return [
+ 'current_user' => $currentUser
+ ];
+ }
+}
diff --git a/main/app/sprinkles/account/src/Util/HashFailedException.php b/main/app/sprinkles/account/src/Util/HashFailedException.php
new file mode 100755
index 0000000..a0b37d1
--- /dev/null
+++ b/main/app/sprinkles/account/src/Util/HashFailedException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Util;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Password hash failure exception. Used when the supplied password could not be hashed for some reason.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class HashFailedException extends HttpException
+{
+ protected $defaultMessage = 'PASSWORD.HASH_FAILED';
+ protected $httpErrorCode = 500;
+}
diff --git a/main/app/sprinkles/account/src/Util/Util.php b/main/app/sprinkles/account/src/Util/Util.php
new file mode 100755
index 0000000..6452990
--- /dev/null
+++ b/main/app/sprinkles/account/src/Util/Util.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Account\Util;
+
+use UserFrosting\Sprinkle\Core\Util\Util as CoreUtil;
+
+/**
+ * Util Class
+ *
+ * Static utility functions for the account Sprinkle.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class Util
+{
+ /**
+ * Generate a random, unique username from a list of adjectives and nouns.
+ */
+ static public function randomUniqueUsername($classMapper, $maxLength, $maxTries = 10)
+ {
+ for ($n = 1; $n <= 3; $n++) {
+ for ($m = 0; $m < 10; $m++) {
+ // Generate a random phrase with $n adjectives
+ $suggestion = CoreUtil::randomPhrase($n, $maxLength, $maxTries, '.');
+ if (!$classMapper->staticMethod('user', 'where', 'user_name', $suggestion)->first()) {
+ return $suggestion;
+ }
+ }
+ }
+
+ return '';
+ }
+
+}
diff --git a/main/app/sprinkles/account/templates/forms/settings-account.html.twig b/main/app/sprinkles/account/templates/forms/settings-account.html.twig
new file mode 100755
index 0000000..996b27b
--- /dev/null
+++ b/main/app/sprinkles/account/templates/forms/settings-account.html.twig
@@ -0,0 +1,37 @@
+<form id="account-settings" role="form" action="{{site.uri.public}}/account/settings" method="post">
+ <div class="box-header">
+ <h3 class="box-title"><i class="fa fa-gear fa-fw"></i> {{translate("ACCOUNT.SETTINGS")}}</h3>
+ </div>
+ <div class="box-body">
+ {% include "forms/csrf.html.twig" %}
+ <!-- Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 -->
+ <input type="text" style="display:none">
+ <input type="password" style="display:none">
+
+ {% block settings_account %}
+ <div class="form-group">
+ <label for="input-email" class="ccontrol-label">{{translate("EMAIL")}}</label>
+ <input type="text" id="input-email" class="form-control" name="email" value="{{current_user.email}}" autocomplete="off" placeholder="{{translate("EMAIL.YOUR")}}" {{page.visibility}}>
+ </div>
+ {% if page.visibility != "disabled" %}
+ <div class="form-group">
+ <label for="input-password" class="control-label">{{translate("PASSWORD.NEW")}}</label>
+ <input type="password" id="input-password" class="form-control" name="password" placeholder="{{translate("PASSWORD.BETWEEN", {min: 12, max: 100})}} ({{translate("OPTIONAL")}})">
+ </div>
+ <div class="form-group">
+ <label for="input-passwordc" class="control-label">{{translate("PASSWORD.CONFIRM_NEW")}}</label>
+ <input type="password" id="input-passwordc" class="form-control" name="passwordc" placeholder="{{translate("PASSWORD.CONFIRM_NEW_HELP")}}">
+ </div>
+ <hr>
+ <div class="form-group">
+ <label for="input-passwordcheck" class="control-label">{{translate("PASSWORD.CURRENT")}}</label>
+ <input type="password" id="input-passwordcheck" class="form-control" name="passwordcheck" placeholder="{{translate("PASSWORD.CURRENT_EXPLAIN")}}">
+ </div>
+ {% endif %}
+ {% endblock %}
+ </div>
+ <div class="box-footer text-center">
+ <button type="reset" class="btn btn-default">{{translate('RESET')}}</button>
+ <button type="submit" class="btn btn-primary js-submit">{{translate('SAVE')}}</button>
+ </div>
+</form> \ No newline at end of file
diff --git a/main/app/sprinkles/account/templates/forms/settings-profile.html.twig b/main/app/sprinkles/account/templates/forms/settings-profile.html.twig
new file mode 100755
index 0000000..0b0a788
--- /dev/null
+++ b/main/app/sprinkles/account/templates/forms/settings-profile.html.twig
@@ -0,0 +1,40 @@
+<form id="profile-settings" role="form" action="{{site.uri.public}}/account/settings/profile" method="post">
+ <div class="box-header">
+ <h3 class="box-title"><i class="fa fa-user fa-fw"></i> {{translate("PROFILE.SETTINGS")}}</h3>
+ </div>
+ <div class="box-body">
+ {% include "forms/csrf.html.twig" %}
+
+ {% block settings_profile %}
+ <label for="input-first-name" class="control-label">{{translate("NAME")}}</label>
+ <div class="row">
+ <div class="col-sm-6">
+ <div class="form-group">
+ <input type="text" id="input-first-name" class="form-control" name="first_name" value="{{current_user.first_name}}" placeholder="{{translate("FIRST_NAME")}}" {{page.visibility}}>
+ </div>
+ </div>
+ <div class="col-sm-6">
+ <div class="form-group">
+ <input type="text" id="input-last-name" class="form-control" name="last_name" value="{{current_user.last_name}}" placeholder="{{translate("LAST_NAME")}}" {{page.visibility}}>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="input-locale" class="control-label">{{translate("LOCALE")}}</label>
+ <select id="input-locale" class="form-control js-select2" name="locale" {{page.visibility}}>
+ {% for option, label in locales %}
+ {% if label is not empty %}
+ <option value="{{option}}" {% if (option == current_user.locale) %}selected{% endif %}>{{label}}</option>
+ {% endif %}
+ {% endfor %}
+ </select>
+ <p class="help-block">{{translate("LOCALE.ACCOUNT")}}.</p>
+ </div>
+ {% endblock %}
+ </div>
+ <div class="box-footer text-center">
+ <button type="reset" class="btn btn-default">{{translate('RESET')}}</button>
+ <button type="submit" class="btn btn-primary js-submit">{{translate('SAVE')}}</button>
+ </div>
+</form>
diff --git a/main/app/sprinkles/account/templates/mail/password-reset.html.twig b/main/app/sprinkles/account/templates/mail/password-reset.html.twig
new file mode 100755
index 0000000..37096ce
--- /dev/null
+++ b/main/app/sprinkles/account/templates/mail/password-reset.html.twig
@@ -0,0 +1,22 @@
+{% block subject %}
+ {{site.title}} - your password reset request
+{% endblock %}
+
+{% block body %}
+<p>Dear {{user.first_name}},
+</p>
+<p>
+A lost password request has been submitted for your account with {{site.title}} ({{site.uri.public}}) on {{request_date | date('m/d/Y g:i A')}}.
+</p>
+<p>
+If you or someone you trust sent this request, and you wish to set a new password, please click this link: <a href="{{site.uri.public}}/account/set-password/confirm?token={{token}}">{{site.uri.public}}/account/set-password/confirm?token={{token}}</a>
+</p>
+
+<p>
+If you did <b>not</b> expect this email, you may click this link to cancel the request: <a href="{{site.uri.public}}/account/set-password/deny?token={{token}}">{{site.uri.public}}/account/set-password/deny?token={{token}}</a>, or simply do nothing and the request will expire on its own.
+</p>
+<p>
+With regards,<br>
+The {{site.title}} Team
+</p>
+{% endblock %} \ No newline at end of file
diff --git a/main/app/sprinkles/account/templates/mail/resend-verification.html.twig b/main/app/sprinkles/account/templates/mail/resend-verification.html.twig
new file mode 100755
index 0000000..ba1c243
--- /dev/null
+++ b/main/app/sprinkles/account/templates/mail/resend-verification.html.twig
@@ -0,0 +1,17 @@
+{% block subject %}
+ {{site.title}} - verify your account
+{% endblock %}
+
+{% block body %}
+<p>Dear {{user.first_name}},
+</p>
+<p>
+We have received a new verification request for your account with {{site.title}} ({{site.uri.public}}). Please follow the link below to verify your account. If your account is already active, please disregard this message.
+</p>
+<a href="{{site.uri.public}}/account/verify?token={{token}}">{{site.uri.public}}/account/verify?token={{token}}</a>
+</p>
+<p>
+With regards,<br>
+The {{site.title}} Team
+</p>
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/mail/verify-account.html.twig b/main/app/sprinkles/account/templates/mail/verify-account.html.twig
new file mode 100755
index 0000000..aa342c7
--- /dev/null
+++ b/main/app/sprinkles/account/templates/mail/verify-account.html.twig
@@ -0,0 +1,21 @@
+{% block subject %}
+ Welcome to {{site.title}} - please verify your account
+{% endblock %}
+
+{% block body %}
+<p>Dear {{user.first_name}},
+</p>
+<p>
+You are receiving this email because you registered with {{site.title}} ({{site.uri.public}}).
+</p>
+<p>
+You will need to verify your account before you can login. Please follow the link below to verify your account.
+</p>
+<p>
+<a href="{{site.uri.public}}/account/verify?token={{token}}">{{site.uri.public}}/account/verify?token={{token}}</a>
+</p>
+<p>
+With regards,<br>
+The {{site.title}} Team
+</p>
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/modals/tos.html.twig b/main/app/sprinkles/account/templates/modals/tos.html.twig
new file mode 100755
index 0000000..d51d897
--- /dev/null
+++ b/main/app/sprinkles/account/templates/modals/tos.html.twig
@@ -0,0 +1,16 @@
+{% extends 'modals/modal.html.twig' %}
+
+{% block modal_title %}
+ {{translate("TOS_FOR", {title: site.title})}}
+{% endblock %}
+
+{% block modal_body %}
+ <div class="text-left">
+ {% include 'pages/partials/legal.html.twig' %}
+ {% include 'pages/partials/privacy.html.twig' %}
+ </div>
+{% endblock %}
+
+{% block modal_footer %}
+ <button type="button" data-dismiss="modal" class="btn btn-primary btn-block">Got it!</button>
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/navigation/main-nav.html.twig b/main/app/sprinkles/account/templates/navigation/main-nav.html.twig
new file mode 100755
index 0000000..e44c9c8
--- /dev/null
+++ b/main/app/sprinkles/account/templates/navigation/main-nav.html.twig
@@ -0,0 +1,13 @@
+{# This extend the same file from core to add a sign-up/sign-in or "my account" link to the "home page" nav menu. #}
+{% extends "@core/navigation/main-nav.html.twig" %}
+
+{% block secondary_nav %}
+ {{parent()}}
+ {% if not checkAuthenticated() %}
+ <li>
+ <a href="{{site.uri.public}}/account/sign-in" class="nav-highlight">{{translate("SIGNIN")}}</a>
+ </li>
+ {% else %}
+ {% include "navigation/user-card.html.twig" %}
+ {% endif %}
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/navigation/user-card.html.twig b/main/app/sprinkles/account/templates/navigation/user-card.html.twig
new file mode 100755
index 0000000..47e18f1
--- /dev/null
+++ b/main/app/sprinkles/account/templates/navigation/user-card.html.twig
@@ -0,0 +1,33 @@
+{% block userCard %}
+<li class="dropdown user user-menu">
+ {% block userCard_nav %}
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ <img src="{{ current_user.avatar }}" class="user-image" alt="User Image">
+ <span class="hidden-xs">{{current_user.first_name}} {{current_user.last_name}}</span>
+ <i class="fa fa-chevron-down"></i>
+ </a>
+ {% endblock %}
+ <ul class="dropdown-menu">
+ {% block userCard_userInfo %}
+ <!-- User image -->
+ <li class="user-header">
+ <img src="{{ current_user.avatar }}" class="img-circle" alt="User Image">
+ <p>
+ {{current_user.first_name}} {{current_user.last_name}}
+ <small>({{current_user.user_name}})</small>
+ </p>
+ </li>
+ {% endblock %}
+
+ <!-- Menu Footer-->
+ <li class="user-footer">
+ {% block userCard_menu %}
+ {% if checkAccess('uri_account_settings') %}
+ <a href="{{site.uri.public}}/account/settings" class="btn btn-default btn-flat btn-block">{{translate("ACCOUNT.MY")}}</a>
+ {% endif %}
+ <a href="{{site.uri.public}}/account/logout" class="btn btn-default btn-flat btn-block">{{translate("LOGOUT")}}</a>
+ {% endblock %}
+ </li>
+ </ul>
+</li>
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/account-settings.html.twig b/main/app/sprinkles/account/templates/pages/account-settings.html.twig
new file mode 100755
index 0000000..61cd3d0
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/account-settings.html.twig
@@ -0,0 +1,45 @@
+{% extends forcedLayout ? forcedLayout : "pages/abstract/default.html.twig" %}
+
+{% set page_active = "account-settings" %}
+
+{% block stylesheets_page %}
+ <!-- Page-specific CSS asset bundle -->
+ {{ assets.css('css/form-widgets') | raw }}
+{% endblock %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate("ACCOUNT.SETTINGS")}}{% endblock %}
+
+{% block page_description %}{{translate("ACCOUNT.SETTINGS.DESCRIPTION")}}{% endblock %}
+
+{% block body_matter %}
+
+ <div class="row">
+ <div class="col-lg-6">
+ {% block settings_profile_box %}
+ <div class="box box-primary">
+ {% include "forms/settings-profile.html.twig" %}
+ </div>
+ {% endblock %}
+ </div>
+ <div class="col-lg-6">
+ {% block settings_account_box %}
+ <div class="box box-primary">
+ {% include "forms/settings-account.html.twig" %}
+ </div>
+ {% endblock %}
+ </div>
+ </div>
+{% endblock %}
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <!-- Include form widgets JS -->
+ {{ assets.js('js/form-widgets') | raw }}
+
+ <!-- Include page-specific JS -->
+ {{ assets.js('js/pages/account-settings') | raw }}
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/error/compromised.html.twig b/main/app/sprinkles/account/templates/pages/error/compromised.html.twig
new file mode 100755
index 0000000..6048619
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/error/compromised.html.twig
@@ -0,0 +1,11 @@
+{% extends "pages/abstract/error.html.twig" %}
+
+{% block page_title %}{{ translate('ACCOUNT.SESSION_COMPROMISED.TITLE') }}{% endblock %}
+
+{% block page_description %}{{ translate('ACCOUNT.SESSION_COMPROMISED.TITLE') }}{% endblock %}
+
+{% block heading %}
+ <i class="fa fa-warning text-yellow"></i> {{ translate('ACCOUNT.SESSION_COMPROMISED.TEXT', {
+ 'url' : site.uri.public ~ '/account/sign-in'
+ }) | raw }}
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/forgot-password.html.twig b/main/app/sprinkles/account/templates/pages/forgot-password.html.twig
new file mode 100755
index 0000000..72b1a2a
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/forgot-password.html.twig
@@ -0,0 +1,46 @@
+{% extends "pages/abstract/base.html.twig" %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate("PASSWORD.FORGOTTEN")}}{% endblock %}
+
+{% block page_description %}{{translate("PASSWORD.FORGET.PAGE")}}{% endblock %}
+
+{% block body_attributes %}
+ class="hold-transition login-page"
+{% endblock %}
+
+{% block content %}
+<div class="login-box">
+ <div class="login-logo">
+ <a href="{{site.uri.public}}">{{site.title}}</a>
+ </div>
+ <!-- /.login-logo -->
+
+ <div class="login-box-body login-form">
+ <p class="login-box-msg"><strong>{{translate("PASSWORD.FORGOTTEN")}}</strong></p>
+ <p class="login-box-msg">{{translate("PASSWORD.FORGET.EMAIL")}}</p>
+
+ <div class="form-alerts" id="alerts-page"></div>
+
+ <form id="request-password-reset" role="form" action="{{site.uri.public}}/account/forgot-password" method="post" class="r-form">
+ {% include "forms/csrf.html.twig" %}
+ <div class="form-group">
+ <label class="sr-only" for="reset-form-email">{{translate("EMAIL")}}</label>
+ <input type="text" name="email" placeholder="{{translate("EMAIL")}}" class="form-control" id="reset-form-email">
+ </div>
+ <button type="submit" class="btn btn-block btn-primary">{{translate("PASSWORD.FORGET.EMAIL_SEND")}}</button>
+ </form>
+ </div>
+ <!-- /.login-box-body -->
+{% endblock %}
+
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <!-- Include page-specific JS bundle -->
+ {{ assets.js('js/pages/forgot-password') | raw }}
+
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/register.html.twig b/main/app/sprinkles/account/templates/pages/register.html.twig
new file mode 100755
index 0000000..bd155ba
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/register.html.twig
@@ -0,0 +1,105 @@
+{% extends "pages/abstract/base.html.twig" %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate('REGISTER')}}{% endblock %}
+
+{% block page_description %}{{translate('PAGE.LOGIN.DESCRIPTION', {'site_name': site.title })}}{% endblock %}
+
+{% block body_attributes %}
+ class="hold-transition login-page"
+{% endblock %}
+
+{% block content %}
+<div class="login-box">
+ <div class="login-logo">
+ <a href="{{site.uri.public}}">{{site.title}}</a>
+ </div>
+ <!-- /.login-logo -->
+
+ <div class="login-box-body register-form">
+ <p class="login-box-msg"><strong>{{translate('REGISTER')}}</strong></p>
+ <div class="form-alerts" id="alerts-page"></div>
+
+ <form id="register" role="form" action="{{site.uri.public}}/account/register" method="post" class="r-form">
+ {% include "forms/csrf.html.twig" %}
+ <label for="r-form-first-name">{{translate('NAME_AND_EMAIL')}}</label>
+ <div class="row">
+ <div class="col-md-6">
+ <div class="form-group">
+ <label class="sr-only" for="r-form-first-name">{{translate('FIRST_NAME')}}</label>
+ <input type="text" name="first_name" placeholder="{{translate('FIRST_NAME')}}" class="form-control" id="r-form-first-name" autocomplete="off">
+ </div>
+ </div>
+ <div class="col-md-6">
+ <div class="form-group">
+ <label class="sr-only" for="r-form-last-name">{{translate('LAST_NAME')}}</label>
+ <input type="text" name="last_name" placeholder="{{translate('LAST_NAME')}}" class="form-control" id="r-form-last-name" autocomplete="off">
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <input type="text" name="email" placeholder="{% if site.registration.require_email_verification %}{{translate('EMAIL.VERIFICATION_REQUIRED')}}{% else %}{{translate('EMAIL.YOUR')}}{% endif %}" class="form-control" id="r-form-email">
+ </div>
+ <div class="form-group">
+ <label for="r-form-username">{{translate('USERNAME')}}</label>
+ <span class="pull-right"><a href="#" id="form-register-username-suggest">[{{translate('SUGGEST')}}]</a></span>
+ <input type="text" name="user_name" placeholder="{{translate('USERNAME.CHOOSE')}}" class="form-control" id="r-form-username" autocomplete="off">
+ </div>
+ <div class="form-group">
+ <label for="r-form-password">{{translate('PASSWORD')}}</label>
+ <input type="password" name="password" placeholder="{{translate('PASSWORD.BETWEEN', {min: 12, max: 100})}}" class="form-control" id="r-form-password">
+ </div>
+ <div class="form-group">
+ <label class="sr-only" for="r-form-passwordc">{{translate('PASSWORD.CONFIRM')}}</label>
+ <input type="password" name="passwordc" placeholder="{{translate('PASSWORD.CONFIRM')}}" class="form-control" id="r-form-passwordc">
+ </div>
+ {% if site.registration.captcha %}
+ <div class="form-group">
+ <label class="sr-only" for="r-form-passwordc">{{translate('CAPTCHA.VERIFY')}}</label>
+ <div class="row">
+ <div class="col-md-6">
+ <input type="text" name="captcha" placeholder="{{translate('CAPTCHA.SPECIFY')}}" class="form-control" id="r-form-captcha">
+ </div>
+ <div class="col-md-6 form-col-captcha">
+ <img src="{{site.uri.public}}/account/captcha" id="captcha" data-target="#r-form-captcha">
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ <div class="collapse">
+ <label>Spiderbro: Don't change me bro, I'm tryin'a catch some flies!</label>
+ <input name="spiderbro" id="spiderbro" value="http://"/>
+ </div>
+ <div class="text-left">
+ <p>
+ {{translate('TOS_AGREEMENT', {
+ 'site_title' : site.title,
+ 'link_attributes' : 'class="js-show-tos" href="#" data-toggle="modal"'
+ }) | raw}}
+ </p>
+ </div>
+ <div>
+ <button type="submit" class="btn btn-block btn-primary">{{translate('REGISTER_ME')}}</button>
+ </div>
+ <div style="padding-top: 10px;">
+ {{translate('SIGN_IN_HERE', {
+ 'url' : site.uri.public ~'/account/sign-in'
+ }) | raw}}
+ </div>
+ </form>
+ </div>
+ <!-- /.login-box-body -->
+
+</div>
+<!-- /.login-box -->
+{% endblock %}
+
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <!-- Include page-specific JS -->
+ {{ assets.js('js/pages/register') | raw }}
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/resend-verification.html.twig b/main/app/sprinkles/account/templates/pages/resend-verification.html.twig
new file mode 100755
index 0000000..627dce0
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/resend-verification.html.twig
@@ -0,0 +1,46 @@
+{% extends "pages/abstract/base.html.twig" %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate("ACCOUNT.VERIFICATION.RESEND")}}{% endblock %}
+
+{% block page_description %}{{translate("ACCOUNT.VERIFICATION.PAGE")}}{% endblock %}
+
+{% block body_attributes %}
+ class="hold-transition login-page"
+{% endblock %}
+
+{% block content %}
+<div class="login-box">
+ <div class="login-logo">
+ <a href="{{site.uri.public}}">{{site.title}}</a>
+ </div>
+ <!-- /.login-logo -->
+
+ <div class="login-box-body login-form">
+ <p class="login-box-msg"><strong>{{translate("ACCOUNT.VERIFICATION.RESEND")}}</strong></p>
+ <p class="login-box-msg">{{translate("ACCOUNT.VERIFICATION.EMAIL")}}</p>
+
+ <div class="form-alerts" id="alerts-page"></div>
+
+ <form id="request-verification-email" role="form" action="{{site.uri.public}}/account/resend-verification" method="post" class="r-form">
+ {% include "forms/csrf.html.twig" %}
+ <div class="form-group">
+ <label class="sr-only" for="verification-form-email">{{translate("EMAIL")}}</label>
+ <input type="text" name="email" placeholder="{{translate("EMAIL")}}" class="form-control" id="verification-form-email">
+ </div>
+ <button type="submit" class="btn btn-block btn-primary">{{translate("ACCOUNT.VERIFICATION.SEND")}}</button>
+ </form>
+ </div>
+ <!-- /.login-box-body -->
+{% endblock %}
+
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <!-- Include page-specific JS -->
+ {{ assets.js('js/pages/resend-verification') | raw }}
+
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/reset-password.html.twig b/main/app/sprinkles/account/templates/pages/reset-password.html.twig
new file mode 100755
index 0000000..8e3a24a
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/reset-password.html.twig
@@ -0,0 +1,56 @@
+{% extends "pages/abstract/base.html.twig" %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate("PASSWORD.RESET")}}{% endblock %}
+
+{% block page_description %}{{translate("PASSWORD.RESET.PAGE")}}{% endblock %}
+
+{% block body_attributes %}
+ class="hold-transition login-page"
+{% endblock %}
+
+{% block content %}
+<div class="login-box">
+ <div class="login-logo">
+ <a href="{{site.uri.public}}">{{site.title}}</a>
+ </div>
+ <!-- /.login-logo -->
+
+ <div class="login-box-body login-form">
+ <p class="login-box-msg"><strong>{{translate("PASSWORD.RESET")}}</strong></p>
+ <p class="login-box-msg">{{translate("PASSWORD.RESET.CHOOSE")}}</p>
+
+ <div class="form-alerts" id="alerts-page"></div>
+
+ <form id="set-or-reset-password" role="form" action="{{site.uri.public}}/account/set-password" method="post" class="r-form">
+ {% include "forms/csrf.html.twig" %}
+ {# Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 #}
+ <input type="text" style="display:none">
+ <input type="password" style="display:none">
+
+ <div class="form-group">
+ <label class="sr-only" for="form-password">{{translate("PASSWORD.NEW")}}</label>
+ <input type="password" name="password" placeholder="{{translate("PASSWORD.BETWEEN", {min: 12, max: 100})}}" class="form-control" id="form-password">
+ </div>
+
+ <div class="form-group">
+ <label class="sr-only" for="form-passwordc">{{translate("PASSWORD.CONFIRM_NEW")}}</label>
+ <input type="password" name="passwordc" placeholder="{{translate("PASSWORD.CONFIRM_NEW_EXPLAIN")}}" class="form-control" id="form-passwordc">
+ </div>
+ <input type="hidden" name="token" value="{{token}}">
+ <button type="submit" class="btn btn-block btn-primary">{{translate("PASSWORD.RESET.SEND")}}</button>
+ </form>
+ </div>
+ <!-- /.login-box-body -->
+{% endblock %}
+
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <!-- Include page-specific JS bundle -->
+ {{ assets.js('js/pages/set-or-reset-password') | raw }}
+
+{% endblock %}
diff --git a/main/app/sprinkles/account/templates/pages/set-password.html.twig b/main/app/sprinkles/account/templates/pages/set-password.html.twig
new file mode 100755
index 0000000..3c4fe2b
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/set-password.html.twig
@@ -0,0 +1,55 @@
+{% extends "pages/abstract/base.html.twig" %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate("PASSWORD.CREATE")}}{% endblock %}
+
+{% block page_description %}{{translate("PASSWORD.CREATE.PAGE")}}{% endblock %}
+
+{% block body_attributes %}
+ class="hold-transition login-page"
+{% endblock %}
+
+{% block content %}
+<div class="login-box">
+ <div class="login-logo">
+ <a href="{{site.uri.public}}">{{site.title}}</a>
+ </div>
+ <!-- /.login-logo -->
+
+ <div class="login-box-body login-form">
+ <p class="login-box-msg"><strong>{{translate("PASSWORD.CREATE")}}</strong></p>
+ <p class="login-box-msg">{{translate("WELCOME_TO", {'title': site.title})}} {{translate("PASSWORD.CREATE.PAGE")}}</p>
+
+ <div class="form-alerts" id="alerts-page"></div>
+
+ <form id="set-or-reset-password" role="form" action="{{site.uri.public}}/account/set-password" method="post" class="r-form">
+ {% include "forms/csrf.html.twig" %}
+ {# Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 #}
+ <input type="text" style="display:none">
+ <input type="password" style="display:none">
+
+ <div class="form-group">
+ <label class="sr-only" for="form-password">{{translate('PASSWORD')}}</label>
+ <input type="password" name="password" placeholder="{{translate('PASSWORD.BETWEEN', {min: 12, max: 100})}}" class="form-control" id="form-password">
+ </div>
+ <div class="form-group">
+ <label class="sr-only" for="form-passwordc">{{translate('PASSWORD.CONFIRM')}}</label>
+ <input type="password" name="passwordc" placeholder="{{translate('PASSWORD.CONFIRM')}}" class="form-control" id="form-passwordc">
+ </div>
+ <input type="hidden" name="token" value="{{token}}">
+ <button type="submit" class="btn btn-block btn-primary">{{translate('PASSWORD.CREATE.SET')}}</button>
+ </form>
+ </div>
+ <!-- /.login-box-body -->
+{% endblock %}
+
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <!-- Include page-specific JS bundle -->
+ {{ assets.js('js/pages/set-or-reset-password') | raw }}
+
+{% endblock %} \ No newline at end of file
diff --git a/main/app/sprinkles/account/templates/pages/sign-in.html.twig b/main/app/sprinkles/account/templates/pages/sign-in.html.twig
new file mode 100755
index 0000000..2fb6e1c
--- /dev/null
+++ b/main/app/sprinkles/account/templates/pages/sign-in.html.twig
@@ -0,0 +1,84 @@
+{% extends "pages/abstract/base.html.twig" %}
+
+{# Overrides blocks in head of base template #}
+{% block page_title %}{{translate('SIGNIN')}}{% endblock %}
+
+{% block page_description %}{{translate('PAGE.LOGIN.DESCRIPTION', {'site_name': site.title })}}{% endblock %}
+
+{% block body_attributes %}
+ class="hold-transition login-page"
+{% endblock %}
+
+{% block content %}
+<div class="login-box">
+ <div class="login-logo">
+ <a href="{{site.uri.public}}">{{site.title}}</a>
+ </div>
+ <!-- /.login-logo -->
+
+ <div class="login-box-body login-form">
+ <p class="login-box-msg"><strong>{{translate('SIGNIN')}}</strong></p>
+
+ <div class="form-alerts" id="alerts-page"></div>
+
+ <form action="{{site.uri.public}}/account/login" id="sign-in" method="post">
+ {% include "forms/csrf.html.twig" %}
+ <div class="form-group has-feedback">
+ <input type="text" class="form-control" placeholder="{% if site.login.enable_email %}{{translate('EMAIL_OR_USERNAME')}}{% else %}{{translate('USERNAME')}}{% endif %}" name="user_name">
+ <i class="glyphicon glyphicon-user form-control-icon" aria-hidden="true"></i>
+ </div>
+ <div class="form-group has-feedback">
+ <input type="password" class="form-control" placeholder="{{translate('PASSWORD')}}" name="password">
+ <i class="glyphicon glyphicon-lock form-control-icon" aria-hidden="true"></i>
+ </div>
+ <div class="row">
+ <div class="col-xs-8">
+ <div class="checkbox icheck">
+ <label>
+ <input type="checkbox" class="js-icheck" name="rememberme"> {{translate('REMEMBER_ME')}}
+ </label>
+ </div>
+ </div>
+ <!-- /.col -->
+ <div class="col-xs-4">
+ <button type="submit" class="btn btn-primary btn-block btn-flat">{{translate('LOGIN')}}</button>
+ </div>
+ <!-- /.col -->
+ </div>
+ </form>
+
+ <a href="{{site.uri.public}}/account/forgot-password">{{translate('PASSWORD.FORGET')}}</a><br>
+ {% if site.registration.require_email_verification %}
+ <a href="{{site.uri.public}}/account/resend-verification">{{translate('ACCOUNT.VERIFICATION.RESEND')}}</a><br>
+ {% endif %}
+ {% if site.registration.enabled %}
+ <a href="{{site.uri.public}}/account/register">{{translate('REGISTER')}}</a>
+ {% endif %}
+
+ </div>
+ <!-- /.login-box-body -->
+</div>
+<!-- /.login-box -->
+{% endblock %}
+
+{% block scripts_page %}
+ <!-- Include validation rules -->
+ <script>
+ {% include "pages/partials/page.js.twig" %}
+ </script>
+
+ <script>
+ site = $.extend(
+ true, // deep extend
+ {
+ "registration" : {
+ "enabled" : "{{site.registration.enabled}}"
+ }
+ },
+ site
+ );
+ </script>
+
+ <!-- Include page-specific JS -->
+ {{ assets.js('js/pages/sign-in') | raw }}
+{% endblock %}
diff --git a/main/app/sprinkles/account/tests/Unit/FactoriesTest.php b/main/app/sprinkles/account/tests/Unit/FactoriesTest.php
new file mode 100755
index 0000000..ee2bf23
--- /dev/null
+++ b/main/app/sprinkles/account/tests/Unit/FactoriesTest.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Tests\Unit;
+
+use UserFrosting\Tests\TestCase;
+use UserFrosting\Tests\DatabaseTransactions;
+
+/**
+ * FactoriesTest class.
+ * Tests the factories defined in this sprinkle are working
+ *
+ * @extends TestCase
+ */
+class FactoriesTest extends TestCase
+{
+ use DatabaseTransactions;
+
+ function testUserFactory()
+ {
+ $fm = $this->ci->factory;
+
+ $user = $fm->create('UserFrosting\Sprinkle\Account\Database\Models\User');
+ $this->assertInstanceOf('UserFrosting\Sprinkle\Account\Database\Models\User', $user);
+ }
+}
diff --git a/main/app/sprinkles/account/tests/Unit/HasherTest.php b/main/app/sprinkles/account/tests/Unit/HasherTest.php
new file mode 100755
index 0000000..711e3cb
--- /dev/null
+++ b/main/app/sprinkles/account/tests/Unit/HasherTest.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Tests\Unit;
+
+use UserFrosting\Sprinkle\Account\Authenticate\Hasher;
+use UserFrosting\Tests\TestCase;
+
+/**
+ * Tests the password Hasher class.
+ *
+ * @extends TestCase
+ */
+class HasherTest extends TestCase
+{
+ protected $plainText = 'hodleth';
+
+ /**
+ * @var string Legacy hash from UserCake (sha1)
+ */
+ protected $userCakeHash = '87e995bde9ebdc73fc58cc75a9fadc4ae630d8207650fbe94e148ccc8058d5de5';
+
+ /**
+ * @var string Legacy hash from UF 0.1.x
+ */
+ protected $legacyHash = '$2y$12$rsXGznS5Ky23lX9iNzApAuDccKRhQFkiy5QfKWp0ACyDWBPOylPB.rsXGznS5Ky23lX9iNzApA9';
+
+ /**
+ * @var string Modern hash that uses password_hash()
+ */
+ protected $modernHash = '$2y$10$ucxLwloFso6wJoct1baBQefdrttws/taEYvavi6qoPsw/vd1u4Mha';
+
+ public function testGetHashType()
+ {
+ $hasher = new Hasher;
+
+ $type = $hasher->getHashType($this->modernHash);
+
+ $this->assertEquals('modern', $type);
+
+ $type = $hasher->getHashType($this->legacyHash);
+
+ $this->assertEquals('legacy', $type);
+
+ $type = $hasher->getHashType($this->userCakeHash);
+
+ $this->assertEquals('sha1', $type);
+ }
+
+ public function testVerify()
+ {
+ $hasher = new Hasher;
+
+ $this->assertTrue($hasher->verify($this->plainText, $this->modernHash));
+ $this->assertTrue($hasher->verify($this->plainText, $this->legacyHash));
+ $this->assertTrue($hasher->verify($this->plainText, $this->userCakeHash));
+ }
+
+ public function testVerifyReject()
+ {
+ $hasher = new Hasher;
+
+ $this->assertFalse($hasher->verify('selleth', $this->modernHash));
+ $this->assertFalse($hasher->verify('selleth', $this->legacyHash));
+ $this->assertFalse($hasher->verify('selleth', $this->userCakeHash));
+ }
+}