diff options
author | marvin-borner@live.com | 2018-04-16 21:09:05 +0200 |
---|---|---|
committer | marvin-borner@live.com | 2018-04-16 21:09:05 +0200 |
commit | cf14306c2b3f82a81f8d56669a71633b4d4b5fce (patch) | |
tree | 86700651aa180026e89a66064b0364b1e4346f3f /main/app/sprinkles/account | |
parent | 619b01b3615458c4ed78bfaeabb6b1a47cc8ad8b (diff) |
Main merge to user management system - files are now at /main/public/
Diffstat (limited to 'main/app/sprinkles/account')
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)); + } +} |