From 937100e9bb2a2f5ab035e283e01e6d96e569ee51 Mon Sep 17 00:00:00 2001
From: marvin-borner@live.com
Date: Sat, 14 Apr 2018 21:01:44 +0200
Subject: Added login things
---
.gitignore | 1 -
login | 1 -
login/.github/CONTRIBUTING.md | 76 +
login/.github/ISSUE_TEMPLATE.md | 2 +
login/.travis.yml | 36 +
login/CHANGELOG.md | 553 ++
login/LICENSE.md | 7 +
login/README.md | 124 +
login/STYLE-GUIDE.md | 44 +
login/app/.env.example | 13 +
login/app/.htaccess | 15 +
login/app/cache/.gitkeep | 0
login/app/defines.php | 52 +
login/app/logs/.gitkeep | 0
login/app/sessions/.gitkeep | 0
login/app/sprinkles.example.json | 10 +
login/app/sprinkles/account/asset-bundles.json | 79 +
.../userfrosting/js/pages/account-settings.js | 29 +
.../userfrosting/js/pages/forgot-password.js | 19 +
.../assets/userfrosting/js/pages/register.js | 94 +
.../userfrosting/js/pages/resend-verification.js | 19 +
.../userfrosting/js/pages/set-or-reset-password.js | 19 +
.../assets/userfrosting/js/pages/sign-in.js | 39 +
login/app/sprinkles/account/bower.json | 28 +
login/app/sprinkles/account/composer.json | 24 +
login/app/sprinkles/account/config/default.php | 79 +
login/app/sprinkles/account/config/production.php | 67 +
.../sprinkles/account/factories/Permissions.php | 19 +
login/app/sprinkles/account/factories/Roles.php | 18 +
login/app/sprinkles/account/factories/Users.php | 23 +
login/app/sprinkles/account/locale/ar/messages.php | 176 +
login/app/sprinkles/account/locale/ar/validate.php | 18 +
.../sprinkles/account/locale/de_DE/messages.php | 188 +
.../sprinkles/account/locale/de_DE/validate.php | 21 +
.../sprinkles/account/locale/en_US/messages.php | 183 +
.../sprinkles/account/locale/en_US/validate.php | 19 +
.../sprinkles/account/locale/es_ES/messages.php | 189 +
.../sprinkles/account/locale/es_ES/validate.php | 19 +
login/app/sprinkles/account/locale/fa/messages.php | 178 +
login/app/sprinkles/account/locale/fa/validate.php | 20 +
.../sprinkles/account/locale/fr_FR/messages.php | 179 +
.../sprinkles/account/locale/fr_FR/validate.php | 18 +
.../sprinkles/account/locale/it_IT/messages.php | 186 +
.../sprinkles/account/locale/it_IT/validate.php | 21 +
.../sprinkles/account/locale/pt_PT/messages.php | 166 +
.../sprinkles/account/locale/pt_PT/validate.php | 18 +
.../sprinkles/account/locale/ru_RU/messages.php | 183 +
.../sprinkles/account/locale/ru_RU/validate.php | 19 +
.../sprinkles/account/locale/th_TH/messages.php | 164 +
.../sprinkles/account/locale/th_TH/validate.php | 18 +
login/app/sprinkles/account/locale/tr/messages.php | 183 +
login/app/sprinkles/account/locale/tr/validate.php | 19 +
.../sprinkles/account/locale/zh_CN/messages.php | 177 +
.../sprinkles/account/locale/zh_CN/validate.php | 19 +
login/app/sprinkles/account/routes/routes.php | 59 +
.../account/schema/requests/account-settings.yaml | 35 +
.../account/schema/requests/account-verify.yaml | 6 +
.../account/schema/requests/check-username.yaml | 17 +
.../account/schema/requests/deny-password.yaml | 5 +
.../account/schema/requests/forgot-password.yaml | 6 +
.../sprinkles/account/schema/requests/login.yaml | 19 +
.../account/schema/requests/profile-settings.yaml | 24 +
.../account/schema/requests/register.yaml | 75 +
.../schema/requests/resend-verification.yaml | 6 +
.../account/schema/requests/set-password.yaml | 29 +
login/app/sprinkles/account/src/Account.php | 20 +
.../account/src/Authenticate/AuthGuard.php | 56 +
.../account/src/Authenticate/Authenticator.php | 419 ++
.../Exception/AccountDisabledException.php | 21 +
.../Exception/AccountInvalidException.php | 21 +
.../Exception/AccountNotVerifiedException.php | 21 +
.../Exception/AuthCompromisedException.php | 20 +
.../Exception/AuthExpiredException.php | 21 +
.../Exception/InvalidCredentialsException.php | 21 +
.../sprinkles/account/src/Authenticate/Hasher.php | 108 +
.../src/Authorize/AccessConditionExpression.php | 139 +
.../src/Authorize/AuthorizationException.php | 23 +
.../account/src/Authorize/AuthorizationManager.php | 157 +
.../src/Authorize/ParserNodeFunctionEvaluator.php | 193 +
.../account/src/Bakery/CreateAdminUser.php | 334 ++
.../account/src/Controller/AccountController.php | 1293 +++++
.../Exception/SpammyRequestException.php | 20 +
.../Database/Migrations/v400/ActivitiesTable.php | 54 +
.../src/Database/Migrations/v400/GroupsTable.php | 82 +
.../Migrations/v400/PasswordResetsTable.php | 57 +
.../Migrations/v400/PermissionRolesTable.php | 55 +
.../Database/Migrations/v400/PermissionsTable.php | 262 +
.../Database/Migrations/v400/PersistencesTable.php | 57 +
.../Database/Migrations/v400/RoleUsersTable.php | 55 +
.../src/Database/Migrations/v400/RolesTable.php | 78 +
.../src/Database/Migrations/v400/UsersTable.php | 69 +
.../Migrations/v400/VerificationsTable.php | 57 +
.../account/src/Database/Models/Activity.php | 86 +
.../account/src/Database/Models/Group.php | 69 +
.../account/src/Database/Models/PasswordReset.php | 76 +
.../account/src/Database/Models/Permission.php | 121 +
.../sprinkles/account/src/Database/Models/Role.php | 105 +
.../sprinkles/account/src/Database/Models/User.php | 493 ++
.../account/src/Database/Models/Verification.php | 70 +
.../Handler/AuthCompromisedExceptionHandler.php | 34 +
.../Error/Handler/AuthExpiredExceptionHandler.php | 50 +
.../Error/Handler/ForbiddenExceptionHandler.php | 31 +
.../app/sprinkles/account/src/Facades/Password.php | 28 +
.../src/Log/UserActivityDatabaseHandler.php | 33 +
.../account/src/Log/UserActivityProcessor.php | 45 +
.../src/Repository/PasswordResetRepository.php | 34 +
.../account/src/Repository/TokenRepository.php | 230 +
.../src/Repository/VerificationRepository.php | 32 +
.../src/ServicesProvider/ServicesProvider.php | 444 ++
.../account/src/Twig/AccountExtension.php | 65 +
.../account/src/Util/HashFailedException.php | 21 +
login/app/sprinkles/account/src/Util/Util.php | 39 +
.../templates/forms/settings-account.html.twig | 37 +
.../templates/forms/settings-profile.html.twig | 40 +
.../templates/mail/password-reset.html.twig | 22 +
.../templates/mail/resend-verification.html.twig | 17 +
.../templates/mail/verify-account.html.twig | 21 +
.../account/templates/modals/tos.html.twig | 16 +
.../templates/navigation/main-nav.html.twig | 13 +
.../templates/navigation/user-card.html.twig | 33 +
.../templates/pages/account-settings.html.twig | 45 +
.../templates/pages/error/compromised.html.twig | 11 +
.../templates/pages/forgot-password.html.twig | 46 +
.../account/templates/pages/register.html.twig | 105 +
.../templates/pages/resend-verification.html.twig | 46 +
.../templates/pages/reset-password.html.twig | 56 +
.../account/templates/pages/set-password.html.twig | 55 +
.../account/templates/pages/sign-in.html.twig | 84 +
.../sprinkles/account/tests/Unit/FactoriesTest.php | 30 +
.../sprinkles/account/tests/Unit/HasherTest.php | 71 +
login/app/sprinkles/admin/asset-bundles.json | 170 +
.../assets/userfrosting/css/tablesorter-custom.css | 30 +
.../assets/userfrosting/js/pages/activities.js | 16 +
.../assets/userfrosting/js/pages/dashboard.js | 49 +
.../admin/assets/userfrosting/js/pages/group.js | 24 +
.../admin/assets/userfrosting/js/pages/groups.js | 24 +
.../assets/userfrosting/js/pages/permission.js | 20 +
.../assets/userfrosting/js/pages/permissions.js | 16 +
.../admin/assets/userfrosting/js/pages/role.js | 23 +
.../admin/assets/userfrosting/js/pages/roles.js | 24 +
.../admin/assets/userfrosting/js/pages/user.js | 25 +
.../admin/assets/userfrosting/js/pages/users.js | 24 +
.../admin/assets/userfrosting/js/widgets/groups.js | 111 +
.../admin/assets/userfrosting/js/widgets/roles.js | 148 +
.../admin/assets/userfrosting/js/widgets/users.js | 277 +
login/app/sprinkles/admin/composer.json | 22 +
login/app/sprinkles/admin/locale/ar/messages.php | 135 +
.../app/sprinkles/admin/locale/de_DE/messages.php | 161 +
.../app/sprinkles/admin/locale/en_US/messages.php | 160 +
.../app/sprinkles/admin/locale/es_ES/messages.php | 164 +
login/app/sprinkles/admin/locale/fa/messages.php | 158 +
.../app/sprinkles/admin/locale/fr_FR/messages.php | 147 +
.../app/sprinkles/admin/locale/it_IT/messages.php | 160 +
.../app/sprinkles/admin/locale/pt_PT/messages.php | 139 +
.../app/sprinkles/admin/locale/ru_RU/messages.php | 160 +
.../app/sprinkles/admin/locale/th_TH/messages.php | 134 +
login/app/sprinkles/admin/locale/tr/messages.php | 160 +
.../app/sprinkles/admin/locale/zh_CN/messages.php | 161 +
login/app/sprinkles/admin/routes/activities.php | 19 +
login/app/sprinkles/admin/routes/admin.php | 23 +
login/app/sprinkles/admin/routes/groups.php | 39 +
login/app/sprinkles/admin/routes/permissions.php | 25 +
login/app/sprinkles/admin/routes/roles.php | 45 +
login/app/sprinkles/admin/routes/users.php | 51 +
.../admin/schema/requests/group/create.yaml | 35 +
.../admin/schema/requests/group/edit-info.yaml | 27 +
.../admin/schema/requests/group/get-by-slug.yaml | 6 +
.../admin/schema/requests/role/create.yaml | 26 +
.../admin/schema/requests/role/edit-field.yaml | 24 +
.../admin/schema/requests/role/edit-info.yaml | 20 +
.../admin/schema/requests/role/get-by-slug.yaml | 6 +
.../admin/schema/requests/user/create.yaml | 72 +
.../admin/schema/requests/user/edit-field.yaml | 60 +
.../admin/schema/requests/user/edit-info.yaml | 36 +
.../admin/schema/requests/user/edit-password.yaml | 30 +
.../schema/requests/user/get-by-username.yaml | 6 +
login/app/sprinkles/admin/src/Admin.php | 20 +
.../admin/src/Controller/ActivityController.php | 85 +
.../admin/src/Controller/AdminController.php | 150 +
.../admin/src/Controller/GroupController.php | 725 +++
.../admin/src/Controller/PermissionController.php | 206 +
.../admin/src/Controller/RoleController.php | 930 ++++
.../admin/src/Controller/UserController.php | 1278 +++++
.../src/ServicesProvider/ServicesProvider.php | 84 +
.../admin/src/Sprunje/ActivitySprunje.php | 80 +
.../sprinkles/admin/src/Sprunje/GroupSprunje.php | 42 +
.../admin/src/Sprunje/PermissionSprunje.php | 93 +
.../admin/src/Sprunje/PermissionUserSprunje.php | 48 +
.../sprinkles/admin/src/Sprunje/RoleSprunje.php | 67 +
.../admin/src/Sprunje/UserPermissionSprunje.php | 48 +
.../sprinkles/admin/src/Sprunje/UserSprunje.php | 185 +
.../admin/templates/forms/group.html.twig | 69 +
.../sprinkles/admin/templates/forms/role.html.twig | 56 +
.../sprinkles/admin/templates/forms/user.html.twig | 125 +
.../admin/templates/mail/password-create.html.twig | 19 +
.../templates/modals/confirm-clear-cache.html.twig | 17 +
.../modals/confirm-delete-group.html.twig | 17 +
.../templates/modals/confirm-delete-role.html.twig | 17 +
.../templates/modals/confirm-delete-user.html.twig | 17 +
.../admin/templates/modals/group.html.twig | 7 +
.../modals/role-manage-permissions.html.twig | 94 +
.../admin/templates/modals/role.html.twig | 7 +
.../templates/modals/user-manage-roles.html.twig | 77 +
.../templates/modals/user-set-password.html.twig | 62 +
.../admin/templates/modals/user.html.twig | 7 +
.../admin/templates/navigation/navbar.html.twig | 15 +
.../templates/navigation/sidebar-menu.html.twig | 38 +
.../templates/navigation/sidebar-user.html.twig | 10 +
.../admin/templates/navigation/sidebar.html.twig | 10 +
.../admin/templates/navigation/user-card.html.twig | 8 +
.../templates/pages/abstract/dashboard.html.twig | 87 +
.../admin/templates/pages/activities.html.twig | 46 +
.../admin/templates/pages/dashboard.html.twig | 282 +
.../admin/templates/pages/group.html.twig | 106 +
.../admin/templates/pages/groups.html.twig | 52 +
.../admin/templates/pages/permission.html.twig | 91 +
.../admin/templates/pages/permissions.html.twig | 45 +
.../sprinkles/admin/templates/pages/role.html.twig | 129 +
.../admin/templates/pages/roles.html.twig | 50 +
.../sprinkles/admin/templates/pages/user.html.twig | 195 +
.../admin/templates/pages/users.html.twig | 53 +
.../admin/templates/tables/activities.html.twig | 73 +
.../admin/templates/tables/groups.html.twig | 69 +
.../admin/templates/tables/permissions.html.twig | 66 +
.../admin/templates/tables/roles.html.twig | 74 +
.../admin/templates/tables/users.html.twig | 149 +
.../admin/tests/Integration/SprunjeTests.php | 111 +
login/app/sprinkles/core/asset-bundles.json | 80 +
.../assets/font-starcraft/css/font-starcraft.css | 61 +
.../assets/font-starcraft/fonts/font-starcraft.eot | Bin 0 -> 48420 bytes
.../assets/font-starcraft/fonts/font-starcraft.svg | 22 +
.../assets/font-starcraft/fonts/font-starcraft.ttf | Bin 0 -> 48248 bytes
.../font-starcraft/fonts/font-starcraft.woff | Bin 0 -> 39164 bytes
.../assets/userfrosting/css/AdminLTE-skins-all.css | 1770 +++++++
.../core/assets/userfrosting/css/AdminLTE.css | 4932 ++++++++++++++++++
.../assets/userfrosting/css/tablesorter-reflow.css | 61 +
.../core/assets/userfrosting/css/uf-alerts.css | 23 +
.../core/assets/userfrosting/css/uf-collection.css | 15 +
.../userfrosting/css/uf-jqueryvalidation.css | 10 +
.../core/assets/userfrosting/css/userfrosting.css | 204 +
.../core/assets/userfrosting/favicons/README.md | 7 +
.../favicons/android-chrome-144x144.png | Bin 0 -> 20991 bytes
.../favicons/android-chrome-192x192.png | Bin 0 -> 29935 bytes
.../favicons/android-chrome-256x256.png | Bin 0 -> 42828 bytes
.../userfrosting/favicons/android-chrome-36x36.png | Bin 0 -> 3150 bytes
.../favicons/android-chrome-384x384.png | Bin 0 -> 69092 bytes
.../userfrosting/favicons/android-chrome-48x48.png | Bin 0 -> 4736 bytes
.../favicons/android-chrome-512x512.png | Bin 0 -> 96438 bytes
.../userfrosting/favicons/android-chrome-72x72.png | Bin 0 -> 8367 bytes
.../userfrosting/favicons/android-chrome-96x96.png | Bin 0 -> 12470 bytes
.../apple-touch-icon-114x114-precomposed.png | Bin 0 -> 14098 bytes
.../favicons/apple-touch-icon-114x114.png | Bin 0 -> 12666 bytes
.../apple-touch-icon-120x120-precomposed.png | Bin 0 -> 15188 bytes
.../favicons/apple-touch-icon-120x120.png | Bin 0 -> 13650 bytes
.../apple-touch-icon-144x144-precomposed.png | Bin 0 -> 19338 bytes
.../favicons/apple-touch-icon-144x144.png | Bin 0 -> 17393 bytes
.../apple-touch-icon-152x152-precomposed.png | Bin 0 -> 20475 bytes
.../favicons/apple-touch-icon-152x152.png | Bin 0 -> 18420 bytes
.../apple-touch-icon-180x180-precomposed.png | Bin 0 -> 25283 bytes
.../favicons/apple-touch-icon-180x180.png | Bin 0 -> 22957 bytes
.../apple-touch-icon-57x57-precomposed.png | Bin 0 -> 5279 bytes
.../favicons/apple-touch-icon-57x57.png | Bin 0 -> 4660 bytes
.../apple-touch-icon-60x60-precomposed.png | Bin 0 -> 5706 bytes
.../favicons/apple-touch-icon-60x60.png | Bin 0 -> 5017 bytes
.../apple-touch-icon-72x72-precomposed.png | Bin 0 -> 7498 bytes
.../favicons/apple-touch-icon-72x72.png | Bin 0 -> 6615 bytes
.../apple-touch-icon-76x76-precomposed.png | Bin 0 -> 8080 bytes
.../favicons/apple-touch-icon-76x76.png | Bin 0 -> 7180 bytes
.../favicons/apple-touch-icon-precomposed.png | Bin 0 -> 25283 bytes
.../userfrosting/favicons/apple-touch-icon.png | Bin 0 -> 22957 bytes
.../assets/userfrosting/favicons/favicon-16x16.png | Bin 0 -> 1374 bytes
.../assets/userfrosting/favicons/favicon-32x32.png | Bin 0 -> 2678 bytes
.../core/assets/userfrosting/favicons/favicon.ico | Bin 0 -> 15086 bytes
.../core/assets/userfrosting/favicons/ieconfig.xml | 12 +
.../assets/userfrosting/favicons/manifest.json | 51 +
.../userfrosting/favicons/mstile-144x144.png | Bin 0 -> 20296 bytes
.../userfrosting/favicons/mstile-150x150.png | Bin 0 -> 18368 bytes
.../userfrosting/favicons/mstile-310x150.png | Bin 0 -> 19511 bytes
.../userfrosting/favicons/mstile-310x310.png | Bin 0 -> 46338 bytes
.../assets/userfrosting/favicons/mstile-70x70.png | Bin 0 -> 12070 bytes
.../userfrosting/favicons/safari-pinned-tab.svg | 246 +
.../core/assets/userfrosting/images/cupcake.png | Bin 0 -> 19738 bytes
.../core/assets/userfrosting/images/logo.svg | 514 ++
.../core/assets/userfrosting/js/AdminLTE-custom.js | 117 +
.../core/assets/userfrosting/js/AdminLTE.js | 763 +++
.../core/assets/userfrosting/js/attrchange.js | 124 +
.../js/fortress-jqueryvalidation-methods.js | 57 +
.../assets/userfrosting/js/handlebars-helpers.js | 119 +
.../core/assets/userfrosting/js/query-string.js | 65 +
.../js/tablesorter/widget-sort2Hash.js | 271 +
.../core/assets/userfrosting/js/uf-alerts.js | 285 +
.../core/assets/userfrosting/js/uf-captcha.js | 13 +
.../core/assets/userfrosting/js/uf-collection.js | 345 ++
.../core/assets/userfrosting/js/uf-copy.js | 50 +
.../core/assets/userfrosting/js/uf-form.js | 297 ++
.../core/assets/userfrosting/js/uf-init.js | 26 +
.../userfrosting/js/uf-jqueryvalidation-config.js | 49 +
.../core/assets/userfrosting/js/uf-modal.js | 192 +
.../core/assets/userfrosting/js/uf-table.js | 704 +++
.../userfrosting/js/uf-tablesorter-parsers.js | 53 +
login/app/sprinkles/core/bower.json | 47 +
login/app/sprinkles/core/composer.json | 44 +
login/app/sprinkles/core/config/default.php | 182 +
login/app/sprinkles/core/config/dev.php | 30 +
login/app/sprinkles/core/config/production.php | 40 +
login/app/sprinkles/core/config/testing.php | 23 +
login/app/sprinkles/core/extra/adjectives.php | 1221 +++++
login/app/sprinkles/core/extra/nouns.php | 90 +
login/app/sprinkles/core/locale/ar/errors.php | 51 +
login/app/sprinkles/core/locale/ar/messages.php | 112 +
login/app/sprinkles/core/locale/ar/validate.php | 25 +
login/app/sprinkles/core/locale/de_DE/errors.php | 53 +
login/app/sprinkles/core/locale/de_DE/messages.php | 123 +
login/app/sprinkles/core/locale/de_DE/validate.php | 32 +
login/app/sprinkles/core/locale/en_US/errors.php | 51 +
login/app/sprinkles/core/locale/en_US/messages.php | 120 +
login/app/sprinkles/core/locale/en_US/validate.php | 33 +
login/app/sprinkles/core/locale/es_ES/errors.php | 51 +
login/app/sprinkles/core/locale/es_ES/messages.php | 115 +
login/app/sprinkles/core/locale/es_ES/validate.php | 35 +
login/app/sprinkles/core/locale/fa/errors.php | 52 +
login/app/sprinkles/core/locale/fa/messages.php | 110 +
login/app/sprinkles/core/locale/fa/validate.php | 31 +
login/app/sprinkles/core/locale/fr_FR/errors.php | 51 +
login/app/sprinkles/core/locale/fr_FR/messages.php | 105 +
login/app/sprinkles/core/locale/fr_FR/validate.php | 33 +
login/app/sprinkles/core/locale/it_IT/errors.php | 53 +
login/app/sprinkles/core/locale/it_IT/messages.php | 123 +
login/app/sprinkles/core/locale/it_IT/validate.php | 32 +
login/app/sprinkles/core/locale/pt_PT/errors.php | 51 +
login/app/sprinkles/core/locale/pt_PT/messages.php | 102 +
login/app/sprinkles/core/locale/pt_PT/validate.php | 25 +
login/app/sprinkles/core/locale/ru_RU/errors.php | 51 +
login/app/sprinkles/core/locale/ru_RU/messages.php | 120 +
login/app/sprinkles/core/locale/ru_RU/validate.php | 33 +
login/app/sprinkles/core/locale/th_TH/errors.php | 51 +
login/app/sprinkles/core/locale/th_TH/messages.php | 102 +
login/app/sprinkles/core/locale/th_TH/validate.php | 25 +
login/app/sprinkles/core/locale/valitron/ar.php | 28 +
login/app/sprinkles/core/locale/valitron/de.php | 33 +
login/app/sprinkles/core/locale/valitron/el.php | 34 +
login/app/sprinkles/core/locale/valitron/en.php | 34 +
login/app/sprinkles/core/locale/valitron/es.php | 34 +
login/app/sprinkles/core/locale/valitron/fr.php | 34 +
login/app/sprinkles/core/locale/valitron/id.php | 33 +
login/app/sprinkles/core/locale/valitron/it.php | 31 +
login/app/sprinkles/core/locale/valitron/ja.php | 33 +
login/app/sprinkles/core/locale/valitron/lv.php | 31 +
login/app/sprinkles/core/locale/valitron/pt-br.php | 28 +
login/app/sprinkles/core/locale/valitron/ro.php | 33 +
login/app/sprinkles/core/locale/valitron/ru.php | 33 +
login/app/sprinkles/core/locale/valitron/th.php | 34 +
login/app/sprinkles/core/locale/valitron/zh-cn.php | 28 +
login/app/sprinkles/core/locale/valitron/zh-tw.php | 28 +
login/app/sprinkles/core/locale/zh_CN/errors.php | 49 +
login/app/sprinkles/core/locale/zh_CN/messages.php | 105 +
login/app/sprinkles/core/locale/zh_CN/validate.php | 29 +
login/app/sprinkles/core/routes/routes.php | 24 +
login/app/sprinkles/core/schema/.gitkeep | 0
login/app/sprinkles/core/src/Alert/AlertStream.php | 144 +
.../sprinkles/core/src/Alert/CacheAlertStream.php | 84 +
.../core/src/Alert/SessionAlertStream.php | 70 +
.../core/src/Controller/CoreController.php | 95 +
.../core/src/Controller/SimpleController.php | 36 +
login/app/sprinkles/core/src/Core.php | 121 +
login/app/sprinkles/core/src/Database/Builder.php | 210 +
.../core/src/Database/DatabaseInvalidException.php | 20 +
.../src/Database/Migrations/v400/SessionsTable.php | 48 +
.../Database/Migrations/v400/ThrottlesTable.php | 52 +
.../Database/Models/Concerns/HasRelationships.php | 278 +
.../sprinkles/core/src/Database/Models/Model.php | 140 +
.../core/src/Database/Models/Throttle.php | 36 +
.../Relations/BelongsToManyConstrained.php | 122 +
.../Database/Relations/BelongsToManyThrough.php | 232 +
.../src/Database/Relations/BelongsToManyUnique.php | 22 +
.../src/Database/Relations/Concerns/Syncable.php | 132 +
.../src/Database/Relations/Concerns/Unique.php | 563 ++
.../src/Database/Relations/HasManySyncable.php | 22 +
.../src/Database/Relations/MorphManySyncable.php | 22 +
.../src/Database/Relations/MorphToManyUnique.php | 22 +
.../core/src/Error/ExceptionHandlerManager.php | 93 +
.../core/src/Error/Handler/ExceptionHandler.php | 275 +
.../Error/Handler/ExceptionHandlerInterface.php | 32 +
.../src/Error/Handler/HttpExceptionHandler.php | 64 +
.../src/Error/Handler/NotFoundExceptionHandler.php | 38 +
.../Error/Handler/PhpMailerExceptionHandler.php | 30 +
.../core/src/Error/Renderer/ErrorRenderer.php | 64 +
.../src/Error/Renderer/ErrorRendererInterface.php | 29 +
.../core/src/Error/Renderer/HtmlRenderer.php | 151 +
.../core/src/Error/Renderer/JsonRenderer.php | 57 +
.../core/src/Error/Renderer/PlainTextRenderer.php | 65 +
.../core/src/Error/Renderer/WhoopsRenderer.php | 712 +++
.../core/src/Error/Renderer/XmlRenderer.php | 48 +
login/app/sprinkles/core/src/Facades/Debug.php | 28 +
.../app/sprinkles/core/src/Facades/Translator.php | 28 +
.../src/Http/Concerns/DeterminesContentType.php | 76 +
.../app/sprinkles/core/src/Log/DatabaseHandler.php | 53 +
.../app/sprinkles/core/src/Log/MixedFormatter.php | 59 +
.../app/sprinkles/core/src/Mail/EmailRecipient.php | 136 +
login/app/sprinkles/core/src/Mail/MailMessage.php | 186 +
login/app/sprinkles/core/src/Mail/Mailer.php | 204 +
.../sprinkles/core/src/Mail/StaticMailMessage.php | 78 +
.../sprinkles/core/src/Mail/TwigMailMessage.php | 93 +
login/app/sprinkles/core/src/Model/UFModel.php | 27 +
login/app/sprinkles/core/src/Router.php | 101 +
.../core/src/ServicesProvider/ServicesProvider.php | 618 +++
login/app/sprinkles/core/src/Sprunje/Sprunje.php | 566 ++
.../sprinkles/core/src/Throttle/ThrottleRule.php | 140 +
.../app/sprinkles/core/src/Throttle/Throttler.php | 178 +
.../core/src/Throttle/ThrottlerException.php | 18 +
login/app/sprinkles/core/src/Twig/CacheHelper.php | 58 +
.../app/sprinkles/core/src/Twig/CoreExtension.php | 124 +
.../core/src/Util/BadClassNameException.php | 18 +
login/app/sprinkles/core/src/Util/Captcha.php | 159 +
.../sprinkles/core/src/Util/CheckEnvironment.php | 340 ++
login/app/sprinkles/core/src/Util/ClassMapper.php | 94 +
.../sprinkles/core/src/Util/EnvironmentInfo.php | 68 +
.../sprinkles/core/src/Util/ShutdownHandler.php | 167 +
login/app/sprinkles/core/src/Util/Util.php | 173 +
.../sprinkles/core/templates/forms/csrf.html.twig | 2 +
login/app/sprinkles/core/templates/mail/.gitkeep | 0
.../core/templates/modals/modal.html.twig | 27 +
.../core/templates/navigation/breadcrumb.html.twig | 4 +
.../core/templates/navigation/main-nav.html.twig | 31 +
.../sprinkles/core/templates/pages/about.html.twig | 173 +
.../core/templates/pages/abstract/base.html.twig | 103 +
.../templates/pages/abstract/default.html.twig | 45 +
.../core/templates/pages/abstract/error.html.twig | 32 +
.../core/templates/pages/error/400.html.twig | 9 +
.../core/templates/pages/error/404.html.twig | 16 +
.../templates/pages/error/config-errors.html.twig | 22 +
.../sprinkles/core/templates/pages/index.html.twig | 185 +
.../sprinkles/core/templates/pages/legal.html.twig | 12 +
.../core/templates/pages/partials/alerts.html.twig | 13 +
.../templates/pages/partials/analytics.html.twig | 15 +
.../core/templates/pages/partials/config.js.twig | 13 +
.../templates/pages/partials/favicons.html.twig | 39 +
.../core/templates/pages/partials/footer.html.twig | 8 +
.../core/templates/pages/partials/legal.html.twig | 95 +
.../core/templates/pages/partials/page.js.twig | 4 +
.../templates/pages/partials/privacy.html.twig | 35 +
.../core/templates/pages/privacy.html.twig | 12 +
.../templates/tables/table-paginated.html.twig | 59 +
.../templates/tables/table-tool-menu.html.twig | 25 +
.../core/tests/Integration/DatabaseTests.php | 1315 +++++
.../core/tests/Unit/BelongsToManyThroughTest.php | 103 +
.../core/tests/Unit/DatabaseSyncableTest.php | 119 +
.../app/sprinkles/core/tests/Unit/SprunjeTest.php | 100 +
login/app/system/Bakery/Bakery.php | 166 +
login/app/system/Bakery/BaseCommand.php | 58 +
login/app/system/Bakery/Command/Bake.php | 77 +
login/app/system/Bakery/Command/BuildAssets.php | 180 +
login/app/system/Bakery/Command/ClearCache.php | 95 +
login/app/system/Bakery/Command/Debug.php | 185 +
login/app/system/Bakery/Command/Migrate.php | 48 +
login/app/system/Bakery/Command/MigrateRefresh.php | 52 +
login/app/system/Bakery/Command/MigrateReset.php | 49 +
.../app/system/Bakery/Command/MigrateRollback.php | 51 +
login/app/system/Bakery/Command/Setup.php | 223 +
login/app/system/Bakery/Command/Test.php | 56 +
login/app/system/Bakery/DatabaseTest.php | 52 +
login/app/system/Bakery/Migration.php | 64 +
login/app/system/Bakery/Migrator.php | 584 +++
.../Database/Migrations/v410/MigrationTable.php | 59 +
login/app/system/Database/Model/Migrations.php | 55 +
login/app/system/Facade.php | 247 +
login/app/system/ServicesProvider.php | 104 +
login/app/system/SlimAppEvent.php | 29 +
login/app/system/Sprinkle/Sprinkle.php | 56 +
login/app/system/Sprinkle/SprinkleManager.php | 236 +
login/app/system/UserFrosting.php | 187 +
login/app/tests/DatabaseTransactions.php | 48 +
login/app/tests/TestCase.php | 236 +
login/app/tests/Unit/ExampleTest.php | 19 +
login/bakery | 34 +
login/build/before_install.sh | 39 +
login/build/gulpfile.js | 183 +
login/build/package-lock.json | 5512 ++++++++++++++++++++
login/build/package.json | 33 +
login/composer.json | 60 +
login/docker-compose.yml | 56 +
login/docker/README.md | 35 +
login/docker/nginx/Dockerfile | 2 +
login/docker/nginx/default.conf | 28 +
login/docker/node/Dockerfile | 4 +
login/docker/php/Dockerfile | 11 +
login/phpunit.xml | 21 +
login/public/.htaccess | 185 +
login/public/index.php | 23 +
login/screenshots/login.png | Bin 0 -> 3464 bytes
login/screenshots/permissions.png | Bin 0 -> 31896 bytes
login/screenshots/users.png | Bin 0 -> 43380 bytes
login/sponsors/nextgi.png | Bin 0 -> 5673 bytes
login/sponsors/usor.png | Bin 0 -> 12098 bytes
login/webserver-configs/htaccess.txt | 185 +
login/webserver-configs/nginx.conf | 127 +
login/webserver-configs/web.config | 92 +
497 files changed, 56763 insertions(+), 2 deletions(-)
delete mode 160000 login
create mode 100755 login/.github/CONTRIBUTING.md
create mode 100755 login/.github/ISSUE_TEMPLATE.md
create mode 100755 login/.travis.yml
create mode 100755 login/CHANGELOG.md
create mode 100755 login/LICENSE.md
create mode 100755 login/README.md
create mode 100755 login/STYLE-GUIDE.md
create mode 100755 login/app/.env.example
create mode 100755 login/app/.htaccess
create mode 100755 login/app/cache/.gitkeep
create mode 100755 login/app/defines.php
create mode 100755 login/app/logs/.gitkeep
create mode 100755 login/app/sessions/.gitkeep
create mode 100755 login/app/sprinkles.example.json
create mode 100755 login/app/sprinkles/account/asset-bundles.json
create mode 100755 login/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js
create mode 100755 login/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js
create mode 100755 login/app/sprinkles/account/assets/userfrosting/js/pages/register.js
create mode 100755 login/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js
create mode 100755 login/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js
create mode 100755 login/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js
create mode 100755 login/app/sprinkles/account/bower.json
create mode 100755 login/app/sprinkles/account/composer.json
create mode 100755 login/app/sprinkles/account/config/default.php
create mode 100755 login/app/sprinkles/account/config/production.php
create mode 100755 login/app/sprinkles/account/factories/Permissions.php
create mode 100755 login/app/sprinkles/account/factories/Roles.php
create mode 100755 login/app/sprinkles/account/factories/Users.php
create mode 100755 login/app/sprinkles/account/locale/ar/messages.php
create mode 100755 login/app/sprinkles/account/locale/ar/validate.php
create mode 100755 login/app/sprinkles/account/locale/de_DE/messages.php
create mode 100755 login/app/sprinkles/account/locale/de_DE/validate.php
create mode 100755 login/app/sprinkles/account/locale/en_US/messages.php
create mode 100755 login/app/sprinkles/account/locale/en_US/validate.php
create mode 100755 login/app/sprinkles/account/locale/es_ES/messages.php
create mode 100755 login/app/sprinkles/account/locale/es_ES/validate.php
create mode 100755 login/app/sprinkles/account/locale/fa/messages.php
create mode 100755 login/app/sprinkles/account/locale/fa/validate.php
create mode 100755 login/app/sprinkles/account/locale/fr_FR/messages.php
create mode 100755 login/app/sprinkles/account/locale/fr_FR/validate.php
create mode 100755 login/app/sprinkles/account/locale/it_IT/messages.php
create mode 100755 login/app/sprinkles/account/locale/it_IT/validate.php
create mode 100755 login/app/sprinkles/account/locale/pt_PT/messages.php
create mode 100755 login/app/sprinkles/account/locale/pt_PT/validate.php
create mode 100755 login/app/sprinkles/account/locale/ru_RU/messages.php
create mode 100755 login/app/sprinkles/account/locale/ru_RU/validate.php
create mode 100755 login/app/sprinkles/account/locale/th_TH/messages.php
create mode 100755 login/app/sprinkles/account/locale/th_TH/validate.php
create mode 100755 login/app/sprinkles/account/locale/tr/messages.php
create mode 100755 login/app/sprinkles/account/locale/tr/validate.php
create mode 100755 login/app/sprinkles/account/locale/zh_CN/messages.php
create mode 100755 login/app/sprinkles/account/locale/zh_CN/validate.php
create mode 100755 login/app/sprinkles/account/routes/routes.php
create mode 100755 login/app/sprinkles/account/schema/requests/account-settings.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/account-verify.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/check-username.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/deny-password.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/forgot-password.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/login.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/profile-settings.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/register.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/resend-verification.yaml
create mode 100755 login/app/sprinkles/account/schema/requests/set-password.yaml
create mode 100755 login/app/sprinkles/account/src/Account.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/AuthGuard.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Authenticator.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php
create mode 100755 login/app/sprinkles/account/src/Authenticate/Hasher.php
create mode 100755 login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php
create mode 100755 login/app/sprinkles/account/src/Authorize/AuthorizationException.php
create mode 100755 login/app/sprinkles/account/src/Authorize/AuthorizationManager.php
create mode 100755 login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php
create mode 100755 login/app/sprinkles/account/src/Bakery/CreateAdminUser.php
create mode 100755 login/app/sprinkles/account/src/Controller/AccountController.php
create mode 100755 login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/Activity.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/Group.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/PasswordReset.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/Permission.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/Role.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/User.php
create mode 100755 login/app/sprinkles/account/src/Database/Models/Verification.php
create mode 100755 login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php
create mode 100755 login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php
create mode 100755 login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php
create mode 100755 login/app/sprinkles/account/src/Facades/Password.php
create mode 100755 login/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php
create mode 100755 login/app/sprinkles/account/src/Log/UserActivityProcessor.php
create mode 100755 login/app/sprinkles/account/src/Repository/PasswordResetRepository.php
create mode 100755 login/app/sprinkles/account/src/Repository/TokenRepository.php
create mode 100755 login/app/sprinkles/account/src/Repository/VerificationRepository.php
create mode 100755 login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php
create mode 100755 login/app/sprinkles/account/src/Twig/AccountExtension.php
create mode 100755 login/app/sprinkles/account/src/Util/HashFailedException.php
create mode 100755 login/app/sprinkles/account/src/Util/Util.php
create mode 100755 login/app/sprinkles/account/templates/forms/settings-account.html.twig
create mode 100755 login/app/sprinkles/account/templates/forms/settings-profile.html.twig
create mode 100755 login/app/sprinkles/account/templates/mail/password-reset.html.twig
create mode 100755 login/app/sprinkles/account/templates/mail/resend-verification.html.twig
create mode 100755 login/app/sprinkles/account/templates/mail/verify-account.html.twig
create mode 100755 login/app/sprinkles/account/templates/modals/tos.html.twig
create mode 100755 login/app/sprinkles/account/templates/navigation/main-nav.html.twig
create mode 100755 login/app/sprinkles/account/templates/navigation/user-card.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/account-settings.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/error/compromised.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/forgot-password.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/register.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/resend-verification.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/reset-password.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/set-password.html.twig
create mode 100755 login/app/sprinkles/account/templates/pages/sign-in.html.twig
create mode 100755 login/app/sprinkles/account/tests/Unit/FactoriesTest.php
create mode 100755 login/app/sprinkles/account/tests/Unit/HasherTest.php
create mode 100755 login/app/sprinkles/admin/asset-bundles.json
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js
create mode 100755 login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js
create mode 100755 login/app/sprinkles/admin/composer.json
create mode 100755 login/app/sprinkles/admin/locale/ar/messages.php
create mode 100755 login/app/sprinkles/admin/locale/de_DE/messages.php
create mode 100755 login/app/sprinkles/admin/locale/en_US/messages.php
create mode 100755 login/app/sprinkles/admin/locale/es_ES/messages.php
create mode 100755 login/app/sprinkles/admin/locale/fa/messages.php
create mode 100755 login/app/sprinkles/admin/locale/fr_FR/messages.php
create mode 100755 login/app/sprinkles/admin/locale/it_IT/messages.php
create mode 100755 login/app/sprinkles/admin/locale/pt_PT/messages.php
create mode 100755 login/app/sprinkles/admin/locale/ru_RU/messages.php
create mode 100755 login/app/sprinkles/admin/locale/th_TH/messages.php
create mode 100755 login/app/sprinkles/admin/locale/tr/messages.php
create mode 100755 login/app/sprinkles/admin/locale/zh_CN/messages.php
create mode 100755 login/app/sprinkles/admin/routes/activities.php
create mode 100755 login/app/sprinkles/admin/routes/admin.php
create mode 100755 login/app/sprinkles/admin/routes/groups.php
create mode 100755 login/app/sprinkles/admin/routes/permissions.php
create mode 100755 login/app/sprinkles/admin/routes/roles.php
create mode 100755 login/app/sprinkles/admin/routes/users.php
create mode 100755 login/app/sprinkles/admin/schema/requests/group/create.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/group/edit-info.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/role/create.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/role/edit-field.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/role/edit-info.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/user/create.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/user/edit-field.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/user/edit-info.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/user/edit-password.yaml
create mode 100755 login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml
create mode 100755 login/app/sprinkles/admin/src/Admin.php
create mode 100755 login/app/sprinkles/admin/src/Controller/ActivityController.php
create mode 100755 login/app/sprinkles/admin/src/Controller/AdminController.php
create mode 100755 login/app/sprinkles/admin/src/Controller/GroupController.php
create mode 100755 login/app/sprinkles/admin/src/Controller/PermissionController.php
create mode 100755 login/app/sprinkles/admin/src/Controller/RoleController.php
create mode 100755 login/app/sprinkles/admin/src/Controller/UserController.php
create mode 100755 login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php
create mode 100755 login/app/sprinkles/admin/src/Sprunje/UserSprunje.php
create mode 100755 login/app/sprinkles/admin/templates/forms/group.html.twig
create mode 100755 login/app/sprinkles/admin/templates/forms/role.html.twig
create mode 100755 login/app/sprinkles/admin/templates/forms/user.html.twig
create mode 100755 login/app/sprinkles/admin/templates/mail/password-create.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/confirm-clear-cache.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/confirm-delete-group.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/confirm-delete-role.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/confirm-delete-user.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/group.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/role-manage-permissions.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/role.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/user-manage-roles.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/user-set-password.html.twig
create mode 100755 login/app/sprinkles/admin/templates/modals/user.html.twig
create mode 100755 login/app/sprinkles/admin/templates/navigation/navbar.html.twig
create mode 100755 login/app/sprinkles/admin/templates/navigation/sidebar-menu.html.twig
create mode 100755 login/app/sprinkles/admin/templates/navigation/sidebar-user.html.twig
create mode 100755 login/app/sprinkles/admin/templates/navigation/sidebar.html.twig
create mode 100755 login/app/sprinkles/admin/templates/navigation/user-card.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/abstract/dashboard.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/activities.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/dashboard.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/group.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/groups.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/permission.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/permissions.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/role.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/roles.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/user.html.twig
create mode 100755 login/app/sprinkles/admin/templates/pages/users.html.twig
create mode 100755 login/app/sprinkles/admin/templates/tables/activities.html.twig
create mode 100755 login/app/sprinkles/admin/templates/tables/groups.html.twig
create mode 100755 login/app/sprinkles/admin/templates/tables/permissions.html.twig
create mode 100755 login/app/sprinkles/admin/templates/tables/roles.html.twig
create mode 100755 login/app/sprinkles/admin/templates/tables/users.html.twig
create mode 100755 login/app/sprinkles/admin/tests/Integration/SprunjeTests.php
create mode 100755 login/app/sprinkles/core/asset-bundles.json
create mode 100755 login/app/sprinkles/core/assets/font-starcraft/css/font-starcraft.css
create mode 100755 login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.eot
create mode 100755 login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.svg
create mode 100755 login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.ttf
create mode 100755 login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.woff
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/AdminLTE-skins-all.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/AdminLTE.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/tablesorter-reflow.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/uf-alerts.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/uf-collection.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/uf-jqueryvalidation.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/css/userfrosting.css
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/README.md
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-144x144.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-192x192.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-256x256.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-36x36.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-384x384.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-48x48.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-512x512.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-72x72.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-96x96.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-precomposed.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/favicon-16x16.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/favicon-32x32.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/favicon.ico
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/ieconfig.xml
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/manifest.json
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/mstile-144x144.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/mstile-150x150.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x150.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x310.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/mstile-70x70.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/favicons/safari-pinned-tab.svg
create mode 100755 login/app/sprinkles/core/assets/userfrosting/images/cupcake.png
create mode 100755 login/app/sprinkles/core/assets/userfrosting/images/logo.svg
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/AdminLTE-custom.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/AdminLTE.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/attrchange.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/query-string.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-form.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-init.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-jqueryvalidation-config.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-modal.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-table.js
create mode 100755 login/app/sprinkles/core/assets/userfrosting/js/uf-tablesorter-parsers.js
create mode 100755 login/app/sprinkles/core/bower.json
create mode 100755 login/app/sprinkles/core/composer.json
create mode 100755 login/app/sprinkles/core/config/default.php
create mode 100755 login/app/sprinkles/core/config/dev.php
create mode 100755 login/app/sprinkles/core/config/production.php
create mode 100755 login/app/sprinkles/core/config/testing.php
create mode 100755 login/app/sprinkles/core/extra/adjectives.php
create mode 100755 login/app/sprinkles/core/extra/nouns.php
create mode 100755 login/app/sprinkles/core/locale/ar/errors.php
create mode 100755 login/app/sprinkles/core/locale/ar/messages.php
create mode 100755 login/app/sprinkles/core/locale/ar/validate.php
create mode 100755 login/app/sprinkles/core/locale/de_DE/errors.php
create mode 100755 login/app/sprinkles/core/locale/de_DE/messages.php
create mode 100755 login/app/sprinkles/core/locale/de_DE/validate.php
create mode 100755 login/app/sprinkles/core/locale/en_US/errors.php
create mode 100755 login/app/sprinkles/core/locale/en_US/messages.php
create mode 100755 login/app/sprinkles/core/locale/en_US/validate.php
create mode 100755 login/app/sprinkles/core/locale/es_ES/errors.php
create mode 100755 login/app/sprinkles/core/locale/es_ES/messages.php
create mode 100755 login/app/sprinkles/core/locale/es_ES/validate.php
create mode 100755 login/app/sprinkles/core/locale/fa/errors.php
create mode 100755 login/app/sprinkles/core/locale/fa/messages.php
create mode 100755 login/app/sprinkles/core/locale/fa/validate.php
create mode 100755 login/app/sprinkles/core/locale/fr_FR/errors.php
create mode 100755 login/app/sprinkles/core/locale/fr_FR/messages.php
create mode 100755 login/app/sprinkles/core/locale/fr_FR/validate.php
create mode 100755 login/app/sprinkles/core/locale/it_IT/errors.php
create mode 100755 login/app/sprinkles/core/locale/it_IT/messages.php
create mode 100755 login/app/sprinkles/core/locale/it_IT/validate.php
create mode 100755 login/app/sprinkles/core/locale/pt_PT/errors.php
create mode 100755 login/app/sprinkles/core/locale/pt_PT/messages.php
create mode 100755 login/app/sprinkles/core/locale/pt_PT/validate.php
create mode 100755 login/app/sprinkles/core/locale/ru_RU/errors.php
create mode 100755 login/app/sprinkles/core/locale/ru_RU/messages.php
create mode 100755 login/app/sprinkles/core/locale/ru_RU/validate.php
create mode 100755 login/app/sprinkles/core/locale/th_TH/errors.php
create mode 100755 login/app/sprinkles/core/locale/th_TH/messages.php
create mode 100755 login/app/sprinkles/core/locale/th_TH/validate.php
create mode 100755 login/app/sprinkles/core/locale/valitron/ar.php
create mode 100755 login/app/sprinkles/core/locale/valitron/de.php
create mode 100755 login/app/sprinkles/core/locale/valitron/el.php
create mode 100755 login/app/sprinkles/core/locale/valitron/en.php
create mode 100755 login/app/sprinkles/core/locale/valitron/es.php
create mode 100755 login/app/sprinkles/core/locale/valitron/fr.php
create mode 100755 login/app/sprinkles/core/locale/valitron/id.php
create mode 100755 login/app/sprinkles/core/locale/valitron/it.php
create mode 100755 login/app/sprinkles/core/locale/valitron/ja.php
create mode 100755 login/app/sprinkles/core/locale/valitron/lv.php
create mode 100755 login/app/sprinkles/core/locale/valitron/pt-br.php
create mode 100755 login/app/sprinkles/core/locale/valitron/ro.php
create mode 100755 login/app/sprinkles/core/locale/valitron/ru.php
create mode 100755 login/app/sprinkles/core/locale/valitron/th.php
create mode 100755 login/app/sprinkles/core/locale/valitron/zh-cn.php
create mode 100755 login/app/sprinkles/core/locale/valitron/zh-tw.php
create mode 100755 login/app/sprinkles/core/locale/zh_CN/errors.php
create mode 100755 login/app/sprinkles/core/locale/zh_CN/messages.php
create mode 100755 login/app/sprinkles/core/locale/zh_CN/validate.php
create mode 100755 login/app/sprinkles/core/routes/routes.php
create mode 100755 login/app/sprinkles/core/schema/.gitkeep
create mode 100755 login/app/sprinkles/core/src/Alert/AlertStream.php
create mode 100755 login/app/sprinkles/core/src/Alert/CacheAlertStream.php
create mode 100755 login/app/sprinkles/core/src/Alert/SessionAlertStream.php
create mode 100755 login/app/sprinkles/core/src/Controller/CoreController.php
create mode 100755 login/app/sprinkles/core/src/Controller/SimpleController.php
create mode 100755 login/app/sprinkles/core/src/Core.php
create mode 100755 login/app/sprinkles/core/src/Database/Builder.php
create mode 100755 login/app/sprinkles/core/src/Database/DatabaseInvalidException.php
create mode 100755 login/app/sprinkles/core/src/Database/Migrations/v400/SessionsTable.php
create mode 100755 login/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php
create mode 100755 login/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php
create mode 100755 login/app/sprinkles/core/src/Database/Models/Model.php
create mode 100755 login/app/sprinkles/core/src/Database/Models/Throttle.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/BelongsToManyConstrained.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/Concerns/Syncable.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/HasManySyncable.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/MorphManySyncable.php
create mode 100755 login/app/sprinkles/core/src/Database/Relations/MorphToManyUnique.php
create mode 100755 login/app/sprinkles/core/src/Error/ExceptionHandlerManager.php
create mode 100755 login/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php
create mode 100755 login/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php
create mode 100755 login/app/sprinkles/core/src/Error/Handler/HttpExceptionHandler.php
create mode 100755 login/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php
create mode 100755 login/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/ErrorRenderer.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/HtmlRenderer.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php
create mode 100755 login/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php
create mode 100755 login/app/sprinkles/core/src/Facades/Debug.php
create mode 100755 login/app/sprinkles/core/src/Facades/Translator.php
create mode 100755 login/app/sprinkles/core/src/Http/Concerns/DeterminesContentType.php
create mode 100755 login/app/sprinkles/core/src/Log/DatabaseHandler.php
create mode 100755 login/app/sprinkles/core/src/Log/MixedFormatter.php
create mode 100755 login/app/sprinkles/core/src/Mail/EmailRecipient.php
create mode 100755 login/app/sprinkles/core/src/Mail/MailMessage.php
create mode 100755 login/app/sprinkles/core/src/Mail/Mailer.php
create mode 100755 login/app/sprinkles/core/src/Mail/StaticMailMessage.php
create mode 100755 login/app/sprinkles/core/src/Mail/TwigMailMessage.php
create mode 100755 login/app/sprinkles/core/src/Model/UFModel.php
create mode 100755 login/app/sprinkles/core/src/Router.php
create mode 100755 login/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php
create mode 100755 login/app/sprinkles/core/src/Sprunje/Sprunje.php
create mode 100755 login/app/sprinkles/core/src/Throttle/ThrottleRule.php
create mode 100755 login/app/sprinkles/core/src/Throttle/Throttler.php
create mode 100755 login/app/sprinkles/core/src/Throttle/ThrottlerException.php
create mode 100755 login/app/sprinkles/core/src/Twig/CacheHelper.php
create mode 100755 login/app/sprinkles/core/src/Twig/CoreExtension.php
create mode 100755 login/app/sprinkles/core/src/Util/BadClassNameException.php
create mode 100755 login/app/sprinkles/core/src/Util/Captcha.php
create mode 100755 login/app/sprinkles/core/src/Util/CheckEnvironment.php
create mode 100755 login/app/sprinkles/core/src/Util/ClassMapper.php
create mode 100755 login/app/sprinkles/core/src/Util/EnvironmentInfo.php
create mode 100755 login/app/sprinkles/core/src/Util/ShutdownHandler.php
create mode 100755 login/app/sprinkles/core/src/Util/Util.php
create mode 100755 login/app/sprinkles/core/templates/forms/csrf.html.twig
create mode 100755 login/app/sprinkles/core/templates/mail/.gitkeep
create mode 100755 login/app/sprinkles/core/templates/modals/modal.html.twig
create mode 100755 login/app/sprinkles/core/templates/navigation/breadcrumb.html.twig
create mode 100755 login/app/sprinkles/core/templates/navigation/main-nav.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/about.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/abstract/base.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/abstract/default.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/abstract/error.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/error/400.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/error/404.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/error/config-errors.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/index.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/legal.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/alerts.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/analytics.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/config.js.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/favicons.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/footer.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/legal.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/page.js.twig
create mode 100755 login/app/sprinkles/core/templates/pages/partials/privacy.html.twig
create mode 100755 login/app/sprinkles/core/templates/pages/privacy.html.twig
create mode 100755 login/app/sprinkles/core/templates/tables/table-paginated.html.twig
create mode 100755 login/app/sprinkles/core/templates/tables/table-tool-menu.html.twig
create mode 100755 login/app/sprinkles/core/tests/Integration/DatabaseTests.php
create mode 100755 login/app/sprinkles/core/tests/Unit/BelongsToManyThroughTest.php
create mode 100755 login/app/sprinkles/core/tests/Unit/DatabaseSyncableTest.php
create mode 100755 login/app/sprinkles/core/tests/Unit/SprunjeTest.php
create mode 100755 login/app/system/Bakery/Bakery.php
create mode 100755 login/app/system/Bakery/BaseCommand.php
create mode 100755 login/app/system/Bakery/Command/Bake.php
create mode 100755 login/app/system/Bakery/Command/BuildAssets.php
create mode 100755 login/app/system/Bakery/Command/ClearCache.php
create mode 100755 login/app/system/Bakery/Command/Debug.php
create mode 100755 login/app/system/Bakery/Command/Migrate.php
create mode 100755 login/app/system/Bakery/Command/MigrateRefresh.php
create mode 100755 login/app/system/Bakery/Command/MigrateReset.php
create mode 100755 login/app/system/Bakery/Command/MigrateRollback.php
create mode 100755 login/app/system/Bakery/Command/Setup.php
create mode 100755 login/app/system/Bakery/Command/Test.php
create mode 100755 login/app/system/Bakery/DatabaseTest.php
create mode 100755 login/app/system/Bakery/Migration.php
create mode 100755 login/app/system/Bakery/Migrator.php
create mode 100755 login/app/system/Database/Migrations/v410/MigrationTable.php
create mode 100755 login/app/system/Database/Model/Migrations.php
create mode 100755 login/app/system/Facade.php
create mode 100755 login/app/system/ServicesProvider.php
create mode 100755 login/app/system/SlimAppEvent.php
create mode 100755 login/app/system/Sprinkle/Sprinkle.php
create mode 100755 login/app/system/Sprinkle/SprinkleManager.php
create mode 100755 login/app/system/UserFrosting.php
create mode 100755 login/app/tests/DatabaseTransactions.php
create mode 100755 login/app/tests/TestCase.php
create mode 100755 login/app/tests/Unit/ExampleTest.php
create mode 100755 login/bakery
create mode 100755 login/build/before_install.sh
create mode 100755 login/build/gulpfile.js
create mode 100755 login/build/package-lock.json
create mode 100755 login/build/package.json
create mode 100755 login/composer.json
create mode 100755 login/docker-compose.yml
create mode 100755 login/docker/README.md
create mode 100755 login/docker/nginx/Dockerfile
create mode 100755 login/docker/nginx/default.conf
create mode 100755 login/docker/node/Dockerfile
create mode 100755 login/docker/php/Dockerfile
create mode 100755 login/phpunit.xml
create mode 100755 login/public/.htaccess
create mode 100755 login/public/index.php
create mode 100755 login/screenshots/login.png
create mode 100755 login/screenshots/permissions.png
create mode 100755 login/screenshots/users.png
create mode 100755 login/sponsors/nextgi.png
create mode 100755 login/sponsors/usor.png
create mode 100755 login/webserver-configs/htaccess.txt
create mode 100755 login/webserver-configs/nginx.conf
create mode 100755 login/webserver-configs/web.config
diff --git a/.gitignore b/.gitignore
index 65c0322..cbc6077 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
assets/php/DataBaseConf.php
-login/_meta/*
login/app/.env
login/composer.lock
login/composer.phar
diff --git a/login b/login
deleted file mode 160000
index c78d17f..0000000
--- a/login
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c78d17f4f439711af7a78c5ea9823a5ba7f995a0
diff --git a/login/.github/CONTRIBUTING.md b/login/.github/CONTRIBUTING.md
new file mode 100755
index 0000000..99b92e0
--- /dev/null
+++ b/login/.github/CONTRIBUTING.md
@@ -0,0 +1,76 @@
+# Guidelines for Getting Help with UserFrosting
+
+**Before** you open a new issue or ask a question in chat, you **must** read these guidelines. If it is evident from your issue that you failed to research your question properly, your issue may be closed without being answered.
+
+## Troubleshooting
+
+There are a few common stumbling blocks that new users face when setting up UserFrosting for the first time. If you are new to the current version of UserFrosting, please first look at the [basic requirements and installation instructions](https://learn.userfrosting.com/basics/requirements/basic-stack).
+
+If you don't find what you're looking for in the troubleshooting page, then please check the [wiki](https://github.com/userfrosting/UserFrosting/wiki) and [existing issues](https://github.com/alexweissman/UserFrosting/issues?utf8=%E2%9C%93&q=is%3Aissue), both opened and closed. Your question may have already been asked and answered before!
+
+You can also search for help on Stack Overflow or in our [Forums](https://forums.userfrosting.com/). In addition to the tags for the components that UF builds upon, such as [Slim](http://stackoverflow.com/questions/tagged/slim), [Twig](http://stackoverflow.com/questions/tagged/twig), [Eloquent](http://stackoverflow.com/questions/tagged/eloquent), [jQuery Validate](http://stackoverflow.com/questions/tagged/jquery-validate), [Select2](http://stackoverflow.com/questions/tagged/jquery-select2), there is now a [UserFrosting tag](http://stackoverflow.com/questions/tagged/userfrosting) as well.
+
+There are also tags for the utilities upon which UserFrosting depends, such as [Composer](http://stackoverflow.com/questions/tagged/composer-php) and [Git](http://stackoverflow.com/questions/tagged/git).
+
+## Asking for Help
+
+In general, the Github issue tracker should only be used for bug reports and feature requests. If you're just having trouble getting something to work, you should ask on Stack Overflow or in our [Forums](https://forums.userfrosting.com/) instead. Tag your question with the `userfrosting` tag, and optionally with any tags specific to the relevant underlying technologies, such as `slim`, `twig`, `eloquent`, `composer`, etc. You should also mention the version of UserFrosting that you are using.
+
+After posting a question on Stack Overflow or in our [Forums](https://forums.userfrosting.com/), please [link to it in chat](https://chat.userfrosting.com). This will ensure that more people see it, and provide a place where we can discuss and help clarify your question.
+
+On Github, Chat, and Stack Overflow, please keep in mind the following:
+
+1. Remember that courtesy and proper grammar go a long way. Please take the time to craft a **precise, polite issue**. We will do our best to help, but remember that this is an open-source project - none of us are getting paid a salary to develop this project, or act as your personal support hotline :wink:
+
+2. Report any errors in detail. Vague issues like "it doesn't work when I do this" are not helpful. Show that you have put some effort into identifying the cause of the error.
+
+3. There are three main places where you may find error messages:
+
+- Backend (PHP-related) fatal errors: in your PHP error log. This is usually a file called `php_error_log` or something like that. In XAMPP, the default location of this file is `XAMPP/xamppfiles/logs/`. For other web hosting platforms, please consult the documentation or do a quick Google search (i.e. "where is the php error log in _____"). Some web hosts may provide a special interface for accessing the php error log, through ssh, cpanel, etc. Please ask them directly for help with this.
+
+- Non-fatal PHP errors will be logged in your UserFrosting error log. Look for your `app/logs/errors.log` file.
+
+- Frontend (Javascript-related) errors: in your browser's Javascript console. See [this guide](https://learn.userfrosting.com/background/client-side) to using your browser console.
+
+You should also try testing your code in a local development environment, to separate **code-related** issues from **server** issues. In general, we recommend that you install a local development server on your computer, rather than [testing your code directly on the production server](https://pbs.twimg.com/media/BxfENwpIYAAcHqQ.png). This means you can test your code directly on your own computer, making development faster and without the risk of exposing sensitive information to the public. We recommend installing [XAMPP](https://www.apachefriends.org) if you don't already have a local server set up.
+
+## Contributing to the Codebase
+
+We welcome your technical expertise! But first, please join us in [chat](https://chat.userfrosting.com) to discuss your proposed changes/fixes/enhancements before you get started. At least one member of our development team will usually be around.
+
+Please also be sure to read our [style guidelines](../STYLE-GUIDE.md).
+
+When it's time to integrate changes, our git flow more or less follows http://nvie.com/posts/a-successful-git-branching-model/.
+
+### Branches
+
+- `master`: The current release or release candidate. Always numbered as `major.minor.revision`, possibly with an `-alpha` or `-beta` extension as well.
+- `develop`: During alpha/beta, contains major changes to a release candidate. After beta, contains breaking changes that will need to wait for the next version to be integrated. Always numbered as `major.minor.x`, possibly with an `-alpha` or `-beta` extension as well.
+
+### Changes
+
+#### Hotfixes
+
+Hotfixes should be created in a separate branch, and then merged into both **master** and **develop**.
+
+#### Features
+
+New features that introduce some breaking changes should be created in a separate branch. When they are ready, they can be merged into `develop`.
+
+### Releases
+
+After every release, the `master` branch (and possibly `develop`, for minor/major releases) should immediately be version-bumped. That way, new changes can be accumulated until the next release.
+
+When a new version is created, the version number need to be changed in `app/define.php`. `CHANGELOG.md` should also be updated and the associated tag should be created on Github.
+
+#### Alpha/beta releases
+
+During alpha/beta, a release candidate sits on the `master` branch. Minor improvements should be treated as hotfixes, while major changes should be treated as features. In alpha/beta, major changes can still be integrated into `master` from `develop`. However, this should bump the revision number instead of the minor/major number.
+
+## Building the API documentation
+
+To build the API documentation, install [ApiGen](http://www.apigen.org/) globally and then run:
+
+`apigen generate --source UserFrosting/app,userfrosting-assets/src,userfrosting-config/Config,userfrosting-fortress/Fortress,userfrosting-i18n/I18n,userfrosting-session/Session,userfrosting-support/Support --destination userfrosting-api --exclude *vendor*,*_meta* --template-theme "bootstrap"`
+
+from inside your dev directory.
diff --git a/login/.github/ISSUE_TEMPLATE.md b/login/.github/ISSUE_TEMPLATE.md
new file mode 100755
index 0000000..e46fc9f
--- /dev/null
+++ b/login/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/login/.travis.yml b/login/.travis.yml
new file mode 100755
index 0000000..4b516f7
--- /dev/null
+++ b/login/.travis.yml
@@ -0,0 +1,36 @@
+sudo: false
+dist: trusty
+language: php
+
+services:
+ - mysql
+ - postgresql
+
+php:
+ - 5.6
+ - 7
+ - 7.1
+
+env:
+ matrix:
+ - DB=mysql
+ - DB=sqlite
+ - DB=pgsql
+
+before_install:
+ # copy sprinkles.json
+ - cp app/sprinkles.example.json app/sprinkles.json
+ # set up db
+ - bash build/before_install.sh $DB
+
+before_script:
+ # install deps and UF
+ - composer install
+ - php bakery migrate
+
+script:
+ # run unit tests
+ - composer test
+
+after_failure:
+ - cat app/log/userfrosting.log
diff --git a/login/CHANGELOG.md b/login/CHANGELOG.md
new file mode 100755
index 0000000..44f7530
--- /dev/null
+++ b/login/CHANGELOG.md
@@ -0,0 +1,553 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## v4.1.17-alpha
+- Lock `gulp-uf-bundle-assets` at v2.28.0 until Silic0nS0ldier/gulp-uf-bundle-assets#5 is resolved (see #859)
+- Add missing getInfo methods for GroupController and RoleController (#837)
+
+## v4.1.16-alpha
+- Fix for `merge` bundling rule (#660)
+- Fix for undefined variable exception under strict mode in `ufAlerts` (#809)
+- Fix for site cache reset upon login (#828)
+- Changed global cache tag to proper prefix
+- Fix broken alert message in registration process (#843)
+- Add partial Turkish translation
+
+## v4.1.15-alpha
+- Refactor `Password` into a instantiable `Hasher` class, service, and `Password` facade (#827)
+- Change default hash cost back to 10 and fix legacy hash detection issue
+
+## v4.1.14-alpha
+- Fix issue with scopes being applied twice in `Unique::getPaginatedQuery` (https://github.com/userfrosting/extend-user/issues/2)
+- Update Bower dependencies in core Sprinkle
+- Refactor the `Password` class to use `hash_equals` for legacy passwords (prevent timing-based attacks) and factor out the default cost (#814)
+- Check if `require_email_verification` is set in `Authenticator` and sign-in page (#815)
+- Factor out hardcoded `sprinkles.json` filename (partially addresses #813)
+- Add Farsi translations (#816)
+- `ufTable`: Make `tableElement` configurable, and check for existence (#824)
+- Put AdminLTE menu toggle button back (Revert e8a26fb and part of a46205f)
+
+## v4.1.13-alpha
+- `ufTable`: Implement `rowTemplate` for customizing how rows are rendered (#787)
+- `ufTable`: Support for passing callbacks for column templates instead of Handlebars templates
+- Remove explicit references to `id` columns (#803)
+- Fix Docker
+- Append full name to User API
+
+## v4.1.12-alpha
+- Separate out user action column from user status column
+- Improve table row menus in mobile views (#724)
+- Hide side menu toggle button in desktop sizes
+- Add chevron to user menu
+- Change "remember me" text
+- Improve table tool buttons
+- Twig extensions now implement `Twig_Extension_GlobalsInterface` as required by https://twig.symfony.com/doc/2.x/advanced.html#id1 (#788)
+- Display element based on permissions for group list/info pages
+- Factor the admin user creation out of migrations and into its own Bakery command (See #778)
+- Bakery `clear-cache` command now clears Twig and router cache (Fix #750)
+- Add Russian translations
+- Add Travis for automated test/build on push
+
+## v4.1.11-alpha
+- Updated `composer/installers` dependency.
+- Patch `composer.json` to fix `illuminate/*` dependencies at 5.4 for now
+
+## v4.1.10-alpha
+- Add support for PHP7 runtime errors to be handled in the same way as Exceptions
+- Implement NotFoundExceptionHandler and pass through all NotFoundExceptions to this handler.
+- Implement `redirect.onAlreadyLoggedIn` service (fixes #680)
+- Deprecate `determineRedirectOnLogin` and replace with `redirect.onLogin` service
+- Fix some PSR-2 compliance issues
+
+## v4.1.9-alpha
+- Fixes #780, and more efficient way to collect ids in Unique::getPaginatedQuery
+- Show "user deleted" in activities table (#782)
+- Patched version of `widget-sort2Hash.js` to prevent writing extraneous browser history entries (#712)
+- Improve handling of fatal/parse errors
+
+## v4.1.8-alpha
+- Normalize paths to always have a leading slash when comparing against the CSRF blacklist (#775) (possible breaking change for some environments - please see updated docs at https://learn.userfrosting.com/routes-and-controllers/client-input/csrf-guard#blacklisting-routes)
+- Set `display_errors` to `true` for development configs (#762), move php settings into a common `php` subkey in config files
+- `ShutdownHandler` no longer responsible for logging fatal errors
+- Set up PHP config values in `Core.php` instead of inside the `config` service definition.
+- Reimplement `Builder::exclude` to maintain a list of excluded columns, and then automatically update list of columns to fetch in `get()`
+- Deprecate `Model::queryBuilder` and `Model::export`
+- Update nginx config file from spdy to http2
+- Add Pagespeed block (commented out) to nginx config file
+- Make fpm-php7.0 the default CGI nginx config file
+
+## v4.1.7-alpha
+- Add the `withTernary` method to the `Unique` trait to allow loading nested child models on ternary relationships.
+- Add skip, take, limit, offset and deprecate withLimit and withOffset in `Unique` trait
+- Support for `withPivot` on `Unique` relationships (in tertiary models)
+- Factor out common code from `PermissionUserSprunje` into `UserSprunje`
+- Rework internals of `Sprunje` to make it more testable. Filters, sorts, and paginations are now applied to a clone of the original queriable object. Deprecated `getResults` and added `getArray` and `getModels`. Result keys can now be customized.
+- Table of user permissions on user info page
+- Simplify by combining `permission-users.html.twig` into options on `users.html.twig`
+- Add Chinese translations
+- Deprecate User::exists() (#771)
+- Add 'password' to hidden fields for User model
+- Replace hardcoded `Builder` with classMapper reference
+
+## v4.1.6-alpha
+- Fix missing permission check when `uri_account_settings` is not in role (#768)
+- Add `getLastRow` method and `transformDropdownSelection` option to `ufCollection`
+- Fix missing slug for permissions in "manage permissions" dropdown
+- Add "manage permissions" to role page menu
+- Factor out custom relation methods into `HasRelationships` trait on `Model`
+- Add `withoutGlobalScopes` to `Syncable::sync`
+- Add option to use `forceCreate` in `Syncable::sync`
+- Add option to use custom key in `Syncable::sync`
+- Complete redesign of `BelongsToManyThrough` - possible BC for a few people, as you now need to load the "via" models explicitly using `withVia`. This fixes a lot of issues with `BelongsToManyThrough`.
+- Deprecate `BelongsToManyConstrained`
+- Add `MorphToManyUnique`
+- Integration tests now use an in-memory sqlite database (`test_integration`) by default
+
+## v4.1.5-alpha
+- Spanish language support (#770)
+- Show current filter in select-menu filters (#744)
+- Cursor styling for ufCopy
+- Transition overlay for ufTables
+- Minor fix to ufTable.cleanHash
+- Correctly target pager container in Tablesorter options
+- Add table of users to role pages
+- Fix issue with links in permission users table
+
+## v4.1.4-alpha
+- Permissions rows get duplicated when upgrading from 4.0 to 4.1 (fix #759)
+- Fix migrate:rollback not running down the migration in the correct order
+- Updated type in `composer.json` for default sprinkles
+- Added missing french translations & more default validation messages
+- Bump Fortress version (fix #766)
+- Support SQLite in Bakery setup
+- Fix for PostgreSQL charset in Bakery (#745)
+
+## v4.1.3-alpha
+- Add Italian translations
+- Add `data-priority` attributes to built-in tables (#752)
+- Use `this.slug` to avoid conflict with helper names (#748)
+- Add block `table_search` to `table-paginated.html.twig`
+
+## v4.1.2-alpha
+- Remove call to setFilters that was causing problems with pagination (#688)
+- Update German translations and factor out some hardcoded text (#725)
+- Update French translations
+- Update Arabic translations (#733, #734, #735)
+
+## v4.1.1-alpha
+- Fixed missing template in ExceptionHandler and `notFoundHandler`
+- Migration rollback will throw a warning if a class is not found instead of aborting
+- Temporary fix for trouble with `npm install` in Windows (#742)
+
+## v4.1.0-alpha
+- Switch from pagination "plugin" to "widget" for Tablesorter. Allows us to update to the latest version of TS (fix #688, #715)
+- Implement `WhoopsRenderer` for pretty debug pages. See (#674)
+- Refactor error handling. Move responsibility for displayErrorDetails to handlers, and factor our ErrorRenderers. Addresses (#702)
+- Move `composer.json` to root directory to allow installing UF via composer create-project
+- Move `sprinkles.json` to app directory to make it easier to find
+- Eliminate the `root` theme Sprinkle. Custom styling for the root user is now handled with some Twig logic in the `admin` Sprinkle (#726)
+- Rename bundle.config.json -> asset-bundles.json (#726)
+- Reorganize assets (#726)
+- Heavily reorganize templates (#726)
+- Move request schema from `schema/` to `schema/requests/` (#726)
+- Factor out "system" classes from core Sprinkle
+- Refactor overall application lifecycle; move main lifecycle into UserFrosting\System\UserFrosting
+- SprinkleManager now better focused on a single responsibility
+- Sprinkle initializer classes now use events to hook into application lifecycle
+- Support for allowing Sprinkles to register middleware (#617)
+- Automatically load Sprinkle service providers (see #636)
+- Get rid of "implicit loading" for core Sprinkle - core is now just an ordinary Sprinkle like any other.
+- The `sprinkles://` stream now represents a virtual filesystem for the root directory of each loaded sprinkle, rather than the `sprinkles/` directory itself.
+- Separate out `localePathBuilder` from the `translator` service. Makes it easier to add/remove paths before actually loading the translations.
+- Only present locale options with non-null names.
+- Rebased ufTable and ufModal with new jQuery plugin template. (part of #646)
+- Removed the search bar from the Dashboard layout
+- Added Tablesorter pagination translation
+- New Translator Facade
+- New CLI tool (Bakery).
+- New migration system based on bakery CLI
+- Listable sprunjing
+- Refactor groups and user routes (Fix #721)
+- Added the `config` alert stream to save ufAlerts to the cache instead of sessions. Fix #633. The old `session` is still the default alertStream in 4.1.
+- Added support for the Redis cache driver and refactored the cache config values.
+- Added user and session cache.
+- Common log file for db queries, auth checks, smtp, errors, and debug messages (#709).
+- Use YAML as default format for request schema (#690)
+
+See [http://learn.userfrosting.com/upgrading/40-to-41](Upgrading 4.0.x to 4.1.x documentation) for complete list of changes and breaking changes.
+
+## v4.0.24-alpha
+- Fixes to nginx config file, and add location block for LE acme challenge
+- Fix JS errors when `#alerts-page` is not present on a page
+- Fix hardcoded User class in AdminController (#753)
+- Update message PASSWORD.FORGET.REQUEST_SENT (#749)
+
+## v4.0.23-alpha
+- Set module dependency versions to ~4.0.0 instead of ^4.0.0 (since 4.1.x will introduce breaking changes)
+- Fix bug in ufCollection
+
+## v4.0.22-alpha
+- Fix issue where 'Change User Password' popup form couldn't handle specifying a new password.
+- Display message when there are no results in `ufTable`
+
+## v4.0.21-alpha
+- Implement reflow and column selector for tables (#670)
+- Overhauled ufAlerts, improving efficiency, reliability, and fixed a discovered edge case that caused `render` to never complete. (part of #646)
+- ufAlerts will only auto-scroll when outside the viewport (even if only partially). Can be overriden with `scrollWhenVisible: true`. (#714)
+- Rebased ufCollection, and ufForm with new jQuery plugin template. (part of #646)
+- Misc UI update
+- Added Twig blocks
+- Fix issue with duplicate query logs when using multiple databases
+
+## v4.0.20-alpha
+- Remove pivot columns from pagination subquery in BelongsToManyThrough, to deal with MySQL's `only_full_group_by` warning
+
+## v4.0.19-alpha
+- Explicit column names in new user permissions relations
+
+## v4.0.18-alpha
+- Permission info page (#638)
+- New custom relationships 'BelongsToManyThrough', 'BelongsToManyUnique', 'BelongsToManyConstrained', 'HasManySyncable', 'MorphManySyncable'
+- Change implementation of User::permissions() to use BelongsToManyThrough
+- New ufForm options: setting reqParams, encType, submittingText
+- ufCollection now triggers a check for virgin rows when _any_ control is touched
+- Fix issue with Sprunje when generating CSV with empty child collections (#697)
+- Authorizer now correctly interprets string literals (#482)
+- Authorizer now correctly interprets numeric types in access conditions. **Caution**: this causes the `equals()` callback to return true in situations where it would have (incorrectly) returned false before. For example, `equals(self.group_id,2)` would have returned false for users in group 2, because it was interpreting `2` as a string and then performing its strict comparison. It now (correctly) returns true. Notice that `equals(self.group_id,'2')`, on the other hand, will now return `false`.
+- User object caches permissions loaded from DB to reduce number of queries (#612)
+- Type declarations in authorization classes (#652)
+- Fix issue with Twig debug (#356)
+- Show disabled/unactivated badges on user info page
+
+## v4.0.17-alpha
+- Add IIS config file (#371)
+- New ufCollection now supports free text input mode
+- New design and layout for user, group, and role summary boxes (also fixes #703)
+- Registration page returns 404 when registration disabled (#705)
+
+## v4.0.16-alpha
+- Add Docker configuration files
+- Begin work on Bakery, the command-line debug tool
+- Lock version of tablesorter due to breaking changes
+- Fix bugs in GroupController and RoleController
+- Fix bug in URLs for redirect-on-login
+- Added UTF-8 as default mail charset
+
+## v4.0.15-alpha
+- Prevent mixed content on demo pages
+- Fixed some missing translations
+- Fixed error in ufAlerts push method
+- Fixed usage of hard coded path
+- Fixed default OS option in migration script
+- Prevents empty locale's from displaying as empty options in profile form
+- Unignore .gitkeeps of directories that need to exist
+
+## v4.0.14-alpha
+- Fix ajax.delay in ufCollection
+- Fix missing translations
+- Minor fix in French translation
+- Fix alert margin when displayed inside a modal
+
+## v4.0.13-alpha
+- Update to RememberMe 2.0 (https://github.com/userfrosting/UserFrosting/issues/635)
+- Remove database checks, as they are apparently no longer needed (https://github.com/userfrosting/UserFrosting/issues/655)
+- Bump dependencies
+
+## v4.0.12-alpha
+- Separate out the registration and sign-in pages (https://github.com/userfrosting/UserFrosting/issues/657) **BC**
+- Slightly change behavior of form validation icons
+- Sprunje input validation (https://github.com/userfrosting/UserFrosting/issues/640)
+- Sprunje sort/filter fields now must be explicitly listed in a whitelist (https://github.com/userfrosting/UserFrosting/issues/640) **BC**
+- Errors from tablesorter now get displayed
+- Support for OR expressions using `||` in Sprunje filters (https://github.com/userfrosting/UserFrosting/issues/647)
+
+## v4.0.11-alpha
+- Fix [#663](https://github.com/userfrosting/UserFrosting/issues/663)
+- Adding more Twig `blocks`
+- ufAlerts now scroll to alert location, if and only if alerts are output.
+- Updated Dutch locale
+- Minor update in French locale
+- Added comments in `.env.example`
+
+## v4.0.10-alpha
+- Move suggestion button outta-da-way
+- Add email to registration success message
+- Separate out some page content into smaller blocks
+- Factor out select2 options in ufCollection, into the 'dropdown' key so that any select2 option can be set
+
+## v4.0.9-alpha
+- Oops, `exists` needs to be static
+
+## v4.0.8-alpha
+- Autogenerate and suggestion features for usernames during account registration (partially addresses https://github.com/userfrosting/UserFrosting/issues/569)
+- Restrict username characters to a-z0-9.-_
+- Require first name by default
+- Throttle registration attempts
+- Implement User::exists method
+- keyupDelay option in ufForm
+- More logging of group and role CRUD
+- Implement extra:// stream
+- Lots of missing translation keys
+
+## v4.0.7-alpha
+- Separate "profile settings" from "account settings"
+
+## v4.0.6-alpha
+- Fix throttling issue #656
+- Other miscellaneous fixes
+
+## v4.0.5-alpha
+- Allow nulling out of throttle rules (to disable)
+- Disable Google Analytics by default (but enabled in production)
+- Other miscellaneous fixes
+
+## v4.0.4-alpha
+- UfAlert style customization (See [#634](https://github.com/userfrosting/UserFrosting/issues/634))
+- Translation function can now display raw placeholder using the `|raw` filter in the placeholder name. Other Twig filters are also avaiable. Requires latest version of the [i18n](https://github.com/userfrosting/i18n) component (See [#621](https://github.com/userfrosting/UserFrosting/issues/621)).
+- Fix the "Root account" message breaking the UI on smaller screens (See [#641](https://github.com/userfrosting/UserFrosting/issues/641)) - Thanks @brunomnsilva !
+- Added `DB_DRIVER` and `DB_PORT` as environment variables to allow better out-of-box database configuration support, and to provide additional protection by obscurity.
+- Normalised default values to `null` for environment variables in configuration.
+- Added `getCallbacks` public method to `AuthorizationManager` to enable drop-in extensions to `AuthorizationManager`.
+- Fixed broken links in generated asset bundles.
+- Introduced `clean` gulp task to act as a shotcut for removing all frontend vendor packages, all generated asset bundles, and copied assets. Accessible via `npm run uf-clean`.
+- Merged `copy` task with `bundle-build`.
+- Fixed missing translations
+- Added Thai translation - Thanks @popiazaza !
+
+## v4.0.3-alpha
+- Add config file for nginx (https://github.com/userfrosting/UserFrosting/issues/373)
+- Add Portuguese translations (thanks to @brunomnsilva!)
+- Add Arabic (MSA) translations (thanks to @abdullah.seba!)
+- Add Dispatcher to db service to allow registering model events.
+- Specify foreign keys explicitly in all relationships.
+- Use classMapper for admin Sprunjes.
+
+## v4.0.2-alpha
+- Specify foreign key explicitly in `User::activities()` relationship.
+- Database checks in installer and Authenticator now respect custom database ports. (See [#628](https://github.com/userfrosting/UserFrosting/issues/628))
+- Fixed edge case where `5%C` would appear in generated urls.
+- Improved stability and added php version check in `migrations/intall.php`
+- Update ClassMapper to throw exception when class is not found
+- Fix minor errors in French locale
+- Fix translation error on the Legal page
+
+## v4.0.1-alpha
+- Bump min version of PHP to 5.6
+- Added German translation (See [#625](https://github.com/userfrosting/UserFrosting/issues/625)) - Thanks @X-Anonymous-Y
+- Improved Gulp Build task
+- Remove site-dev from example sprinkles.json
+- Fix some styling issues on the Dashboard and footer
+- Display group link in menu for group admins
+- Keep dashboard sidebar collapsed across page load (See [#616](https://github.com/userfrosting/UserFrosting/issues/616))
+- Fixed missing translation keys inside Handlebar tables (See [#624](https://github.com/userfrosting/UserFrosting/issues/624))
+- Admin panel link style in main dropdown menu (See [#627](https://github.com/userfrosting/UserFrosting/issues/627))
+- Implement AuthGuard middleware
+- Handling of redirect after login (See [#627#issuecomment-275607492](https://github.com/userfrosting/UserFrosting/issues/627#issuecomment-275607492))
+- Directly check database in installer using PDO
+- Refactor installer and how version are displayed in system info panel. Added notice when a migration is available for a sprinkle
+- Etc.
+
+## v4.0.0-alpha
+
+**Initial release of UserFrosting V4**
+
+- The [Sprinkle](https://learn.userfrosting.com/sprinkles) system, which keeps your code completely separate from the core UF codebase;
+- We're upgraded from Slim 2 to Slim 3, which is significantly different;
+- Completely redesigned [database structure](https://learn.userfrosting.com/database/default-tables);
+- Initialization is now achieved through [services](https://learn.userfrosting.com/services), with the Pimple dependency injection container;
+- [Composer](https://learn.userfrosting.com/installation/requirements/essential-tools-for-php#composer) is now a mandatory part of the installation process;
+- [Bower](https://learn.userfrosting.com/sprinkles/contents#-bower-json) is now used to install third-party client-side assets (Javascript/CSS packages);
+- "Groups" and "Primary Group" have been replaced by "Roles" and "Group", respectively;
+- Tables no longer need to be "registered" in any kind of initialization routine. Simply set the table names directly in your data models;
+- Twig templates have been [reorganized and redesigned](https://learn.userfrosting.com/templating-with-twig/sprinkle-templates);
+- SB Admin has been removed and we now use the [AdminLTE](https://adminlte.io/) front-end theme;
+- Client-side code has been heavily refactored into reusable [components](https://learn.userfrosting.com/client-side-code/components).
+
+## v0.3.1.23
+- Also fix the `occurred_at` timestamp in the `user_event` table to allow null, for newer versions of MySQL that don't allow a zero date (see #605).
+
+## v0.3.1.22
+- Use `nullableTimestamps` instead of `timestamps` in installer, to prevent conflict with MySQL modes 'NO_ZERO_IN_DATE' and 'NO_ZERO_DATE'.
+
+## v0.3.1.21
+- Use Laravel's Schema interface to create tables and default rows, instead of constructing them with SQL
+
+## v0.3.1.20
+- Added `pushAlert()`,`clearAlerts()` in `public/js/userfrosting.js` and updated `flashAlerts()`
+- Revert changes to User::fresh() but leave comment regarding upgrading Eloquent
+
+## v0.3.1.19
+- Fix some minor error screen layout issues
+- Make User::fresh() compatible with Eloquent\Model v5.2.40+
+- Update composer require to allow for Fortress 1.x bugfixes
+- Allow database port definitions in config-userfrosting.php
+- Fix fatal error when evaluateCondition is called before the router populates current route information
+
+## v0.3.1.18
+- Add check for logging being enabled but log file not existing yet
+
+## v0.3.1.17
+- Fix occasional bug when end-of-file is reached on log file before requested number of lines is reached
+- Roll back database connection checking to fix installer routines (frostbitten)
+- UI fixes for smaller screens (frostbitten)
+- Update Gitter references to Rocket.chat
+- Clarify hotfix branch procedure for contributions
+
+## v0.3.1.16
+- Fix comment reference to \Fortress\JqueryValidationAdaptor
+- CONTRIBUTING.md - Add note about proper Pull Requests
+- French language file fixes (#565) (lcharette)
+- Add HTTP status codes to 404 errors and database errors (frostbitten)
+- Change database errors to use BaseController instead of DatabaseController (frostbitten)
+
+## v0.3.1.15
+- Fix unattached submitHandler bug in Group and Auth edit interfaces (#465)
+- Remove references to nonexistent `formUserView` and `formGroupView` (#478)
+- Gracefully handle session destruction due to missing or disabled accounts (#510)
+- Add `attributeExists` and `relationExists` for models (#520)
+
+## v0.3.1.14
+- Stop reading entire log files to avoid out-of-memory errors (#497)
+- Deploy [league/csv](https://github.com/thephpleague/csv) to properly generate CSV files (#557)
+- Fix typos in language files
+
+## v0.3.1.13
+- Bump dependencies
+- userfrosting/fortress now has a release version
+
+## v0.3.1.12
+- Add sendmail support in Notification class
+- Fix problem with strict comparison in Handlebars templates and inconsistent data types among different database technologies
+- Override paths to font files for Bootstrap Glyphicons to support the UserFrosting directory structure
+- Added missing lines of Thai language (popiazaza)
+- Fix a vulnerability where users still logged in wouldn't automatically be logged out if they were disabled
+- Add option for HTTPS in `.htaccess`, commented out by default
+- Minor syntax fixes in `public/js/userfrosting.js`, `widget-auth.js`, `widget-groups.js`, and `widget-users.js`
+
+## v0.3.1.11
+- Composer can now include composer.json files from plugin folders (added "wikimedia/composer-merge-plugin" to composer)
+
+## v0.3.1.10
+- Select correct versions (PHP 5.x compatible) of packages in `composer.json`
+- Turkish language translation
+- Return `User` object created in `AccountController::register`
+
+## v0.3.1.9
+- Revert to loose comparison for `user_id`s because of issues with Ubuntu's PDO driver (see http://stackoverflow.com/questions/5323146/mysql-integer-field-is-returned-as-string-in-php#comment41836471_5323169)
+
+## v0.3.1.8
+- Finish replacing all usages of `*Loader` classes with Eloquent syntax
+- Installer warning for missing `imagepng`
+- Fix bug in CSV generation for user table
+
+## v0.3.1.7
+- Change "default theme" to "guest theme" and fix loading issues (#463). What used to be called "default theme" is now base theme, i.e. the theme to fall back to when a template file cannot be found in the current theme (user group or guest theme)
+- New public template for "nyx" theme
+- Remove trailing slash from configuration JS/CSS paths to make uniform with site.uri.public
+- Make routes for config.js and theme.css dynamically generated from configuration variables (#461)
+- Make cookie name for "remember me" use session name
+- Fix potential bug in configuration user_id's for guest, master accounts
+
+## v0.3.1.6
+- Fix exception-handling for mail server errors
+- Notify if account creation was successful, even if mail server failed.
+
+## v0.3.1.5
+- Add Romanian translation
+- Upgrade Tablesorter and pretty URLs for searched/sorted/paginated tables
+- Fix bug in default value for user `secret_token`
+
+## v0.3.1.4
+- .htaccess redirect trailing slash: change to only redirect GET requests
+- Natural sort order in API
+- Fix bug in table pagination
+- Fix bug in loading user primary group properties as user properties
+- Fix mailto link bug in tables
+- Warn if config file missing (#445)
+- Fix dutch error (#447)
+
+## v0.3.1.3
+- Implement CSV download feature
+
+## v0.3.1.2
+- Implement `no_leading_whitespace` and `no_trailing_whitespace` rules
+
+## v0.3.1
+- Improved initialization routine as middleware
+- Implemented "remember me" for persistent sessions - see https://github.com/gbirke/rememberme
+- Converted page templates to inheritance architecture, using Twig `extends`
+- Start using the `.twig` extension for template files
+- All content is now part of a theme, and site can be configured so that one theme is the default theme for unauthenticated users
+- User session stored via `user_id`, rather than the entire User object
+- Data model is now built on Eloquent, instead of in-house
+- Cleaned up some of the per-page Javascript, refactoring repetitive code
+- Implement server-side pagination
+- Upgrade to Tablesorter v2.23.4
+- Switch from DateJS to momentjs
+- Switch to jQueryValidation from FormValidation
+- Implement basic interface for modifying group authorization rules
+- User events - timestamps for things like sign-in, sign-up, password reset, etc are now stored in a `user_event` table
+- Wrapper class Notification for sending emails, other notifications to users
+- Remove username requirement for password reset. It is more likely that an attacker would know the user's username, than the user themselves. For the next version, we can try to implement some real multi-factor authentication.
+- When a user creates another user, they don't need to set a password. Instead, an email is sent out to the new user, with a token allowing them to set their own password.
+- Admins can manually generate a password reset request for another user, or directly change the user's password.
+
+## v0.3.0
+- [Autoloading with Composer](https://v3.userfrosting.com/navigating/#composer)
+- [MVC Architecture](https://v3.userfrosting.com/navigating/#structure)
+- [Front Controllers and the Slim Microframework](https://v3.userfrosting.com/navigating/#slim)
+- [Twig - Templating](http://twig.sensiolabs.org/)
+- [Theming](https://v3.userfrosting.com/components/#theming)
+- [Plugins](https://v3.userfrosting.com/components/#plugins)
+
+## v0.2.1
+- Implemented db-driven menu system. Menu items are pulled from the database, and can be modified via plugins.
+- Implemented backend templating of forms and tables via [Bootsole](https://github.com/alexweissman/bootsole).
+
+## v0.2.0 (butterflyknife)
+- Converted all DB calls to PDO.
+- Renamed "permissions" to "groups". Same concept, but using the word "group" suggests that it can be used for more than just access control.
+- Implemented "primary group" membership for users. A user can belong to multiple groups, but only one of those will be their primary group.
+- Implemented DB-driven home pages for groups. Upon login, a user will be redirected to the `home_page` for their primary group.
+- Implemented templated menus. Every group has a corresponding menu template in `models/menu-templates`. Upon login, the menu for a user's primary group is automatically loaded and rendered.
+- Implemented function-level user authorization. Whenever a function in `secure_functions` is called, the `user_action_permits` table is checked to see whether or not that user has access to the function (the `action` column), conditional on the boolean functions specified in the `permits` column.
+- Organized pages into four categories: account pages, API pages, form pages, and public pages. Public pages reside in the root directory and can be accessed by anyone. Account pages are in the `account` directory and are only accessible after logging in. API pages are in the `api` directory, and consist of all the pages that process or fetch data from the DB and interact with the frontend via AJAX/JSON. They are accessible by any logged in user, but will only perform a function if the user is authorized. Form pages are in the `forms` directory, and consist of pages that generate forms (for creating/updating users, groups, etc.)
+- Converted registration page to AJAX.
+- Improved installer with site configuration.
+
+## v0.1.7
+- Page scrolls back to top after AJAX submit.
+- "Website url" is automatically suffixed with "/" if necessary.
+- Fixed bad link to forgot_password.php.
+- Began implementing action authorization scheme.
+
+## v0.1.6
+- Implemented CSRF token checking for creating and updating users
+- Moved much of the nuts and bolts for generating the user-create and user-update forms to the server side, so as to streamline rendering process and require fewer requests by the client (see load_form_user.php)
+- Improved responsive layout for rendering nicely on mobile devices
+
+## v0.1.5
+- More improvements to error-handling/rendering
+- HTTPS/SSL compatible
+- Fixed bug with different table name prefixes
+- Improvements to CSRF tokens
+
+## v0.1.4
+- Updated password hashing from md5 to modern bcrypt (more secure) - thanks to contributor @r3wt
+- Included better functions for sanitizing user input, validating user ip, generating csrf (cross-site request forgery) tokens - thanks to contributor @r3wt
+
+## v0.1.3
+- Root account (user id = 1) : created upon installation, cannot be deleted or disabled.
+- Special color scheme for when logged in as root user.
+- Installer now guides user through creation of root account
+- Moved common JS and CSS includes to "includes.php"
+
+## v0.1.2
+- Improved error and exception handling
+- Added 404 error page
+- Standardized JSON interface for backend scripts
+- Front-end should now be able to catch virtually any backend error and take an appropriate action (instead of white screen of death)
diff --git a/login/LICENSE.md b/login/LICENSE.md
new file mode 100755
index 0000000..476c3ca
--- /dev/null
+++ b/login/LICENSE.md
@@ -0,0 +1,7 @@
+Copyright (c) 2017 by Alexander Weissman (https://alexanderweissman.com)
+
+UserFrosting is 100% free and open-source.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/login/README.md b/login/README.md
new file mode 100755
index 0000000..d136dc8
--- /dev/null
+++ b/login/README.md
@@ -0,0 +1,124 @@
+# UserFrosting 4.1
+
+[https://www.userfrosting.com](https://www.userfrosting.com)
+
+[](https://travis-ci.org/userfrosting/UserFrosting)
+[](https://chat.userfrosting.com/channel/support)
+[](#backers) [](#sponsors)
+
+
+
+
+
+If you simply want to show that you like this project, or want to remember it for later, you should **star**, not **fork**, this repository. Forking is only for when you are ready to create your own copy of the code to work on.
+
+## By [Alex Weissman](https://alexanderweissman.com)
+
+Copyright (c) 2018, free to use in personal and commercial software as per the [license](LICENSE.md).
+
+UserFrosting is a secure, modern user management system written in PHP and built on top of the [Slim Microframework](http://www.slimframework.com/), [Twig](http://twig.sensiolabs.org/) templating engine, and [Eloquent](https://laravel.com/docs/5.4/eloquent#introduction) ORM.
+
+## Features
+
+### User login screen
+
+
+### User management page
+
+
+### Permissions management page
+
+
+## [Demo](https://demo.userfrosting.com)
+
+## Installation
+
+Please see our [installation guide](https://learn.userfrosting.com/installation).
+
+## Troubleshooting
+
+If you are having trouble installing UserFrosting, please [join us in chat](https://chat.userfrosting.com) or try our [forums](https://forums.userfrosting.com).
+
+If you are generally confused about the structure and layout of the code, or it doesn't look like the kind of PHP code that you're used to, please [start from the beginning](https://learn.userfrosting.com/background).
+
+## Mission Objectives
+
+UserFrosting seeks to balance modern programming principles, like DRY and MVC, with a shallow learning curve for new developers. Our goals are to:
+
+- Create a fully-functioning user management script that can be set up in just a few minutes
+- Make it easy for users to quickly adapt the code for their needs
+- Introduce novice developers to best practices such as separation of concerns and DRY programming
+- Introduce novice developers to modern constructs such as front-end controllers, RESTful URLs, namespacing, and object-oriented modeling
+- Build on existing, widely used server- and client-side components
+- Clean, consistent, and well-documented code
+
+## Documentation
+
+### [Learning UserFrosting](https://learn.userfrosting.com)
+
+### [API documentation](http://api.userfrosting.com)
+
+### [Change log](CHANGELOG.md)
+
+## Running tests
+
+Run `php bakery test` from the root project directory. Any tests included in `sprinkles/*/tests` will be run.
+
+## Development Team
+
+### Alexander Weissman
+
+Alex is the founder and co-owner of two companies, one that does [math tutoring at Indiana University](https://bloomingtontutors.com) in Bloomington, IN and another company that does [math tutoring at UMD](https://collegeparktutors.com) in College Park, MD. He is a PhD student in the School of Informatics and Computing at Indiana University.
+
+### Louis Charette
+
+Louis's a civil engineer in Montréal, Québec who also has a passion for coding. He is one of the main contributors for SimpsonsCity.com and likes to share his knowledge by helping others the same way he was helped when he first started coding.
+
+### Jordan Mele
+
+Jordan's a developer at Mayvin Training and a student studying Computer Science at the University of Wollongong. His passion is creating software-based solutions to overcomplicated problems, without taking control away from the user. He's also Australian.
+
+### Sarah Baghdadi
+
+Sarah is UserFrosting's UX specialist and frontend designer. In addition to her work on the UF application itself, she is responsible for the amazing design of https://www.userfrosting.com and https://learn.userfrosting.com.
+
+### Srinivas Nukala
+
+Srinivas's a web applications architect, with a passion for open source technologies. He is experienced in building SaaS (software as a service) web applications and enjoys working on open source projects and contributing to the community. He has a Masters in Computer Science from Pune University, India.
+
+## Contributing
+
+This project exists thanks to all the people who contribute. If you're interested in contributing to the UserFrosting codebase, please see our [contributing guidelines](.github/CONTRIBUTING.md) as well as our [style guidelines](STYLE-GUIDE.md).
+
+
+
+### Thanks to our translators!
+
+- Louis Charette (@lcharette) - French
+- Karuhut Komol (@popiazaza) - Thai
+- Pietro Marangon (@Pe46dro) - Italian
+- Abdullah Seba (@abdullahseba) - Arabic
+- Bruno Silva (@brunomnsilva) - Portuguese
+- @BruceGui - Chinese
+- @kevinrombach - German
+- @rafa31gz - Spanish
+- @splitt3r - German
+- @X-Anonymous-Y - German
+- Dmitriy (@rendername) - Russian
+- Amin Akbari (@aminakbari) - Farsi
+- Dumblledore - Turkish
+
+## Supporting UserFrosting
+
+### Backers
+
+Backers help us continue to develop UserFrosting by pledging a regular monthly contribution of $5 or more. [[Become a backer](https://opencollective.com/userfrosting#contribute)]
+
+
+
+#### Sponsors
+
+Support this project by becoming a sponsor. Sponsors have contributed a total of $500 or more to UserFrosting (either as an ongoing backer or one-time contributions). Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/userfrosting#sponsor)]
+
+[](https://usorgames.com)
+[](https://nextgi.com)
diff --git a/login/STYLE-GUIDE.md b/login/STYLE-GUIDE.md
new file mode 100755
index 0000000..94c10b2
--- /dev/null
+++ b/login/STYLE-GUIDE.md
@@ -0,0 +1,44 @@
+# Style guide for contributing to UserFrosting
+
+## PHP
+
+All PHP contributions must adhere to [PSR-1](http://www.php-fig.org/psr/psr-1/) and [PSR-2](http://www.php-fig.org/psr/psr-2/) specifications.
+
+In addition:
+
+### Documentation
+
+- All documentation blocks must adhere to the [PHPDoc](https://phpdoc.org/) format and syntax.
+- All PHP files MUST contain the following documentation block immediately after the opening `
+
+RewriteEngine On
+
+## Begin - Security
+# Block all direct access to files and folders beginning with a dot
+RewriteRule (^\.|/\.) - [F]
+# Block access to specific files in the root folder
+RewriteRule ^(LICENSE.txt|composer.lock|composer.json|\.htaccess|\.env)$ error [F]
+## End - Security
+
+
diff --git a/login/app/cache/.gitkeep b/login/app/cache/.gitkeep
new file mode 100755
index 0000000..e69de29
diff --git a/login/app/defines.php b/login/app/defines.php
new file mode 100755
index 0000000..d129424
--- /dev/null
+++ b/login/app/defines.php
@@ -0,0 +1,52 @@
+=5.6"
+ },
+ "autoload": {
+ "psr-4": {
+ "UserFrosting\\Sprinkle\\Account\\": "src/"
+ }
+ }
+}
diff --git a/login/app/sprinkles/account/config/default.php b/login/app/sprinkles/account/config/default.php
new file mode 100755
index 0000000..e154643
--- /dev/null
+++ b/login/app/sprinkles/account/config/default.php
@@ -0,0 +1,79 @@
+ [
+ '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/login/app/sprinkles/account/config/production.php b/login/app/sprinkles/account/config/production.php
new file mode 100755
index 0000000..b7c3288
--- /dev/null
+++ b/login/app/sprinkles/account/config/production.php
@@ -0,0 +1,67 @@
+ [
+ '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/login/app/sprinkles/account/factories/Permissions.php b/login/app/sprinkles/account/factories/Permissions.php
new file mode 100755
index 0000000..591f5fd
--- /dev/null
+++ b/login/app/sprinkles/account/factories/Permissions.php
@@ -0,0 +1,19 @@
+define('UserFrosting\Sprinkle\Account\Database\Models\Permission')->setDefinitions([
+ 'slug' => Faker::word(),
+ 'name' => Faker::word(),
+ 'description' => Faker::paragraph(),
+ 'conditions' => Faker::word()
+]);
diff --git a/login/app/sprinkles/account/factories/Roles.php b/login/app/sprinkles/account/factories/Roles.php
new file mode 100755
index 0000000..cdbb5a3
--- /dev/null
+++ b/login/app/sprinkles/account/factories/Roles.php
@@ -0,0 +1,18 @@
+define('UserFrosting\Sprinkle\Account\Database\Models\Role')->setDefinitions([
+ 'slug' => Faker::unique()->word(),
+ 'name' => Faker::word(),
+ 'description' => Faker::paragraph()
+]);
diff --git a/login/app/sprinkles/account/factories/Users.php b/login/app/sprinkles/account/factories/Users.php
new file mode 100755
index 0000000..7390c44
--- /dev/null
+++ b/login/app/sprinkles/account/factories/Users.php
@@ -0,0 +1,23 @@
+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/login/app/sprinkles/account/locale/ar/messages.php b/login/app/sprinkles/account/locale/ar/messages.php
new file mode 100755
index 0000000..7203904
--- /dev/null
+++ b/login/app/sprinkles/account/locale/ar/messages.php
@@ -0,0 +1,176 @@
+ [
+ "@TRANSLATION" => "الحساب",
+
+ "ACCESS_DENIED" => "يبدو أنك لا تملك صلاحية للقيام بذلك",
+
+ "DISABLED" => "هذا الحساب معطل يمكنك الاتصال بنا للحصول على مزيد من المعلومات",
+
+ "EMAIL_UPDATED" => "تم تجديد البريد الإلكتروني بالحساب",
+
+ "INVALID" => "هذا الحساب غير موجود قد تم حذفه يمكنك الاتصا بنا للحصول على مزيد من المعلومات",
+
+ "MASTER_NOT_EXISTS" => "لا يمكنك تسجيل حساب جديد حتى تم إنشاء الحساب الرئيسي",
+ "MY" => "حسابي",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "تم اختراق جلسنك يجب عليك الخروج على كافة الأجهزة، ثم تسجيل الدخول مرة أخرى والتأكد من أن المعلومات الخاصة بك لم يعبث بها",
+ "TITLE" => "من الممكن أن حسابك قد اخترق",
+ "TEXT" => "ربما استخدم شخص معلومات التسجيل الدخول للدخول إلى هذه الصفحة. لسلامتك، تم انتهاء جميع الجلسات يرجا التسجيل مرة اخرى وتحقق من حسابك بسبب النشاط الغريب قد ترغب في تغيير كلمة المرور"
+ ],
+
+ "SESSION_EXPIRED" => "انتهت جلستك تستطيع تسجيل الدخول مرة أخرى",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "إعدادات الحساب",
+ "DESCRIPTION" => "غير إعدادات حسابك، بما في ذلك البريد الإلكتروني، واسم وكلمة المرور
+",
+ "UPDATED" => "تم تجديد إعدادات الحساب"
+ ],
+
+ "TOOLS" => "أدوات الحساب",
+
+ "UNVERIFIED" => "لم يتم التحقق من حسابك بعد افحص في رسائل البريد الإلكتروني و ملف البريد المزعج للحصول على تعليمات تفعيل الحساب",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "لقد أرسلنا رابط جديدا لتحقق عبر البريد الإلكتروني إلى {{email}} افحص في رسائل البريد الإلكتروني و ملف البريد المزعج",
+ "RESEND" => "إعادة ارسال بريد التحقق",
+ "COMPLETE" => "لقد تم التحقق من حسابك بنجاح يمكنك الآن تسجيل الدخول",
+ "EMAIL" => "ادخل عنوان البريد الإلكتروني الذي استخدمته للتسجيل، و سوف نرسل البريد الإلكتروني لتحقق مرة أخرى",
+ "PAGE" => "إعادة إرسال البريد الإلكتروني التحقق من حسابك الجديد",
+ "SEND" => "ارسل رابط للتحقق عبر البريد الالكتروني",
+ "TOKEN_NOT_FOUND" => "رمز التحقق غير موجود أو تم تحقق الحساب من قبل",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "لا يوجد حساب ل {{email}}",
+ "IN_USE" => "البريد الإلكتروني {{email}} قيد الاستخدام"
+ ],
+
+ "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" => "لم يتم العثور على إعادة تعيين كلمة المرور، أو انتهت صلاحية رابط حاول إعادة تقديم طلبك",
+ "PAGE" => "الحصول على رابط لإعادة تعيين كلمة المرور",
+ "REQUEST_CANNED" => "إلغاء طلب كلمة المرور",
+ "REQUEST_SENT" => "إذا تطابق البريد الإلكتروني {{email}} حسابا في نظامنا، فسيتم إرسال رابط إعادة تعيين كلمة المرور إلى {{email}}."
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "إعادة تعيين كلمة المرور",
+ "CHOOSE" => "اختيار كلمة مرور جديدة للتواصل",
+ "PAGE" => "اختيار كلمة مرور جديدة لحسابك",
+ "SEND" => "تعيين كلمة المرور الجديدة وتسجيل الدخول"
+ ],
+
+ "HASH_FAILED" => "فشلت التجزئة كلمة المرور يرجى الاتصال بمسؤول الموقع",
+ "INVALID" => "كلمة مرور الحالية لا تتطابق مع ما لدينا",
+ "NEW" => "كلمة مرور الجديدة",
+ "NOTHING_TO_UPDATE" => "لا يمكنك تحديث مع نفس كلمة مرور",
+ "UPDATED" => "جدد كلمة مرور",
+
+ "CREATE" => [
+ "@TRANSLATION" => "إنشاء كلمة مرور",
+ "PAGE" => "اختر كلمة مرور لحسابك الجديد",
+ "SET" => "تعيين كلمة المرور وتسجيل الدخول"
+ ]
+ ],
+
+ "REGISTER" => "تسجيل",
+ "REGISTER_ME" => "سجلني",
+ "SIGN_IN_HERE" => "هل لديك حساب؟ تسجيل الدخول هنا",
+
+ "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}}, انت تقبل الأحكام والشروط",
+ "TOS_FOR" => "الأحكام والشروط ل {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "اسم المستخدم",
+
+ "CHOOSE" => "اختيار اسم مستخدم فريد",
+ "INVALID" => "اسم المستخدم غير صالح",
+ "IN_USE" => "اسم المستخدم {{user_name}} قيد الاستخدام"
+ ],
+
+ "USER_ID_INVALID" => "عدم وجود هوية المستخدم المطلوب",
+ "USER_OR_EMAIL_INVALID" => "اسم المستخدم أو عنوان البريد الإلكتروني غير صالح",
+ "USER_OR_PASS_INVALID" => "اسم المستخدم أو كلمة المرور غير صالحة",
+
+ "WELCOME" => "مرحبا بعودتك, {{first_name}}"
+];
diff --git a/login/app/sprinkles/account/locale/ar/validate.php b/login/app/sprinkles/account/locale/ar/validate.php
new file mode 100755
index 0000000..37693fb
--- /dev/null
+++ b/login/app/sprinkles/account/locale/ar/validate.php
@@ -0,0 +1,18 @@
+ [
+ "PASSWORD_MISMATCH" => "يجب أن تكون كلمة المرور وكلمة المرور التأكيدية نفس"
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/de_DE/messages.php b/login/app/sprinkles/account/locale/de_DE/messages.php
new file mode 100755
index 0000000..b331552
--- /dev/null
+++ b/login/app/sprinkles/account/locale/de_DE/messages.php
@@ -0,0 +1,188 @@
+ [
+ "@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 melden Sie sich neu an 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 {{email}}.",
+ "IN_USE" => "Die E-Mail Adresse {{email}} 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" => "{{locale}} 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 Ihre Anfrage erneut einzureichen.",
+ "PAGE" => "Holen Sie sich einen Link, um Ihr Passwort zurückzusetzen.",
+ "REQUEST_CANNED" => "Verlorene Passwortanforderung abgebrochen.",
+ "REQUEST_SENT" => "Wenn die E-Mail {{email}} mit einem Account in unserem System übereinstimmt, wird ein Passwort-Reset-Link an {{email}} 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? Melden Sie sich hier an.",
+ "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 Bedingungen .",
+ "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 {{user_name}} wird bereits verwendet.",
+ "NOT_AVAILABLE" => "Benutzername {{user_name}} 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/login/app/sprinkles/account/locale/de_DE/validate.php b/login/app/sprinkles/account/locale/de_DE/validate.php
new file mode 100755
index 0000000..30cf98b
--- /dev/null
+++ b/login/app/sprinkles/account/locale/de_DE/validate.php
@@ -0,0 +1,21 @@
+ [
+ "PASSWORD_MISMATCH" => "Ihr Passwort und das Bestätigungspasswort müssen übereinstimmen.",
+ "USERNAME" => "Benutzernamen dürfen nur aus Kleinbuchstaben, Zahlen, '.', '-' und '_' bestehen."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/en_US/messages.php b/login/app/sprinkles/account/locale/en_US/messages.php
new file mode 100755
index 0000000..17d7582
--- /dev/null
+++ b/login/app/sprinkles/account/locale/en_US/messages.php
@@ -0,0 +1,183 @@
+ [
+ "@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 log in 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 {{email}}.",
+ "IN_USE" => "Email {{email}} 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" => "{{locale}} 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 resubmitting your request.",
+ "PAGE" => "Get a link to reset your password.",
+ "REQUEST_CANNED" => "Lost password request cancelled.",
+ "REQUEST_SENT" => "If the email {{email}} matches an account in our system, a password reset link will be sent to {{email}}."
+ ],
+
+ "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 {{email}}. 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? Sign in here.",
+ "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 terms and conditions.",
+ "TOS_FOR" => "Terms and Conditions for {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Username",
+
+ "CHOOSE" => "Choose a unique username",
+ "INVALID" => "Invalid username",
+ "IN_USE" => "Username {{user_name}} is already in use.",
+ "NOT_AVAILABLE" => "Username {{user_name}} 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/login/app/sprinkles/account/locale/en_US/validate.php b/login/app/sprinkles/account/locale/en_US/validate.php
new file mode 100755
index 0000000..00c0aef
--- /dev/null
+++ b/login/app/sprinkles/account/locale/en_US/validate.php
@@ -0,0 +1,19 @@
+ [
+ "PASSWORD_MISMATCH" => "Your password and confirmation password must match.",
+ "USERNAME" => "Username may consist only of lowercase letters, numbers, '.', '-', and '_'."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/es_ES/messages.php b/login/app/sprinkles/account/locale/es_ES/messages.php
new file mode 100755
index 0000000..aa8b8ed
--- /dev/null
+++ b/login/app/sprinkles/account/locale/es_ES/messages.php
@@ -0,0 +1,189 @@
+ [
+ "@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. ingrese 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 {{email}} .",
+ "IN_USE" => "El correo electrónico {{email}} 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" => "{{locale}} 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 volver a enviar tu solicitud .",
+ "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 {{email}} ."
+ ],
+
+ "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 {{email}} . 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? Acceda aquí. ",
+
+
+ "TOS" => "Términos y Condiciones",
+ "TOS_AGREEMENT" => "Al registrar una cuenta con {{site_title}}, acepta los términos y condiciones .",
+ "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 {{user_name}} ya está en uso.",
+ "NOT_AVAILABLE" => "El nombre de usuario {{user_name}} 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/login/app/sprinkles/account/locale/es_ES/validate.php b/login/app/sprinkles/account/locale/es_ES/validate.php
new file mode 100755
index 0000000..c8ea0a4
--- /dev/null
+++ b/login/app/sprinkles/account/locale/es_ES/validate.php
@@ -0,0 +1,19 @@
+ [
+ "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/login/app/sprinkles/account/locale/fa/messages.php b/login/app/sprinkles/account/locale/fa/messages.php
new file mode 100755
index 0000000..22623ba
--- /dev/null
+++ b/login/app/sprinkles/account/locale/fa/messages.php
@@ -0,0 +1,178 @@
+ [
+ "@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" => "حساب کاربری با {{email}} ثبت نشده است.",
+ "IN_USE" => "ایمیل {{email}} قبلا استفاده شده است",
+ "VERIFICATION_REQUIRED" => "آدرس پست الکترونیکی را بصورت صحیح وارد کنید"
+ ],
+
+ "EMAIL_OR_USERNAME" => "نام کاربری یا آدرس پست الکترونیکی",
+
+ "FIRST_NAME" => "نام",
+
+ "HEADER_MESSAGE_ROOT" => "شما بعنوان کاربر اصلی وارد شده اید",
+
+ "LAST_NAME" => "نام خانوادگی",
+
+ "LOCALE" => [
+ "ACCOUNT" => "زبان انتخابی برای حساب شما",
+ "INVALID" => "{{locale}} زبان صحیحی نیست"
+ ],
+
+ "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" => "درخواست بازیابی کذرواژه پیدا نشد و یا منقضی شده است. لطفا درخواست را دوباره ارسال کنید",
+ "PAGE" => "دریافت لینک بازیابی گذرواژه",
+ "REQUEST_CANNED" => "درخواست فراموشی گذرواژه، حذف شد.",
+ "REQUEST_SENT" => "ایمیل بازیابی گذرواژه به {{email}} ارسال شد."
+ ],
+
+ "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" => "شما با موفقیت ثبت نام کردید. لینک فعال سازی حساب به آدرس پست الکترونیکیتان {{email}} ارسال شد. بدون فعال سازی نمیتوانید وارد شوید.",
+ "DISABLED" => "با عرض تاسف، امکان ثبت در وب سایت، غیر فعال شده است.",
+ "LOGOUT" => "شما همزمان این که وارد شده اید نمیتوانید حساب کاربری جدیدی بسازید. لطفا ابتدا خارج شوید.",
+ "WELCOME" => "سریع و ساده ثبت نام کنید"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "شما محدودیت تعداد انجام این کار را پشت سر گذاشتید. لطفا {{delay}} ثانیه دیگر صبر کرده و دوباره تلاش کنید.",
+ "REMEMBER_ME" => "من را به خاطر بسپار!",
+ "REMEMBER_ME_ON_COMPUTER" => "من را در این دستگاه به خاطر بسپار (برای دستگاه های عمومی پیشنهاد نمی شود)",
+
+ "SIGNIN" => "ورود",
+ "SIGNIN_OR_REGISTER" => "ثبت نام کنید و یا وارد شوید",
+ "SIGNUP" => "ثبت نام",
+
+ "TOS" => "شرایط و مقررات",
+ "TOS_AGREEMENT" => "با ثبت نام در {{site_title}} موافقت خود با شرایط و مقررات را نشان میدهید.",
+ "TOS_FOR" => "شرایط و مقررات {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "نام کاربری",
+
+ "CHOOSE" => "یک نام کاربری منحصر به فرد انتخاب کنید",
+ "INVALID" => "نام کاربری معتبر نیست",
+ "IN_USE" => "نام کاربری {{user_name}} قبلا استفاده شده است",
+ "NOT_AVAILABLE" => "نام کاربری {{user_name}} موجود نیست. لطفا نام کاربری دیگری انتخاب کنید"
+ ],
+
+ "USER_ID_INVALID" => "آی دی کاربری مد نظر شما موجود نیست",
+ "USER_OR_EMAIL_INVALID" => "نام کاربری و یا آدرس پست الکترونیکی معتبر نیست",
+ "USER_OR_PASS_INVALID" => "کاربری یافت نشد و یا گذرواژه صحیح نیست",
+
+ "WELCOME" => "خوش آمدید {{first_name}}"
+];
diff --git a/login/app/sprinkles/account/locale/fa/validate.php b/login/app/sprinkles/account/locale/fa/validate.php
new file mode 100755
index 0000000..a63cae1
--- /dev/null
+++ b/login/app/sprinkles/account/locale/fa/validate.php
@@ -0,0 +1,20 @@
+ [
+ "PASSWORD_MISMATCH" => "گذرواژه و تکرار آن باید با یکدیگر تطبیق پیدا کنند",
+ "USERNAME" => "نام کاربری فقط میتواند از حروف کوچک، اعداد، '.'، '-' و '_' متشکل شوند."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/fr_FR/messages.php b/login/app/sprinkles/account/locale/fr_FR/messages.php
new file mode 100755
index 0000000..6e5a032
--- /dev/null
+++ b/login/app/sprinkles/account/locale/fr_FR/messages.php
@@ -0,0 +1,179 @@
+ [
+ "@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 {{email}}.",
+ "IN_USE" => "Le email {{email}} 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" => "{{locale}} 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 de soumettre votre demande .",
+ "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 {{email}} correspond à un compte dans notre système, un lien de réinitialisation de mot de passe sera envoyé à {{email}}."
+ ],
+
+ "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 termes et conditions.",
+ "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 {{user_name}} 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/login/app/sprinkles/account/locale/fr_FR/validate.php b/login/app/sprinkles/account/locale/fr_FR/validate.php
new file mode 100755
index 0000000..44b1bc1
--- /dev/null
+++ b/login/app/sprinkles/account/locale/fr_FR/validate.php
@@ -0,0 +1,18 @@
+ [
+ "PASSWORD_MISMATCH" => "Votre mot de passe et votre mot de passe de confirmation doivent correspondre."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/it_IT/messages.php b/login/app/sprinkles/account/locale/it_IT/messages.php
new file mode 100755
index 0000000..fee2e8c
--- /dev/null
+++ b/login/app/sprinkles/account/locale/it_IT/messages.php
@@ -0,0 +1,186 @@
+ [
+ "@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. Accedi 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 {{email}}.",
+ "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" => "{{locale}} 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 riprovare la tua richiesta.",
+ "PAGE" => "Ottieni un collegamento per reimpostare la tua password.",
+ "REQUEST_CANNED" => "Richiesta di recupero password cancellata.",
+ "REQUEST_SENT" => "Se l'email {{email}} corrisponde a un account nel nostro sistema, verrà inviato un collegamento per la reimpostazione della password a {{email}}."
+ ],
+
+ "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 {{email}} 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? Accedi qui",
+ "SIGNIN" => "Accedi",
+ "SIGNIN_OR_REGISTER" => "Accedi o registri",
+ "SIGNUP" => "Registrazione",
+
+ "TOS" => "Termini e condizioni",
+ "TOS_AGREEMENT" => "Registrando un account con {{site_title}}, accetti il termini e condizioni.",
+ "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 {{user_name}} 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/login/app/sprinkles/account/locale/it_IT/validate.php b/login/app/sprinkles/account/locale/it_IT/validate.php
new file mode 100755
index 0000000..713ccba
--- /dev/null
+++ b/login/app/sprinkles/account/locale/it_IT/validate.php
@@ -0,0 +1,21 @@
+ [
+ "PASSWORD_MISMATCH" => "I due campi devono combaciare",
+ "USERNAME" => "L'username può essere composto da caratteri alfanumerici, '.', '-', e '_'."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/pt_PT/messages.php b/login/app/sprinkles/account/locale/pt_PT/messages.php
new file mode 100755
index 0000000..3db4200
--- /dev/null
+++ b/login/app/sprinkles/account/locale/pt_PT/messages.php
@@ -0,0 +1,166 @@
+ [
+ "@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 {{email}}.",
+ "IN_USE" => "O email {{email}} 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 resubmitting your request.",
+ "PAGE" => "Obtenha um link para fazer reset à sua password.",
+ "REQUEST_CANNED" => "Pedido de password esquecida foi cancelado.",
+ "REQUEST_SENT" => "Se o email {{email}} corresponder a uma conta em nosso sistema, um link de redefinição de senha será enviado para {{email}}."
+ ],
+
+ "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 termos e condições.",
+ "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 {{user_name}} 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/login/app/sprinkles/account/locale/pt_PT/validate.php b/login/app/sprinkles/account/locale/pt_PT/validate.php
new file mode 100755
index 0000000..c05f14c
--- /dev/null
+++ b/login/app/sprinkles/account/locale/pt_PT/validate.php
@@ -0,0 +1,18 @@
+ [
+ "PASSWORD_MISMATCH" => "A password e respetiva confirmação têm de coincidir."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/ru_RU/messages.php b/login/app/sprinkles/account/locale/ru_RU/messages.php
new file mode 100755
index 0000000..328db13
--- /dev/null
+++ b/login/app/sprinkles/account/locale/ru_RU/messages.php
@@ -0,0 +1,183 @@
+ [
+ "@TRANSLATION" => "Аккаунт",
+
+ "ACCESS_DENIED" => "Для получения доступа у вас недостаточно прав.",
+
+ "DISABLED" => "Аккаунт отключен. Пожалуйста, свяжитесь с нами для получения дополнительной информации.",
+
+ "EMAIL_UPDATED" => "Email аккаунта обновлён",
+
+ "INVALID" => "Этот аккаунт не существует. Возможно, он удалён. Пожалуйста, свяжитесь с нами для получения дополнительной информации.",
+
+ "MASTER_NOT_EXISTS" => "Вы не можете зарегистрировать аккаунт до тех пор, пока основная учётная запись не будет создана!",
+ "MY" => "Мой профиль",
+
+ "SESSION_COMPROMISED" => [
+ "@TRANSLATION" => "Ваша сессия была скомпрометирована. Вы должны выйти на всех устройствах, а затем снова войти и убедиться, что ваши данные не были изменены.",
+ "TITLE" => "Возможно, ваш аккаунт был скомпрометированн",
+ "TEXT" => "Возможно, кто-то использовал ваши данные для входа на эту страницу. В целях безопасности все сеансы были завершены. Пожалуйста, повторно войдите и проверьте свой аккаунт на подозрительную активность. Рекомендуем сменить пароль."
+ ],
+ "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" => "Нет не одного аккаунта с {{email}} .",
+ "IN_USE" => "Email {{email}} уже используется.",
+ "VERIFICATION_REQUIRED" => "Email (указывайте верный - необходим для активации!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "Имя пользователя или Email",
+
+ "FIRST_NAME" => "Имя",
+
+ "HEADER_MESSAGE_ROOT" => "ВЫ АВТОРИЗОВАНЫ С СУПЕР-ПРАВАМИ",
+
+ "LAST_NAME" => "Фамилия",
+ "LOCALE" => [
+ "ACCOUNT" => "Основной язык для вашего аккаунта",
+ "INVALID" => "{{locale}} язык недопустим."
+ ],
+ "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" => "Этот запрос сброса пароля не может быть найден, или истек. Пожалуйста, попробуйте повторно сбросить пароль.",
+ "PAGE" => "Получите ссылку для сброса пароля.",
+ "REQUEST_CANNED" => "Запрос на сброс пароля отменен.",
+ "REQUEST_SENT" => "Если email {{email}} существует в нашей системе у какого-либо аккаунта, ссылка на сброс пароля будет направлена на {{email}}."
+ ],
+
+ "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" => "Вы успешно зарегистрировались. Ссылка для активации вашего аккаунта была отправлена на {{email}}. Вы сможете войти в систему только после активации аккаунта.",
+ "DISABLED" => "Извините, регистрация аккаунта была отключена.",
+ "LOGOUT" => "Извините, вы не можете зарегистрироваться когда уже авторизовались в системе. Сначала выйдите из системы.",
+ "WELCOME" => "Быстрая и простая регистрация."
+ ],
+ "REMEMBER_ME" => "Запомнить",
+ "REMEMBER_ME_ON_COMPUTER" => "Запомнить меня на этом компьютере (не рекомендуется для общедоступных компьютеров)",
+
+ "SIGN_IN_HERE" => "Уже есть аккаунт? войти.",
+ "SIGNIN" => "Вход",
+ "SIGNIN_OR_REGISTER" => "Регистрация или вход",
+ "SIGNUP" => "Вход",
+
+ "TOS" => "Пользовательское соглашение",
+ "TOS_AGREEMENT" => "Регистрируя аккаунт на {{site_title}}, вы принимаете условия и положения.",
+ "TOS_FOR" => "Правила и условия для {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "Пользователь",
+
+ "CHOOSE" => "Выберите имя пользователя",
+ "INVALID" => "Недопустимое имя пользователя",
+ "IN_USE" => "{{user_name}} имя пользователя уже используется.",
+ "NOT_AVAILABLE" => "Имя пользователя {{user_name}} не доступно. Выберите другое имя или нажмите кнопку «предложить»."
+ ],
+
+ "USER_ID_INVALID" => "ID запрашиваемого пользователя не существует.",
+ "USER_OR_EMAIL_INVALID" => "Имя пользователя или email не верный.",
+ "USER_OR_PASS_INVALID" => "Пользователь не найден или пароль является недействительным.",
+
+ "WELCOME" => "Добро пожаловать, {{first_name}}"
+];
diff --git a/login/app/sprinkles/account/locale/ru_RU/validate.php b/login/app/sprinkles/account/locale/ru_RU/validate.php
new file mode 100755
index 0000000..8ede5d8
--- /dev/null
+++ b/login/app/sprinkles/account/locale/ru_RU/validate.php
@@ -0,0 +1,19 @@
+ [
+ "PASSWORD_MISMATCH" => "Пароли не совпадают.",
+ "USERNAME" => "Имя может состоять только из строчных букв, цифр, '.', '-' и «_»."
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/th_TH/messages.php b/login/app/sprinkles/account/locale/th_TH/messages.php
new file mode 100755
index 0000000..642a7c5
--- /dev/null
+++ b/login/app/sprinkles/account/locale/th_TH/messages.php
@@ -0,0 +1,164 @@
+ [
+ "@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" => "อีเมล {{email}} ไม่มีอยู่จริง",
+ "IN_USE" => "อีเมล {{email}} ได้ถูกใช้งานแล้ว"
+ ],
+
+ "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" => "ขอรีเซ็ตรหัสผ่านนี้ไม่มีอยู่ หรือหมดอายุไปแล้ว กรุณาลอง ส่งคำขอของคุณอีกครั้ง",
+ "PAGE" => "รับลิงก์สำหรับการรีเซ็ตรหัสผ่านของคุณ",
+ "REQUEST_CANNED" => "คำขอลืมรหัสผ่านได้ถูกยกเลิก",
+ "REQUEST_SENT" => "หากอีเมล {{email}} ตรงกับบัญชีในระบบของเราลิงก์การรีเซ็ตรหัสผ่านจะถูกส่งไปที่ {{email}}"
+ ],
+
+ "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}} หมายถึงคุณยอมรับ ข้อตกลงและเงื่อนไข แล้ว",
+ "TOS_FOR" => "ข้อตกลงและเงื่อนไขสำหรับ {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "ชื่อผู้ใช้",
+
+ "CHOOSE" => "เลือกชื่อผู้ใช้ที่เป็นเป็นเอกลักษณ์",
+ "INVALID" => "ชื่อผู้ใช้ไม่ถูกต้อง",
+ "IN_USE" => "ชื่อผู้ใช้ {{user_name}} ถูกใช้งานแล้ว"
+ ],
+
+ "USER_ID_INVALID" => "ไม่พบหมายเลขผู้ใช้ที่ร้องขอมา",
+ "USER_OR_EMAIL_INVALID" => "ชื่อผู้ใช้หรือที่อยู่อีเมลไม่ถูกต้อง",
+ "USER_OR_PASS_INVALID" => "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง",
+
+ "WELCOME" => "ยินดีต้อนรับ {{first_name}}"
+];
diff --git a/login/app/sprinkles/account/locale/th_TH/validate.php b/login/app/sprinkles/account/locale/th_TH/validate.php
new file mode 100755
index 0000000..1a2e90a
--- /dev/null
+++ b/login/app/sprinkles/account/locale/th_TH/validate.php
@@ -0,0 +1,18 @@
+ [
+ "PASSWORD_MISMATCH" => "รหัสผ่านและรหัสผ่านยืนยันของคุณจะต้องตรงกัน"
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/tr/messages.php b/login/app/sprinkles/account/locale/tr/messages.php
new file mode 100755
index 0000000..5213490
--- /dev/null
+++ b/login/app/sprinkles/account/locale/tr/messages.php
@@ -0,0 +1,183 @@
+ [
+ "@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 giriş yapınve şü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" => "{{email}} için hesap yoktur.",
+ "IN_USE" => "E-posta {{email}} 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" => "{{locale}} 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 isteğinizi yeniden göndermeyideneyin.",
+ "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{{email}} sistemdeki bir hesap ile eşleşirse, bir şifre yenileme bağlantısı{{email}} 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{{email}}. 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?burada giriş yap",
+ "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 şartlar ve koşulları.",
+ "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" => "{{user_name}} kullanıcı adı zaten mevcut.",
+ "NOT_AVAILABLE" => "{{user_name}} 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/login/app/sprinkles/account/locale/tr/validate.php b/login/app/sprinkles/account/locale/tr/validate.php
new file mode 100755
index 0000000..298bdbc
--- /dev/null
+++ b/login/app/sprinkles/account/locale/tr/validate.php
@@ -0,0 +1,19 @@
+ [
+ "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/login/app/sprinkles/account/locale/zh_CN/messages.php b/login/app/sprinkles/account/locale/zh_CN/messages.php
new file mode 100755
index 0000000..60adcf0
--- /dev/null
+++ b/login/app/sprinkles/account/locale/zh_CN/messages.php
@@ -0,0 +1,177 @@
+ [
+ "@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" => "{{email}} 没有账户注册.",
+ "IN_USE" => "邮箱 {{email}} 已被使用.",
+ "VERIFICATION_REQUIRED" => "邮箱 (需要进行验证 - 请使用一个有效的!)"
+ ],
+
+ "EMAIL_OR_USERNAME" => "用户名或邮箱地址",
+
+ "FIRST_NAME" => "名字",
+
+ "HEADER_MESSAGE_ROOT" => "你现在以超级用户登陆",
+
+ "LAST_NAME" => "姓氏",
+
+ "LOCALE" => [
+ "ACCOUNT" => "设置你账户的地区和语言",
+ "INVALID" => "{{locale}} 不是一个有效的地区."
+ ],
+
+ "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" => "这个重置密码请求无法使用, 或已过期. 请 重新发送请求.",
+ "PAGE" => "获取重置密码的链接.",
+ "REQUEST_CANNED" => "取消重置请求.",
+ "REQUEST_SENT" => "重置密码的链接已经发送 {{email}}."
+ ],
+
+ "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" => "成功注册. 激活链接已经发送给 {{email}}. 激活之前无法登陆.",
+ "DISABLED" => "抱歉, 账户注册以禁用.",
+ "LOGOUT" => "抱歉, 登陆时不能注册. 请先注销.",
+ "WELCOME" => "注册简单快速."
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "行动速度过快. 请等 {{delay}} 秒后再尝试新的操作.",
+ "REMEMBER_ME" => "记住我!",
+ "REMEMBER_ME_ON_COMPUTER" => "在此电脑上记住我 (不推荐在公共电脑上)",
+
+ "SIGNIN" => "登陆",
+ "SIGNIN_OR_REGISTER" => "登陆或注册",
+ "SIGNUP" => "注销",
+
+ "TOS" => "条款和说明",
+ "TOS_AGREEMENT" => "在 {{site_title}} 注册, 你需要接收 条款和说明.",
+ "TOS_FOR" => "{{title}}的条款和说明",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "用户名",
+
+ "CHOOSE" => "取一个唯一的用户名",
+ "INVALID" => "无效的用户名",
+ "IN_USE" => "用户名 {{user_name}} 已存在.",
+ "NOT_AVAILABLE" => "用户名 {{user_name}} 不可用. 重新选择用户名, 或者点击 '建议'."
+ ],
+
+ "USER_ID_INVALID" => "请求的用户不存在.",
+ "USER_OR_EMAIL_INVALID" => "用户名或邮箱无效.",
+ "USER_OR_PASS_INVALID" => "没有发现用户或密码错误.",
+
+ "WELCOME" => "欢迎回来, {{first_name}}"
+];
diff --git a/login/app/sprinkles/account/locale/zh_CN/validate.php b/login/app/sprinkles/account/locale/zh_CN/validate.php
new file mode 100755
index 0000000..3ca368a
--- /dev/null
+++ b/login/app/sprinkles/account/locale/zh_CN/validate.php
@@ -0,0 +1,19 @@
+ [
+ "PASSWORD_MISMATCH" => "密码不一致.",
+ "USERNAME" => "用户名必须以小写字母, 数字, '.', '-', 和 '_'组成."
+ ]
+];
diff --git a/login/app/sprinkles/account/routes/routes.php b/login/app/sprinkles/account/routes/routes.php
new file mode 100755
index 0000000..8198255
--- /dev/null
+++ b/login/app/sprinkles/account/routes/routes.php
@@ -0,0 +1,59 @@
+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/login/app/sprinkles/account/schema/requests/account-settings.yaml b/login/app/sprinkles/account/schema/requests/account-settings.yaml
new file mode 100755
index 0000000..4a2d368
--- /dev/null
+++ b/login/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/login/app/sprinkles/account/schema/requests/account-verify.yaml b/login/app/sprinkles/account/schema/requests/account-verify.yaml
new file mode 100755
index 0000000..01f3155
--- /dev/null
+++ b/login/app/sprinkles/account/schema/requests/account-verify.yaml
@@ -0,0 +1,6 @@
+---
+token:
+ validators:
+ required:
+ label: validation token
+ message: VALIDATION.REQUIRED
diff --git a/login/app/sprinkles/account/schema/requests/check-username.yaml b/login/app/sprinkles/account/schema/requests/check-username.yaml
new file mode 100755
index 0000000..778b5e5
--- /dev/null
+++ b/login/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/login/app/sprinkles/account/schema/requests/deny-password.yaml b/login/app/sprinkles/account/schema/requests/deny-password.yaml
new file mode 100755
index 0000000..3b3e919
--- /dev/null
+++ b/login/app/sprinkles/account/schema/requests/deny-password.yaml
@@ -0,0 +1,5 @@
+---
+token:
+ validators:
+ required:
+ message: PASSWORD.FORGET.INVALID
diff --git a/login/app/sprinkles/account/schema/requests/forgot-password.yaml b/login/app/sprinkles/account/schema/requests/forgot-password.yaml
new file mode 100755
index 0000000..70072b5
--- /dev/null
+++ b/login/app/sprinkles/account/schema/requests/forgot-password.yaml
@@ -0,0 +1,6 @@
+---
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
diff --git a/login/app/sprinkles/account/schema/requests/login.yaml b/login/app/sprinkles/account/schema/requests/login.yaml
new file mode 100755
index 0000000..b78596a
--- /dev/null
+++ b/login/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/login/app/sprinkles/account/schema/requests/profile-settings.yaml b/login/app/sprinkles/account/schema/requests/profile-settings.yaml
new file mode 100755
index 0000000..c2b5ee8
--- /dev/null
+++ b/login/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/login/app/sprinkles/account/schema/requests/register.yaml b/login/app/sprinkles/account/schema/requests/register.yaml
new file mode 100755
index 0000000..75dae59
--- /dev/null
+++ b/login/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/login/app/sprinkles/account/schema/requests/resend-verification.yaml b/login/app/sprinkles/account/schema/requests/resend-verification.yaml
new file mode 100755
index 0000000..70072b5
--- /dev/null
+++ b/login/app/sprinkles/account/schema/requests/resend-verification.yaml
@@ -0,0 +1,6 @@
+---
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
diff --git a/login/app/sprinkles/account/schema/requests/set-password.yaml b/login/app/sprinkles/account/schema/requests/set-password.yaml
new file mode 100755
index 0000000..ae59d1c
--- /dev/null
+++ b/login/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/login/app/sprinkles/account/src/Account.php b/login/app/sprinkles/account/src/Account.php
new file mode 100755
index 0000000..49c2de9
--- /dev/null
+++ b/login/app/sprinkles/account/src/Account.php
@@ -0,0 +1,20 @@
+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/login/app/sprinkles/account/src/Authenticate/Authenticator.php b/login/app/sprinkles/account/src/Authenticate/Authenticator.php
new file mode 100755
index 0000000..5fb8920
--- /dev/null
+++ b/login/app/sprinkles/account/src/Authenticate/Authenticator.php
@@ -0,0 +1,419 @@
+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/login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php b/login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
new file mode 100755
index 0000000..e79ceb5
--- /dev/null
+++ b/login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
@@ -0,0 +1,21 @@
+ $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/login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php b/login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php
new file mode 100755
index 0000000..dd5647e
--- /dev/null
+++ b/login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php
@@ -0,0 +1,139 @@
+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 = "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/login/app/sprinkles/account/src/Authorize/AuthorizationException.php b/login/app/sprinkles/account/src/Authorize/AuthorizationException.php
new file mode 100755
index 0000000..251b67f
--- /dev/null
+++ b/login/app/sprinkles/account/src/Authorize/AuthorizationException.php
@@ -0,0 +1,23 @@
+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/login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php b/login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php
new file mode 100755
index 0000000..e8e5cde
--- /dev/null
+++ b/login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php
@@ -0,0 +1,193 @@
+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/login/app/sprinkles/account/src/Bakery/CreateAdminUser.php b/login/app/sprinkles/account/src/Bakery/CreateAdminUser.php
new file mode 100755
index 0000000..cfaacef
--- /dev/null
+++ b/login/app/sprinkles/account/src/Bakery/CreateAdminUser.php
@@ -0,0 +1,334 @@
+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("Testing database connection", 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("\nSaving the root user details...");
+ $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/login/app/sprinkles/account/src/Controller/AccountController.php b/login/app/sprinkles/account/src/Controller/AccountController.php
new file mode 100755
index 0000000..ce99370
--- /dev/null
+++ b/login/app/sprinkles/account/src/Controller/AccountController.php
@@ -0,0 +1,1293 @@
+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/login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php b/login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php
new file mode 100755
index 0000000..9713360
--- /dev/null
+++ b/login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php
@@ -0,0 +1,20 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php
new file mode 100755
index 0000000..c74615f
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php
@@ -0,0 +1,82 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php
new file mode 100755
index 0000000..e785ccc
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php
@@ -0,0 +1,57 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php
new file mode 100755
index 0000000..2c2990c
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php
@@ -0,0 +1,55 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php
new file mode 100755
index 0000000..684b01a
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php
@@ -0,0 +1,262 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php
new file mode 100755
index 0000000..b96e327
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php
@@ -0,0 +1,57 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php
new file mode 100755
index 0000000..7f3648b
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php
@@ -0,0 +1,55 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php
new file mode 100755
index 0000000..9cef494
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php
@@ -0,0 +1,78 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php
new file mode 100755
index 0000000..a65eeed
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php
@@ -0,0 +1,69 @@
+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/login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php
new file mode 100755
index 0000000..fa54da6
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php
@@ -0,0 +1,57 @@
+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/login/app/sprinkles/account/src/Database/Models/Activity.php b/login/app/sprinkles/account/src/Database/Models/Activity.php
new file mode 100755
index 0000000..d5be589
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/Activity.php
@@ -0,0 +1,86 @@
+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/login/app/sprinkles/account/src/Database/Models/Group.php b/login/app/sprinkles/account/src/Database/Models/Group.php
new file mode 100755
index 0000000..f10e066
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/Group.php
@@ -0,0 +1,69 @@
+classMapper;
+
+ return $this->hasMany($classMapper->getClassMapping('user'), 'group_id');
+ }
+}
diff --git a/login/app/sprinkles/account/src/Database/Models/PasswordReset.php b/login/app/sprinkles/account/src/Database/Models/PasswordReset.php
new file mode 100755
index 0000000..ac8a930
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/PasswordReset.php
@@ -0,0 +1,76 @@
+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/login/app/sprinkles/account/src/Database/Models/Permission.php b/login/app/sprinkles/account/src/Database/Models/Permission.php
new file mode 100755
index 0000000..463af8d
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/Permission.php
@@ -0,0 +1,121 @@
+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/login/app/sprinkles/account/src/Database/Models/Role.php b/login/app/sprinkles/account/src/Database/Models/Role.php
new file mode 100755
index 0000000..ce9cb8c
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/Role.php
@@ -0,0 +1,105 @@
+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/login/app/sprinkles/account/src/Database/Models/User.php b/login/app/sprinkles/account/src/Database/Models/User.php
new file mode 100755
index 0000000..235f2ef
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/User.php
@@ -0,0 +1,493 @@
+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/login/app/sprinkles/account/src/Database/Models/Verification.php b/login/app/sprinkles/account/src/Database/Models/Verification.php
new file mode 100755
index 0000000..cd5166d
--- /dev/null
+++ b/login/app/sprinkles/account/src/Database/Models/Verification.php
@@ -0,0 +1,70 @@
+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/login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php b/login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php
new file mode 100755
index 0000000..330ca65
--- /dev/null
+++ b/login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php
@@ -0,0 +1,34 @@
+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/login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php b/login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php
new file mode 100755
index 0000000..c651f77
--- /dev/null
+++ b/login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php
@@ -0,0 +1,50 @@
+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/login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php b/login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php
new file mode 100755
index 0000000..e22f02b
--- /dev/null
+++ b/login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php
@@ -0,0 +1,31 @@
+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/login/app/sprinkles/account/src/Log/UserActivityProcessor.php b/login/app/sprinkles/account/src/Log/UserActivityProcessor.php
new file mode 100755
index 0000000..2575270
--- /dev/null
+++ b/login/app/sprinkles/account/src/Log/UserActivityProcessor.php
@@ -0,0 +1,45 @@
+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/login/app/sprinkles/account/src/Repository/PasswordResetRepository.php b/login/app/sprinkles/account/src/Repository/PasswordResetRepository.php
new file mode 100755
index 0000000..2dcffd3
--- /dev/null
+++ b/login/app/sprinkles/account/src/Repository/PasswordResetRepository.php
@@ -0,0 +1,34 @@
+password = Password::hash($args['password']);
+ // TODO: generate user activity? or do this in controller?
+ $user->save();
+ }
+}
diff --git a/login/app/sprinkles/account/src/Repository/TokenRepository.php b/login/app/sprinkles/account/src/Repository/TokenRepository.php
new file mode 100755
index 0000000..a299439
--- /dev/null
+++ b/login/app/sprinkles/account/src/Repository/TokenRepository.php
@@ -0,0 +1,230 @@
+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/login/app/sprinkles/account/src/Repository/VerificationRepository.php b/login/app/sprinkles/account/src/Repository/VerificationRepository.php
new file mode 100755
index 0000000..b0cf048
--- /dev/null
+++ b/login/app/sprinkles/account/src/Repository/VerificationRepository.php
@@ -0,0 +1,32 @@
+flag_verified = 1;
+ // TODO: generate user activity? or do this in controller?
+ $user->save();
+ }
+}
diff --git a/login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php b/login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php
new file mode 100755
index 0000000..4c3ab15
--- /dev/null
+++ b/login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php
@@ -0,0 +1,444 @@
+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/login/app/sprinkles/account/src/Twig/AccountExtension.php b/login/app/sprinkles/account/src/Twig/AccountExtension.php
new file mode 100755
index 0000000..12bacba
--- /dev/null
+++ b/login/app/sprinkles/account/src/Twig/AccountExtension.php
@@ -0,0 +1,65 @@
+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/login/app/sprinkles/account/src/Util/HashFailedException.php b/login/app/sprinkles/account/src/Util/HashFailedException.php
new file mode 100755
index 0000000..a0b37d1
--- /dev/null
+++ b/login/app/sprinkles/account/src/Util/HashFailedException.php
@@ -0,0 +1,21 @@
+staticMethod('user', 'where', 'user_name', $suggestion)->first()) {
+ return $suggestion;
+ }
+ }
+ }
+
+ return '';
+ }
+
+}
diff --git a/login/app/sprinkles/account/templates/forms/settings-account.html.twig b/login/app/sprinkles/account/templates/forms/settings-account.html.twig
new file mode 100755
index 0000000..996b27b
--- /dev/null
+++ b/login/app/sprinkles/account/templates/forms/settings-account.html.twig
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/login/app/sprinkles/account/templates/forms/settings-profile.html.twig b/login/app/sprinkles/account/templates/forms/settings-profile.html.twig
new file mode 100755
index 0000000..0b0a788
--- /dev/null
+++ b/login/app/sprinkles/account/templates/forms/settings-profile.html.twig
@@ -0,0 +1,40 @@
+
diff --git a/login/app/sprinkles/account/templates/mail/password-reset.html.twig b/login/app/sprinkles/account/templates/mail/password-reset.html.twig
new file mode 100755
index 0000000..37096ce
--- /dev/null
+++ b/login/app/sprinkles/account/templates/mail/password-reset.html.twig
@@ -0,0 +1,22 @@
+{% block subject %}
+ {{site.title}} - your password reset request
+{% endblock %}
+
+{% block body %}
+
Dear {{user.first_name}},
+
+
+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')}}.
+
+{% endblock %}
\ No newline at end of file
diff --git a/login/app/sprinkles/account/templates/mail/resend-verification.html.twig b/login/app/sprinkles/account/templates/mail/resend-verification.html.twig
new file mode 100755
index 0000000..ba1c243
--- /dev/null
+++ b/login/app/sprinkles/account/templates/mail/resend-verification.html.twig
@@ -0,0 +1,17 @@
+{% block subject %}
+ {{site.title}} - verify your account
+{% endblock %}
+
+{% block body %}
+
Dear {{user.first_name}},
+
+
+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.
+
+ {% include 'pages/partials/legal.html.twig' %}
+ {% include 'pages/partials/privacy.html.twig' %}
+
+{% endblock %}
+
+{% block modal_footer %}
+
+{% endblock %}
diff --git a/login/app/sprinkles/account/templates/navigation/main-nav.html.twig b/login/app/sprinkles/account/templates/navigation/main-nav.html.twig
new file mode 100755
index 0000000..e44c9c8
--- /dev/null
+++ b/login/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() %}
+
+
+{% endblock %}
+
+{% block scripts_page %}
+
+
+
+
+
+
+ {{ assets.js('js/pages/sign-in') | raw }}
+{% endblock %}
diff --git a/login/app/sprinkles/account/tests/Unit/FactoriesTest.php b/login/app/sprinkles/account/tests/Unit/FactoriesTest.php
new file mode 100755
index 0000000..ee2bf23
--- /dev/null
+++ b/login/app/sprinkles/account/tests/Unit/FactoriesTest.php
@@ -0,0 +1,30 @@
+ci->factory;
+
+ $user = $fm->create('UserFrosting\Sprinkle\Account\Database\Models\User');
+ $this->assertInstanceOf('UserFrosting\Sprinkle\Account\Database\Models\User', $user);
+ }
+}
diff --git a/login/app/sprinkles/account/tests/Unit/HasherTest.php b/login/app/sprinkles/account/tests/Unit/HasherTest.php
new file mode 100755
index 0000000..711e3cb
--- /dev/null
+++ b/login/app/sprinkles/account/tests/Unit/HasherTest.php
@@ -0,0 +1,71 @@
+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));
+ }
+}
diff --git a/login/app/sprinkles/admin/asset-bundles.json b/login/app/sprinkles/admin/asset-bundles.json
new file mode 100755
index 0000000..1bc1706
--- /dev/null
+++ b/login/app/sprinkles/admin/asset-bundles.json
@@ -0,0 +1,170 @@
+{
+ "bundle": {
+ "js/admin": {
+ "scripts": [
+ "vendor/moment/moment.js",
+ "userfrosting/js/handlebars-helpers.js",
+ "vendor/tablesorter/dist/js/jquery.tablesorter.js",
+ "vendor/tablesorter/dist/js/jquery.tablesorter.widgets.js",
+ "userfrosting/js/tablesorter/widget-sort2Hash.js",
+ "vendor/tablesorter/dist/js/widgets/widget-columnSelector.min.js",
+ "vendor/tablesorter/dist/js/widgets/widget-reflow.min.js",
+ "vendor/tablesorter/dist/js/widgets/widget-pager.min.js",
+ "userfrosting/js/query-string.js",
+ "userfrosting/js/uf-table.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/activities": {
+ "scripts": [
+ "userfrosting/js/pages/activities.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/dashboard": {
+ "scripts": [
+ "userfrosting/js/widgets/users.js",
+ "userfrosting/js/pages/dashboard.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/group": {
+ "scripts": [
+ "userfrosting/js/widgets/users.js",
+ "userfrosting/js/widgets/groups.js",
+ "userfrosting/js/pages/group.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/permission": {
+ "scripts": [
+ "userfrosting/js/widgets/users.js",
+ "userfrosting/js/pages/permission.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/role": {
+ "scripts": [
+ "userfrosting/js/widgets/roles.js",
+ "userfrosting/js/pages/role.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/user": {
+ "scripts": [
+ "userfrosting/js/widgets/users.js",
+ "userfrosting/js/pages/user.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/users": {
+ "scripts": [
+ "userfrosting/js/widgets/users.js",
+ "userfrosting/js/pages/users.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/groups": {
+ "scripts": [
+ "userfrosting/js/widgets/groups.js",
+ "userfrosting/js/pages/groups.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/permissions": {
+ "scripts": [
+ "userfrosting/js/pages/permissions.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "js/pages/roles": {
+ "scripts": [
+ "userfrosting/js/widgets/roles.js",
+ "userfrosting/js/pages/roles.js"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "scripts": "plain"
+ }
+ }
+ }
+ },
+ "css/admin": {
+ "styles": [
+ "font-starcraft/css/font-starcraft.css",
+ "vendor/tablesorter/dist/css/theme.bootstrap.min.css",
+ "vendor/tablesorter/dist/css/jquery.tablesorter.pager.min.css",
+ "userfrosting/css/tablesorter-reflow.css",
+ "userfrosting/css/tablesorter-custom.css"
+ ],
+ "options": {
+ "result": {
+ "type": {
+ "styles": "plain"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css b/login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css
new file mode 100755
index 0000000..405c1ca
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css
@@ -0,0 +1,30 @@
+/* ==========================================================================
+ Custom styling for tablesorter tables
+ ========================================================================== */
+
+/* Custom styling for pager controls
+ ========================================================================== */
+
+.pager-lg {
+ font-size: large;
+}
+
+.pager-control {
+ font-size: x-large;
+ padding: 4px 8px;
+ cursor: pointer;
+}
+
+/* Custom styling for panels that contain buttons in the header (eg, CSV download buttons)
+ ========================================================================== */
+
+.panel-heading-buttons h3 {
+ padding-top: 7.5px;
+}
+
+/* Don't display tablesorter filter field when disabled
+ ========================================================================== */
+
+.tablesorter thead .disabled {
+ display: none
+}
\ No newline at end of file
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js
new file mode 100755
index 0000000..89ac5c1
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js
@@ -0,0 +1,16 @@
+/**
+ * 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 uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /activities
+ */
+
+$(document).ready(function() {
+ // Set up table of activities
+ $("#widget-activities").ufTable({
+ dataUrl: site.uri.public + "/api/activities",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js
new file mode 100755
index 0000000..f2b8a4f
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js
@@ -0,0 +1,49 @@
+/**
+ * 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 }}
+ *
+ * Target page: /dashboard
+ */
+
+$(document).ready(function() {
+ $('.js-clear-cache').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/dashboard/clear-cache",
+ ajaxParams: {
+ slug: $(this).data('slug')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ form.ufForm()
+ .on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+ });
+
+ // Table of site activities
+ $("#widget-activities").ufTable({
+ dataUrl: site.uri.public + "/api/activities",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Table of users in current user's group
+ $("#widget-group-users").ufTable({
+ dataUrl: site.uri.public + "/api/groups/g/" + page.group_slug + "/users",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Bind user creation button
+ bindUserCreationButton($("#widget-group-users"));
+
+ // Bind user table buttons
+ $("#widget-group-users").on("pagerComplete.ufTable", function () {
+ bindUserButtons($(this));
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js
new file mode 100755
index 0000000..a1ca959
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js
@@ -0,0 +1,24 @@
+/**
+ * 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 uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /groups/g/{slug}
+ */
+
+$(document).ready(function() {
+ // Control buttons
+ bindGroupButtons($("#view-group"));
+
+ // Table of users in this group
+ $("#widget-group-users").ufTable({
+ dataUrl: site.uri.public + '/api/groups/g/' + page.group_slug + '/users',
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Bind user table buttons
+ $("#widget-group-users").on("pagerComplete.ufTable", function () {
+ bindUserButtons($(this));
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js
new file mode 100755
index 0000000..0bfc65a
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js
@@ -0,0 +1,24 @@
+/**
+ * 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 widgets/groups.js, uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /groups
+ */
+
+$(document).ready(function() {
+
+ $("#widget-groups").ufTable({
+ dataUrl: site.uri.public + "/api/groups",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Bind creation button
+ bindGroupCreationButton($("#widget-groups"));
+
+ // Bind table buttons
+ $("#widget-groups").on("pagerComplete.ufTable", function () {
+ bindGroupButtons($(this));
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js
new file mode 100755
index 0000000..87e851f
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js
@@ -0,0 +1,20 @@
+/**
+ * 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 uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /permissions/p/{id}
+ */
+
+$(document).ready(function() {
+ $("#widget-permission-users").ufTable({
+ dataUrl: site.uri.public + '/api/permissions/p/' + page.permission_id + '/users',
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Bind table buttons
+ $("#widget-permission-users").on("pagerComplete.ufTable", function () {
+ bindUserButtons($(this));
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js
new file mode 100755
index 0000000..6266ff4
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js
@@ -0,0 +1,16 @@
+/**
+ * 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 widgets/permissions.js, uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /permissions
+ */
+
+$(document).ready(function() {
+ // Set up table of permissions
+ $("#widget-permissions").ufTable({
+ dataUrl: site.uri.public + "/api/permissions",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js
new file mode 100755
index 0000000..8dae7f5
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js
@@ -0,0 +1,23 @@
+/**
+ * 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 uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /roles/r/{slug}
+ */
+
+$(document).ready(function() {
+ // Control buttons
+ bindRoleButtons($("#view-role"));
+
+ $("#widget-role-permissions").ufTable({
+ dataUrl: site.uri.public + '/api/roles/r/' + page.role_slug + '/permissions',
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ $("#widget-role-users").ufTable({
+ dataUrl: site.uri.public + '/api/roles/r/' + page.role_slug + '/users',
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js
new file mode 100755
index 0000000..b1febb2
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js
@@ -0,0 +1,24 @@
+/**
+ * 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 widgets/roles.js, uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /roles
+ */
+
+$(document).ready(function() {
+ // Set up table of roles
+ $("#widget-roles").ufTable({
+ dataUrl: site.uri.public + "/api/roles",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Bind creation button
+ bindRoleCreationButton($("#widget-roles"));
+
+ // Bind table buttons
+ $("#widget-roles").on("pagerComplete.ufTable", function () {
+ bindRoleButtons($(this));
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js
new file mode 100755
index 0000000..70acf7c
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js
@@ -0,0 +1,25 @@
+/**
+ * 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 uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /users/u/{user_name}
+ */
+
+$(document).ready(function() {
+ // Control buttons
+ bindUserButtons($("#view-user"));
+
+ // Table of activities
+ $("#widget-user-activities").ufTable({
+ dataUrl: site.uri.public + '/api/users/u/' + page.user_name + '/activities',
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Table of permissions
+ $("#widget-permissions").ufTable({
+ dataUrl: site.uri.public + '/api/users/u/' + page.user_name + '/permissions',
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js
new file mode 100755
index 0000000..d9e4bb7
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js
@@ -0,0 +1,24 @@
+/**
+ * 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 widgets/users.js, uf-table.js, moment.js, handlebars-helpers.js
+ *
+ * Target page: /users
+ */
+
+$(document).ready(function() {
+ // Set up table of users
+ $("#widget-users").ufTable({
+ dataUrl: site.uri.public + "/api/users",
+ useLoadingTransition: site.uf_table.use_loading_transition
+ });
+
+ // Bind creation button
+ bindUserCreationButton($("#widget-users"));
+
+ // Bind table buttons
+ $("#widget-users").on("pagerComplete.ufTable", function () {
+ bindUserButtons($(this));
+ });
+});
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js
new file mode 100755
index 0000000..d701d81
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js
@@ -0,0 +1,111 @@
+/**
+ * Groups widget. Sets up dropdowns, modals, etc for a table of groups.
+ */
+
+/**
+ * Set up the form in a modal after being successfully attached to the body.
+ */
+function attachGroupForm() {
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ /**
+ * Set up modal widgets
+ */
+ // Set up any widgets inside the modal
+ form.find(".js-select2").select2({
+ width: '100%'
+ });
+
+ // Auto-generate slug
+ form.find('input[name=name]').on('input change', function() {
+ var manualSlug = form.find('#form-group-slug-override').prop('checked');
+ if (!manualSlug) {
+ var slug = getSlug($(this).val());
+ form.find('input[name=slug]').val(slug);
+ }
+ });
+
+ form.find('#form-group-slug-override').on('change', function() {
+ if ($(this).prop('checked')) {
+ form.find('input[name=slug]').prop('readonly', false);
+ } else {
+ form.find('input[name=slug]').prop('readonly', true);
+ form.find('input[name=name]').trigger('change');
+ }
+ });
+
+ // Set icon when changed
+ form.find('input[name=icon]').on('input change', function() {
+ $(this).prev(".icon-preview").find("i").removeClass().addClass($(this).val());
+ });
+
+ // Set up the form for submission
+ form.ufForm({
+ validators: page.validators
+ }).on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+}
+
+/**
+ * Link group action buttons, for example in a table or on a specific group's page.
+ */
+function bindGroupButtons(el) {
+ /**
+ * Link row buttons after table is loaded.
+ */
+
+ /**
+ * Buttons that launch a modal dialog
+ */
+ // Edit group details button
+ el.find('.js-group-edit').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/groups/edit",
+ ajaxParams: {
+ slug: $(this).data('slug')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ attachGroupForm();
+ });
+
+ // Delete group button
+ el.find('.js-group-delete').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/groups/confirm-delete",
+ ajaxParams: {
+ slug: $(this).data('slug')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ form.ufForm()
+ .on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+ });
+}
+
+function bindGroupCreationButton(el) {
+ // Link create button
+ el.find('.js-group-create').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/groups/create",
+ msgTarget: $("#alerts-page")
+ });
+
+ attachGroupForm();
+ });
+};
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js
new file mode 100755
index 0000000..0e32651
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js
@@ -0,0 +1,148 @@
+/**
+ * Roles widget. Sets up dropdowns, modals, etc for a table of roles.
+ */
+
+/**
+ * Set up the form in a modal after being successfully attached to the body.
+ */
+function attachRoleForm() {
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ /**
+ * Set up modal widgets
+ */
+
+ // Auto-generate slug
+ form.find('input[name=name]').on('input change', function() {
+ var manualSlug = form.find('#form-role-slug-override').prop('checked');
+ if (!manualSlug) {
+ var slug = getSlug($(this).val());
+ form.find('input[name=slug]').val(slug);
+ }
+ });
+
+ form.find('#form-role-slug-override').on('change', function() {
+ if ($(this).prop('checked')) {
+ form.find('input[name=slug]').prop('readonly', false);
+ } else {
+ form.find('input[name=slug]').prop('readonly', true);
+ form.find('input[name=name]').trigger('change');
+ }
+ });
+
+ // Set up the form for submission
+ form.ufForm({
+ validators: page.validators
+ }).on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+}
+
+/**
+ * Link role action buttons, for example in a table or on a specific role's page.
+ */
+function bindRoleButtons(el) {
+ /**
+ * Link row buttons after table is loaded.
+ */
+
+ // Manage permissions button
+ el.find('.js-role-permissions').click(function() {
+ var slug = $(this).data('slug');
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/roles/permissions",
+ ajaxParams: {
+ slug: slug
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ // Set up collection widget
+ var permissionWidget = modal.find('.js-form-permissions');
+ permissionWidget.ufCollection({
+ dropdown: {
+ ajax: {
+ url : site.uri.public + '/api/permissions'
+ },
+ placeholder : "Select a permission"
+ },
+ dropdownTemplate: modal.find('#role-permissions-select-option').html(),
+ rowTemplate : modal.find('#role-permissions-row').html()
+ });
+
+ // Get current roles and add to widget
+ $.getJSON(site.uri.public + '/api/roles/r/' + slug + '/permissions')
+ .done(function (data) {
+ $.each(data.rows, function (idx, permission) {
+ permission.text = permission.name;
+ permissionWidget.ufCollection('addRow', permission);
+ });
+ });
+
+ // Set up form for submission
+ form.ufForm({
+ }).on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+ });
+
+ /**
+ * Buttons that launch a modal dialog
+ */
+ // Edit role details button
+ el.find('.js-role-edit').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/roles/edit",
+ ajaxParams: {
+ slug: $(this).data('slug')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ attachRoleForm();
+ });
+
+ // Delete role button
+ el.find('.js-role-delete').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/roles/confirm-delete",
+ ajaxParams: {
+ slug: $(this).data('slug')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ form.ufForm()
+ .on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+ });
+}
+
+function bindRoleCreationButton(el) {
+ // Link create button
+ el.find('.js-role-create').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/roles/create",
+ msgTarget: $("#alerts-page")
+ });
+
+ attachRoleForm();
+ });
+};
diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js
new file mode 100755
index 0000000..2e153e5
--- /dev/null
+++ b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js
@@ -0,0 +1,277 @@
+/**
+ * Users widget. Sets up dropdowns, modals, etc for a table of users.
+ */
+
+/**
+ * Set up the form in a modal after being successfully attached to the body.
+ */
+function attachUserForm() {
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ // Set up any widgets inside the modal
+ form.find(".js-select2").select2({
+ width: '100%'
+ });
+
+ // Set up the form for submission
+ form.ufForm({
+ validators: page.validators
+ }).on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+}
+
+/**
+ * Enable/disable password fields when switch is toggled
+ */
+function toggleChangePasswordMode(el, userName, changePasswordMode) {
+ var form = el.find("form");
+ if (changePasswordMode == 'link') {
+ $(".controls-password").find("input[type='password']").prop('disabled', true);
+ // Form submits password reset request
+ form.attr({
+ method: 'POST',
+ action: site.uri.public + '/api/users/u/' + userName + '/password-reset'
+ });
+
+ var validator = form.validate();
+ if (validator) {
+ //Iterate through named elements inside of the form, and mark them as error free
+ el.find("input[type='password']").each(function() {
+ validator.successList.push(this); //mark as error free
+ });
+ validator.resetForm();//remove error class on name elements and clear history
+ validator.reset();//remove all error and success data
+ }
+ el.find("input[type='password']").closest('.form-group')
+ .removeClass('has-error has-success');
+ el.find('.form-control-feedback').each(function () {
+ $(this).remove();
+ });
+ } else {
+ $(".controls-password").find("input[type='password']").prop('disabled', false);
+ // Form submits direct password update
+ form.attr({
+ method: 'PUT',
+ action: site.uri.public + '/api/users/u/' + userName + '/password'
+ });
+ }
+}
+
+/**
+ * Update user field(s)
+ */
+function updateUser(userName, fieldName, fieldValue) {
+ var data = {
+ 'value': fieldValue
+ };
+
+ data[site.csrf.keys.name] = site.csrf.name;
+ data[site.csrf.keys.value] = site.csrf.value;
+
+ var url = site.uri.public + '/api/users/u/' + userName + '/' + fieldName;
+ var debugAjax = (typeof site !== "undefined") && site.debug.ajax;
+
+ return $.ajax({
+ type: "PUT",
+ url: url,
+ data: data,
+ dataType: debugAjax ? 'html' : 'json',
+ converters: {
+ // Override jQuery's strict JSON parsing
+ 'text json': function(result) {
+ try {
+ // First try to use native browser parsing
+ if (typeof JSON === 'object' && typeof JSON.parse === 'function') {
+ return JSON.parse(result);
+ } else {
+ return $.parseJSON(result);
+ }
+ } catch (e) {
+ // statements to handle any exceptions
+ console.log("Warning: Could not parse expected JSON response.");
+ return {};
+ }
+ }
+ }
+ }).fail(function (jqXHR) {
+ // Error messages
+ if (debugAjax && jqXHR.responseText) {
+ document.write(jqXHR.responseText);
+ document.close();
+ } else {
+ console.log("Error (" + jqXHR.status + "): " + jqXHR.responseText );
+
+ // Display errors on failure
+ // TODO: ufAlerts widget should have a 'destroy' method
+ if (!$("#alerts-page").data('ufAlerts')) {
+ $("#alerts-page").ufAlerts();
+ } else {
+ $("#alerts-page").ufAlerts('clear');
+ }
+
+ $("#alerts-page").ufAlerts('fetch').ufAlerts('render');
+ }
+
+ return jqXHR;
+ }).done(function (response) {
+ window.location.reload();
+ });
+}
+
+/**
+ * Link user action buttons, for example in a table or on a specific user's page.
+ */
+ function bindUserButtons(el) {
+
+ /**
+ * Buttons that launch a modal dialog
+ */
+ // Edit general user details button
+ el.find('.js-user-edit').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/users/edit",
+ ajaxParams: {
+ user_name: $(this).data('user_name')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ attachUserForm();
+ });
+
+ // Manage user roles button
+ el.find('.js-user-roles').click(function() {
+ var userName = $(this).data('user_name');
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/users/roles",
+ ajaxParams: {
+ user_name: userName
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ // Set up collection widget
+ var roleWidget = modal.find('.js-form-roles');
+ roleWidget.ufCollection({
+ dropdown : {
+ ajax: {
+ url : site.uri.public + '/api/roles'
+ },
+ placeholder : "Select a role"
+ },
+ dropdownTemplate: modal.find('#user-roles-select-option').html(),
+ rowTemplate : modal.find('#user-roles-row').html()
+ });
+
+ // Get current roles and add to widget
+ $.getJSON(site.uri.public + '/api/users/u/' + userName + '/roles')
+ .done(function (data) {
+ $.each(data.rows, function (idx, role) {
+ role.text = role.name;
+ roleWidget.ufCollection('addRow', role);
+ });
+ });
+
+ // Set up form for submission
+ form.ufForm({
+ }).on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+ });
+
+ // Change user password button
+ el.find('.js-user-password').click(function() {
+ var userName = $(this).data('user_name');
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/users/password",
+ ajaxParams: {
+ user_name: userName
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ // Set up form for submission
+ form.ufForm({
+ validators: page.validators
+ }).on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+
+ toggleChangePasswordMode(modal, userName, 'link');
+
+ // On submission, submit either the PUT request, or POST for a password reset, depending on the toggle state
+ modal.find("input[name='change_password_mode']").click(function() {
+ var changePasswordMode = $(this).val();
+ toggleChangePasswordMode(modal, userName, changePasswordMode);
+ });
+ });
+ });
+
+ // Delete user button
+ el.find('.js-user-delete').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/users/confirm-delete",
+ ajaxParams: {
+ user_name: $(this).data('user_name')
+ },
+ msgTarget: $("#alerts-page")
+ });
+
+ $("body").on('renderSuccess.ufModal', function (data) {
+ var modal = $(this).ufModal('getModal');
+ var form = modal.find('.js-form');
+
+ form.ufForm()
+ .on("submitSuccess.ufForm", function() {
+ // Reload page on success
+ window.location.reload();
+ });
+ });
+ });
+
+ /**
+ * Direct action buttons
+ */
+ el.find('.js-user-activate').click(function() {
+ var btn = $(this);
+ updateUser(btn.data('user_name'), 'flag_verified', '1');
+ });
+
+ el.find('.js-user-enable').click(function () {
+ var btn = $(this);
+ updateUser(btn.data('user_name'), 'flag_enabled', '1');
+ });
+
+ el.find('.js-user-disable').click(function () {
+ var btn = $(this);
+ updateUser(btn.data('user_name'), 'flag_enabled', '0');
+ });
+}
+
+function bindUserCreationButton(el) {
+ // Link create button
+ el.find('.js-user-create').click(function() {
+ $("body").ufModal({
+ sourceUrl: site.uri.public + "/modals/users/create",
+ msgTarget: $("#alerts-page")
+ });
+
+ attachUserForm();
+ });
+};
diff --git a/login/app/sprinkles/admin/composer.json b/login/app/sprinkles/admin/composer.json
new file mode 100755
index 0000000..8ccd5c0
--- /dev/null
+++ b/login/app/sprinkles/admin/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "userfrosting/sprinkle-admin",
+ "type": "userfrosting-sprinkle",
+ "description": "Administrative 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": {
+ "php": ">=5.6"
+ },
+ "autoload": {
+ "psr-4": {
+ "UserFrosting\\Sprinkle\\Admin\\": "src/"
+ }
+ }
+}
diff --git a/login/app/sprinkles/admin/locale/ar/messages.php b/login/app/sprinkles/admin/locale/ar/messages.php
new file mode 100755
index 0000000..d4a3a44
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/ar/messages.php
@@ -0,0 +1,135 @@
+ [
+ 1 => "نشاط",
+ 2 => "أنشطة",
+
+ "LAST" => "النشاط الاخير",
+ "PAGE" => "قائمة من أنشطة المستخدم",
+ "TIME" => "وقت نشاط"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "مسح ذاكرة التخزين",
+ "CLEAR_CONFIRM" => "هل أنت متأكد أنك تريد مسح ذاكرة التخزين بالموقع؟",
+ "CLEAR_CONFIRM_YES" => "نعم، إمسح ذاكرة التخزين",
+ "CLEARED" => "تم مسح ذاكرة التخزين بنجاح"
+ ],
+
+ "DASHBOARD" => "لوحة القيادة",
+ "NO_FEATURES_YET" => "لا يبدو أن أي ميزات تم إعدادها لهذا الحساب حتى الآن. ربما لم يتم تنفيذها بعد، أو ربما شخص نسي أن يعطيك الوصول. في كلتا الحالتين، نحن سعداء أن يكون لك على متن!",
+ "DELETE_MASTER" => "لا يمكنك حذف الحساب الرئيسي",
+ "DELETION_SUCCESSFUL" => "المستعمل {{user_name}} حذف بنجاح",
+ "DETAILS_UPDATED" => "جدد تفاصيل الحساب للمستخدم {{user_name}}",
+ "DISABLE_MASTER" => "لا يمكنك تعطيل الحساب الرئيسي",
+ "DISABLE_SUCCESSFUL" => "حساب المستخدم {{user_name}} عطيل بنجاح",
+
+ "ENABLE_SUCCESSFUL" => "حساب المستخدم {{user_name}} مكين بنجاح",
+
+ "GROUP" => [
+ 1 => "مجموعة",
+ 2 => "مجموعات",
+
+ "CREATE" => "إنشاء مجموعة",
+ "DELETE" => "حذف مجموعة",
+ "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف مجموعة {{name}}?",
+ "DELETE_YES" => "نعم، إحذف مجموعة",
+ "EDIT" => "تعديل مجموعة",
+ "ICON" => "رمز المجموعة",
+ "ICON_EXPLAIN" => "رمز المستخدمين في المجموعه",
+ "INFO_PAGE" => "صفحة معلومات المجموعة ل {{name}}",
+ //"MANAGE" => "Manage group",
+ "NAME" => "أسم المجموعة",
+ "NAME_EXPLAIN" => "ادخال اسم للمجموعة",
+ "PAGE_DESCRIPTION" => "قائمة المجموعات لموقعك يوفر أدوات لإدارة التحرير وحذف مجموعات"
+ ],
+
+ "MANUALLY_ACTIVATED" => "تم تفعيل حساب{{user_name}}",
+ "MASTER_ACCOUNT_EXISTS" => "الحساب الرئيسي موجود بالفعل",
+ "MIGRATION" => [
+ "REQUIRED" => "تحديث قاعدة البيانات مطلوب"
+ ],
+
+ "PERMISSION" => [
+ 1 => "الإذن",
+ 2 => "مأذونيات",
+
+ "ASSIGN_NEW" => "تعيين إذن جديد",
+ "HOOK_CONDITION" => "الظروف",
+ "MANAGE" => "إدارة المأذونات",
+ "PAGE_DESCRIPTION" => "قائمة المأذونات لموقعك",
+ "UPDATE" => "تحديث المأذونات"
+ ],
+
+ "ROLE" => [
+ 1 => "وظيفة",
+ 2 => "وظائف",
+
+ "ASSIGN_NEW" => "تعيين دور جديد",
+ "CREATE" => "إنشاء دور",
+ "DELETE" => "حذف دور",
+ "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف الدور {{name}}?",
+ "DELETE_YES" => "نعم، حذف دور",
+ "EDIT" => "إدارة دور",
+ "INFO_PAGE" => "صفحة معلومات دور {{name}}",
+ "MANAGE" => "إدارة الوظائف",
+ "NAME" => "اسم",
+ "NAME_EXPLAIN" => "أدخل اسما للدور",
+ "PAGE_DESCRIPTION" => "قائمة الوظائف لموقعك",
+ "UPDATED" => "تحديث الوظائف"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "معلومات الجهاز",
+
+ "DB_NAME" => "اسم قاعدة البيانات",
+ "DB_VERSION" => "إصدار قاعدة البيانات",
+ "DIRECTORY" => "دليل المشروع",
+ "PHP_VERSION" => "الإصدار PHP",
+ "SERVER" => "برنامج الخادم",
+ "SPRINKLES" => "sprinkles المحمل",
+ "UF_VERSION" => "إصدار UserFrosting",
+ "URL" => "رابط قاعدة الموقع"
+ ],
+
+ "USER" => [
+ 1 => "مستخدم",
+ 2 => "المستخدمين",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "تغيير كلمة المرور للمستخدم",
+ "SEND_PASSWORD_LINK" => "إرسال المستخدم وصلة من شأنها أن تسمح لهم لاختيار كلمة المرور الخاصة بهم",
+ "SET_PASSWORD" => "تعيين كلمة المرور الخاصة بالمستخدم"
+ ],
+
+ "ACTIVATE" => "تفعيل المستخدم",
+ "CREATE" => "إنشاء مستخدم",
+ "DELETE" => "مسح المستخدم",
+ "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف المستخدم {{name}}?",
+ "DELETE_YES" => "نعم، حذف المستخدم",
+ "DISABLE" => "تعطيل المستخدم ",
+ "EDIT" => "إدارة المستخدم",
+ "ENABLE" => "تمكين المستخدم",
+ "INFO_PAGE" => "صفحة معلومات المستخدم {{name}}",
+ "PAGE_DESCRIPTION" => "قائمة المستخدمين لموقعك",
+ "LATEST" => "أحدث المستخدمين",
+ "VIEW_ALL" => "عرض جميع المستخدمين"
+ ],
+ "X_USER" => [
+ 0 => "لا يوجد اي مستخدمين",
+ 1 => "{{plural}} مستخدم",
+ 2 => "{{plural}} المستخدمين"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/de_DE/messages.php b/login/app/sprinkles/admin/locale/de_DE/messages.php
new file mode 100755
index 0000000..6e21ab5
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/de_DE/messages.php
@@ -0,0 +1,161 @@
+ [
+ 1 => "Aktivität",
+ 2 => "Aktivitäten",
+
+ "LAST" => "Letzte Aktivität",
+ "PAGE" => "Eine Auflistung der Benutzeraktivitäten",
+ "TIME" => "Aktivitätszeit"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Cache löschen",
+ "CLEAR_CONFIRM" => "Sind Sie sicher, dass Sie den Seiten-Cache löschen möchten?",
+ "CLEAR_CONFIRM_YES" => "Ja, Cache löschen",
+ "CLEARED" => "Cache wurde erfolgreich gelöscht!"
+ ],
+
+ "DASHBOARD" => "Übersicht",
+ "NO_FEATURES_YET" => "Es sieht aus, als wären für Ihren Account noch keine Funktionen aktiviert... bisher. Entweder sie wurden bisher noch nicht implementiert, oder Ihnen fehlen noch die Berechtigungen. Trotzdem ist es schön, dass Sie auf unsere Seite gekommen sind!",
+ "DELETE_MASTER" => "Sie können das Root-Konto nicht löschen!",
+ "DELETION_SUCCESSFUL" => "Benutzer {{user_name}} wurde erfolgreich gelöscht.",
+ "DETAILS_UPDATED" => "Konto-Daten für {{user_name}} aktualisiert.",
+ "DISABLE_MASTER" => "Sie können das Root-Konto nicht deaktivieren!",
+ "DISABLE_SELF" => "Sie können Ihr eigenes Konto nicht deaktivieren!",
+ "DISABLE_SUCCESSFUL" => "Konto von {{user_name}} wurde erfolgreich deaktiviert.",
+
+ "ENABLE_SUCCESSFUL" => "Konto von {{user_name}} wurde erfolgreich aktiviert.",
+
+ "GROUP" => [
+ 1 => "Gruppe",
+ 2 => "Gruppen",
+
+ "CREATE" => "Gruppe erstellen",
+ "CREATION_SUCCESSFUL" => "Die Gruppe {{name}} wurde erfolgreich erstellt",
+ "DELETE" => "Gruppe löschen",
+ "DELETE_CONFIRM" => "Möchten Sie die Gruppe {{name}} wirklich löschen?",
+ "DELETE_DEFAULT" => "Sie können die Gruppe {{name}} nicht löschen, da es die Standardgruppe für neu registrierte Benutzer ist.",
+ "DELETE_YES" => "Ja, Gruppe löschen",
+ "DELETION_SUCCESSFUL" => "Die Gruppe {{name}} wurde erfolgreich gelöscht",
+ "EDIT" => "Gruppe bearbeiten",
+ "ICON" => "Gruppensymbol",
+ "ICON_EXPLAIN" => "Symbol für Gruppenmitglieder",
+ "INFO_PAGE" => "Gruppeninformationsseite für {{name}}",
+ "MANAGE" => "Gruppe verwalten",
+ "NAME" => "Gruppenname",
+ "NAME_EXPLAIN" => "Geben Sie einen Namen für die Gruppe ein",
+ "NOT_EMPTY" => "Sie können das nicht tun, denn es sind noch Benutzer mit der Gruppe {{name}} verbunden.",
+ "PAGE_DESCRIPTION" => "Eine Liste der Gruppen für Ihre Website. Bietet Verwaltungstools für das Bearbeiten und Löschen von Gruppen.",
+ "SUMMARY" => "Gruppen Zusammenfassung",
+ "UPDATE" => "Details für die Gruppe {{name}} aktualisiert"
+ ],
+
+ "MANUALLY_ACTIVATED" => "{{user_name}}'s Konto wurde manuell aktiviert.",
+ "MASTER_ACCOUNT_EXISTS" => "Das Root-Konto existiert bereits!",
+ "MIGRATION" => [
+ "REQUIRED" => "Datenbankaktualisierung erforderlich"
+ ],
+
+ "PERMISSION" => [
+ 1 => "Berechtigung",
+ 2 => "Berechtigungen",
+
+ "ASSIGN_NEW" => "Neue Berechtigung zuweisen",
+ "HOOK_CONDITION" => "Haken/Bedingungen",
+ "ID" => "Berechtigungs-ID",
+ "INFO_PAGE" => "Berechtigungs Informationen für '{{name}}'",
+ "MANAGE" => "Berechtigungen verwalten",
+ "NOTE_READ_ONLY" => "Bitte beachten Sie: Berechtigungen werden als \"Teil des Quelltexts\" gesehen und können hier nicht bearbeitet werden. Um Berechtigungen hinzuzufügen, zu bearbeiten, oder zu löschen, benutzen Sie bitte folgende Dokumentation zur Datenbank Migration.",
+ "PAGE_DESCRIPTION" => "Eine Liste der Berechtigungen für Ihre Website. Bietet Verwaltungstools zum Bearbeiten und Löschen von Berechtigungen.",
+ "SUMMARY" => "Berechtigungs Zusammenfassung",
+ "UPDATE" => "Berechtigungen aktualisieren",
+ "VIA_ROLES" => "Besitzt die Berechtigung durch die Rolle"
+ ],
+
+ "ROLE" => [
+ 1 => "Rolle",
+ 2 => "Rollen",
+
+ "ASSIGN_NEW" => "Neue Rolle zuweisen",
+ "CREATE" => "Rolle erstellen",
+ "CREATION_SUCCESSFUL" => "Die Rolle {{name}} wurde erfolgreich erstellt",
+ "DELETE" => "Rolle löschen",
+ "DELETE_CONFIRM" => "Sind Sie sicher, dass Sie die Rolle {{name}} löschen möchten?",
+ "DELETE_DEFAULT" => "Sie können die Rolle {{name}} nicht löschen, da es eine Standardrolle für neu registrierte Benutzer ist.",
+ "DELETE_YES" => "Ja, Rolle löschen",
+ "DELETION_SUCCESSFUL" => "Die Rolle {{name}} wurde erfolgreich gelöscht",
+ "EDIT" => "Rolle bearbeiten",
+ "HAS_USERS" => "Sie können das nicht machen weil es noch Benutzer gibt, die die Rolle {{name}} haben.",
+ "INFO_PAGE" => "Rolleninformationsseite für {{name}}",
+ "MANAGE" => "Rollen verwalten",
+ "NAME" => "Name",
+ "NAME_EXPLAIN" => "Geben Sie einen Namen für die Rolle ein",
+ "NAME_IN_USE" => "Eine Rolle mit dem Namen {{name}} existiert bereits",
+ "PAGE_DESCRIPTION" => "Eine Liste der Rollen für Ihre Website. Bietet Verwaltungstools zum Bearbeiten und Löschen von Rollen.",
+ "PERMISSIONS_UPDATED" => "Berechtigungen für die Rolle {{name}} aktualisiert",
+ "SUMMARY" => "Rollen Zusammenfassung",
+ "UPDATED" => "Rollen aktualisieren"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "System Information",
+
+ "DB_NAME" => "Name der Datenbank",
+ "DB_VERSION" => "Datenbankversion",
+ "DIRECTORY" => "Projektverzeichnis",
+ "PHP_VERSION" => "PHP-Version",
+ "SERVER" => "Web-Server-Software",
+ "SPRINKLES" => "Geladene Sprinkles",
+ "UF_VERSION" => "UserFrosting Version",
+ "URL" => "Website-Stamm-Url"
+ ],
+
+ "TOGGLE_COLUMNS" => "Spalten anpassen",
+
+ "USER" => [
+ 1 => "Benutzer",
+ 2 => "Benutzer",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Benutzerpasswort ändern",
+ "SEND_PASSWORD_LINK" => "Senden Sie dem Benutzer einen Link, der ihnen erlaubt, ihr eigenes Passwort zu wählen",
+ "SET_PASSWORD" => "Setzen Sie das Passwort des Benutzers als"
+ ],
+
+ "ACTIVATE" => "Benutzer aktivieren",
+ "CREATE" => "Benutzer erstellen",
+ "CREATED" => "Benutzer {{user_name}} wurde erfolgreich erstellt",
+ "DELETE" => "Benutzer löschen",
+ "DELETE_CONFIRM" => "Sind Sie sicher, dass Sie den Benutzer {{name}} löschen möchten?",
+ "DELETE_YES" => "Ja, Benutzer löschen",
+ "DISABLE" => "Benutzer deaktivieren",
+ "EDIT" => "Benutzer bearbeiten",
+ "ENABLE" => "Benutzer aktivieren",
+ "INFO_PAGE" => "Benutzerinformationsseite für {{name}}",
+ "LATEST" => "Neueste Benutzer",
+ "PAGE_DESCRIPTION" => "Eine Liste der Benutzer für Ihre Website. Bietet Management-Tools, einschließlich der Möglichkeit, Benutzerdaten bearbeiten, manuell aktivieren, Benutzer aktivieren/deaktivieren, und vieles mehr.",
+ "SUMMARY" => "Benutzer Zusammenfassung",
+ "VIEW_ALL" => "Alle Benutzer anzeigen",
+ "WITH_PERMISSION" => "Benutzer mit dieser Berechtigung"
+ ],
+ "X_USER" => [
+ 0 => "Keine Benutzer",
+ 1 => "{{plural}} Benutzer",
+ 2 => "{{plural}} Benutzer"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/en_US/messages.php b/login/app/sprinkles/admin/locale/en_US/messages.php
new file mode 100755
index 0000000..a21e325
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/en_US/messages.php
@@ -0,0 +1,160 @@
+ [
+ 1 => "Activity",
+ 2 => "Activities",
+
+ "LAST" => "Last Activity",
+ "PAGE" => "A listing of user activities",
+ "TIME" => "Activity Time"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Clear cache",
+ "CLEAR_CONFIRM" => "Are you sure you want to clear the site cache?",
+ "CLEAR_CONFIRM_YES" => "Yes, clear cache",
+ "CLEARED" => "Cache cleared successfully !"
+ ],
+
+ "DASHBOARD" => "Dashboard",
+ "NO_FEATURES_YET" => "It doesn't look like any features have been set up for this account...yet. Maybe they haven't been implemented yet, or maybe someone forgot to give you access. Either way, we're glad to have you aboard!",
+ "DELETE_MASTER" => "You cannot delete the master account!",
+ "DELETION_SUCCESSFUL" => "User {{user_name}} has been successfully deleted.",
+ "DETAILS_UPDATED" => "Account details updated for user {{user_name}}",
+ "DISABLE_MASTER" => "You cannot disable the master account!",
+ "DISABLE_SELF" => "You cannot disable your own account!",
+ "DISABLE_SUCCESSFUL" => "Account for user {{user_name}} has been successfully disabled.",
+
+ "ENABLE_SUCCESSFUL" => "Account for user {{user_name}} has been successfully enabled.",
+
+ "GROUP" => [
+ 1 => "Group",
+ 2 => "Groups",
+
+ "CREATE" => "Create group",
+ "CREATION_SUCCESSFUL" => "Successfully created group {{name}}",
+ "DELETE" => "Delete group",
+ "DELETE_CONFIRM" => "Are you sure you want to delete the group {{name}}?",
+ "DELETE_DEFAULT" => "You can't delete the group {{name}} because it is the default group for newly registered users.",
+ "DELETE_YES" => "Yes, delete group",
+ "DELETION_SUCCESSFUL" => "Successfully deleted group {{name}}",
+ "EDIT" => "Edit group",
+ "ICON" => "Group icon",
+ "ICON_EXPLAIN" => "Icon for group members",
+ "INFO_PAGE" => "Group information page for {{name}}",
+ "MANAGE" => "Manage group",
+ "NAME" => "Group name",
+ "NAME_EXPLAIN" => "Please enter a name for the group",
+ "NOT_EMPTY" => "You can't do that because there are still users associated with the group {{name}}.",
+ "PAGE_DESCRIPTION" => "A listing of the groups for your site. Provides management tools for editing and deleting groups.",
+ "SUMMARY" => "Group Summary",
+ "UPDATE" => "Details updated for group {{name}}"
+ ],
+
+ "MANUALLY_ACTIVATED" => "{{user_name}}'s account has been manually activated",
+ "MASTER_ACCOUNT_EXISTS" => "The master account already exists!",
+ "MIGRATION" => [
+ "REQUIRED" => "Database update required"
+ ],
+
+ "PERMISSION" => [
+ 1 => "Permission",
+ 2 => "Permissions",
+
+ "ASSIGN_NEW" => "Assign new permission",
+ "HOOK_CONDITION" => "Hook/Conditions",
+ "ID" => "Permission ID",
+ "INFO_PAGE" => "Permission information page for '{{name}}'",
+ "MANAGE" => "Manage permissions",
+ "NOTE_READ_ONLY" => "Please note: permissions are considered \"part of the code\" and cannot be modified through the interface. To add, remove, or modify permissions, the site maintainers will need to use a database migration.",
+ "PAGE_DESCRIPTION" => "A listing of the permissions for your site. Provides management tools for editing and deleting permissions.",
+ "SUMMARY" => "Permission Summary",
+ "UPDATE" => "Update permissions",
+ "VIA_ROLES" => "Has permission via roles"
+ ],
+
+ "ROLE" => [
+ 1 => "Role",
+ 2 => "Roles",
+
+ "ASSIGN_NEW" => "Assign new role",
+ "CREATE" => "Create role",
+ "CREATION_SUCCESSFUL" => "Successfully created role {{name}}",
+ "DELETE" => "Delete role",
+ "DELETE_CONFIRM" => "Are you sure you want to delete the role {{name}}?",
+ "DELETE_DEFAULT" => "You can't delete the role {{name}} because it is a default role for newly registered users.",
+ "DELETE_YES" => "Yes, delete role",
+ "DELETION_SUCCESSFUL" => "Successfully deleted role {{name}}",
+ "EDIT" => "Edit role",
+ "HAS_USERS" => "You can't do that because there are still users who have the role {{name}}.",
+ "INFO_PAGE" => "Role information page for {{name}}",
+ "MANAGE" => "Manage Roles",
+ "NAME" => "Name",
+ "NAME_EXPLAIN" => "Please enter a name for the role",
+ "NAME_IN_USE" => "A role named {{name}} already exist",
+ "PAGE_DESCRIPTION" => "A listing of the roles for your site. Provides management tools for editing and deleting roles.",
+ "PERMISSIONS_UPDATED" => "Permissions updated for role {{name}}",
+ "SUMMARY" => "Role Summary",
+ "UPDATED" => "Details updated for role {{name}}"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "System information",
+
+ "DB_NAME" => "Database name",
+ "DB_VERSION" => "Database version",
+ "DIRECTORY" => "Project directory",
+ "PHP_VERSION" => "PHP version",
+ "SERVER" => "Webserver software",
+ "SPRINKLES" => "Loaded sprinkles",
+ "UF_VERSION" => "UserFrosting version",
+ "URL" => "Site root url"
+ ],
+
+ "TOGGLE_COLUMNS" => "Toggle columns",
+
+ "USER" => [
+ 1 => "User",
+ 2 => "Users",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Change User Password",
+ "SEND_PASSWORD_LINK" => "Send the user a link that will allow them to choose their own password",
+ "SET_PASSWORD" => "Set the user's password as"
+ ],
+
+ "ACTIVATE" => "Activate user",
+ "CREATE" => "Create user",
+ "CREATED" => "User {{user_name}} has been successfully created",
+ "DELETE" => "Delete user",
+ "DELETE_CONFIRM" => "Are you sure you want to delete the user {{name}}?",
+ "DELETE_YES" => "Yes, delete user",
+ "DELETED" => "User deleted",
+ "DISABLE" => "Disable user",
+ "EDIT" => "Edit user",
+ "ENABLE" => "Enable user",
+ "INFO_PAGE" => "User information page for {{name}}",
+ "LATEST" => "Latest Users",
+ "PAGE_DESCRIPTION" => "A listing of the users for your site. Provides management tools including the ability to edit user details, manually activate users, enable/disable users, and more.",
+ "SUMMARY" => "Account Summary",
+ "VIEW_ALL" => "View all users",
+ "WITH_PERMISSION" => "Users with this permission"
+ ],
+ "X_USER" => [
+ 0 => "No users",
+ 1 => "{{plural}} user",
+ 2 => "{{plural}} users"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/es_ES/messages.php b/login/app/sprinkles/admin/locale/es_ES/messages.php
new file mode 100755
index 0000000..a8950c0
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/es_ES/messages.php
@@ -0,0 +1,164 @@
+ [
+ 1 => "Actividad",
+ 2 => "Actividades",
+
+ "LAST" => "Última actividad",
+ "PAGE" => "Una lista de las actividades del usuario",
+ "TIME" => "Tiempo de Actividad"
+ ],
+
+ "ADMIN" => [
+ "PANEL" => "Panel de administración"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Limpiar cache",
+ "CLEAR_CONFIRM" => "¿Está seguro de que desea borrar la caché del sitio?",
+ "CLEAR_CONFIRM_YES" => "Sí, borrar caché",
+ "CLEARED" => "¡Cache borrado correctamente!"
+ ],
+
+ "DASHBOARD" => "Tablero",
+ "NO_FEATURES_YET" => "No parece que se hayan configurado funciones para esta cuenta ... todavía. Tal vez no se han implementado todavía, o tal vez alguien se olvidó de darle acceso. De cualquier manera, ¡estamos encantados de tenerte a bordo!",
+ "DELETE_MASTER" => "¡No puede eliminar la cuenta principal!",
+ "DELETION_SUCCESSFUL" => "El usuario {{user_name}} se ha eliminado correctamente.",
+ "DETAILS_UPDATED" => "Detalles de la cuenta actualizados para el usuario {{user_name}} ",
+ "DISABLE_MASTER" => "¡No puedes deshabilitar la cuenta principal!",
+ "DISABLE_SELF" => "¡No puedes inhabilitar tu propia cuenta!",
+ "DISABLE_SUCCESSFUL" => "La cuenta para el usuario {{user_name}} se ha desactivado correctamente.",
+
+ "ENABLE_SUCCESSFUL" => "La cuenta para el usuario {{user_name}} se ha habilitado correctamente.",
+
+ "GROUP" => [
+ 1 => "Grupo",
+ 2 => "Grupos",
+
+ "CREATE" => "Crea un grupo",
+ "CREATION_SUCCESSFUL" => "Grupo creado correctamente {{name}} ",
+ "DELETE" => "Delete group",
+ "DELETE_CONFIRM" => "¿Seguro que quieres eliminar el grupo {{name}} ?",
+ "DELETE_DEFAULT" => "No puedes eliminar el grupo {{name}} porque es el grupo predeterminado para los usuarios recién registrados.",
+ "DELETE_YES" => "Sí, eliminar grupo",
+ "DELETION_SUCCESSFUL" => "Grupo eliminado correctamente {{name}} ",
+ "EDIT" => "Editar grupo",
+ "ICON" => "Icono de grupo",
+ "ICON_EXPLAIN" => "Icono para los miembros del grupo",
+ "INFO_PAGE" => "Página de información de grupo para {{name}}",
+ "MANAGE" => "Administrar grupo",
+ "NAME" => "Nombre del grupo",
+ "NAME_EXPLAIN" => "Introduzca un nombre para el grupo",
+ "NOT_EMPTY" => "No puedes hacerlo porque todavía hay usuarios asociados con el grupo {{name}} .",
+ "PAGE_DESCRIPTION" => "Un listado de los grupos para su sitio. Proporciona herramientas de administración para editar y eliminar grupos.",
+ "SUMMARY" => "Resumen del grupo",
+ "UPDATE" => "Detalles actualizados para el grupo {{name}} "
+ ],
+
+ "MANUALLY_ACTIVATED" => "La cuenta de {{user_name}} se ha activado manualmente",
+ "MASTER_ACCOUNT_EXISTS" => "¡La cuenta maestra ya existe!",
+ "MIGRATION" => [
+ "REQUIRED" => "Se requiere actualizar la base de datos"
+ ],
+
+ "PERMISSION" => [
+ 1 => "Permiso",
+ 2 => "Permisos",
+
+ "ASSIGN_NEW" => "Asignar nuevo permiso",
+ "HOOK_CONDITION" => "Hook/Condiciones",
+ "ID" => "ID de permiso",
+ "INFO_PAGE" => "Página de autor del permiso de '{{name}}'",
+ "MANAGE" => "Administrar permisos",
+ "NOTE_READ_ONLY" => " Tenga en cuenta: los permisos se consideran \"parte del código\" y no se pueden modificar a través de la interfaz. Para agregar, eliminar o modificar permisos, los mantenedores del sitio necesitarán usar una migración de la base de datos . ",
+ "PAGE_DESCRIPTION" => "Una lista de los permisos para su sitio. Proporciona herramientas de administración para editar y eliminar permisos.",
+ "SUMMARY" => "Resumen del permiso",
+ "UPDATE" => "Actualizar permisos",
+ "VIA_ROLES" => "Tiene permiso para los roles"
+ ],
+
+ "ROLE" => [
+ 1 => "Rol(funcion)",
+ 2 => "Roles(funciones)",
+
+ "ASSIGN_NEW" => "Asignar nueva rol",
+ "CREATE" => "Crear un rol",
+ "CREATION_SUCCESSFUL" => "Función creada correctamente {{name}} ",
+ "DELETE" => "Eliminar rol",
+ "DELETE_CONFIRM" => "¿Seguro que quieres eliminar la función {{name}} ?",
+ "DELETE_DEFAULT" => "No puedes eliminar el rol {{name}} porque es un rol predeterminado para los usuarios recién registrados.",
+ "DELETE_YES" => "Sí, borrar función",
+ "DELETION_SUCCESSFUL" => "Se ha eliminado la función {{nombre}} ",
+ "EDIT" => "Editar función",
+ "HAS_USERS" => "No puedes hacerlo porque todavía hay usuarios que tienen el rol {{name}} .",
+ "INFO_PAGE" => "Página de información de funciones de {{name}}",
+ "MANAGE" => "Administrar roles",
+ "NAME" => "Nombre",
+ "NAME_EXPLAIN" => "Ingrese un nombre para el rol",
+ "NAME_IN_USE" => "Ya existe un rol denominado {{name}} ",
+ "PAGE_DESCRIPTION" => "Una lista de las funciones de su sitio. Proporciona herramientas de administración para editar y eliminar roles.",
+ "PERMISSIONS_UPDATED" => "Permisos actualizados para el rol {{name}} ",
+ "SUMMARY" => "Resumen del rol",
+ "UPDATED" => "Detalles actualizados para el rol {{name}} "
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "Información del sistema",
+
+ "DB_NAME" => "Nombre de la base de datos",
+ "DB_VERSION" => "Versión de base de datos",
+ "DIRECTORY" => "Directorio del proyecto",
+ "PHP_VERSION" => "Versión de PHP",
+ "SERVER" => "Software de servidor Web",
+ "SPRINKLES" => "Sprinkles cargados",
+ "UF_VERSION" => "UserFrosting versión",
+ "URL" => "URL root del sitio"
+ ],
+
+ "TOGGLE_COLUMNS" => "Alternar columnas",
+ "NO_DATA" => "No puede quedar vacio.",
+
+ "USER" => [
+ 1 => "Usuario",
+ 2 => "Usuarios",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Cambiar contraseña de usuario",
+ "SEND_PASSWORD_LINK" => "Enviar al usuario un enlace que les permita elegir su propia contraseña",
+ "SET_PASSWORD" => "Establezca la contraseña del usuario como"
+ ],
+
+ "ACTIVATE" => "Activar usuario",
+ "CREATE" => "Crear usuario",
+ "CREATED" => "Se ha creado correctamente el usuario {{user_name}} ",
+ "DELETE" => "Borrar usuario",
+ "DELETE_CONFIRM" => "¿Seguro que desea eliminar el usuario {{name}} ?",
+ "DELETE_YES" => "Sí, eliminar usuario",
+ "DISABLE" => "Deshabilitar usuario",
+ "EDIT" => "Editar usuario",
+ "ENABLE" => "Habilitar usuario",
+ "INFO_PAGE" => "Página de información de usuario de {{name}}",
+ "LATEST" => "Usuarios más recientes",
+ "PAGE_DESCRIPTION" => "Una lista de los usuarios para su sitio. Proporciona herramientas de administración que incluyen la capacidad de editar detalles de usuario, activar manualmente usuarios, habilitar / deshabilitar usuarios y más.",
+ "SUMMARY" => "Resumen de la cuenta",
+ "VIEW_ALL" => "Ver todos los usuarios",
+ "WITH_PERMISSION" => "Usuarios con este permiso"
+ ],
+ "X_USER" => [
+ 0 => "No hay usuarios",
+ 1 => "{{plural}} usuario",
+ 2 => "{{plural}} usuarios"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/fa/messages.php b/login/app/sprinkles/admin/locale/fa/messages.php
new file mode 100755
index 0000000..75a8dee
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/fa/messages.php
@@ -0,0 +1,158 @@
+ [
+ 1 => "فعالیت",
+ 2 => "فعالیت ها",
+
+ "LAST" => "آخرین فعالیت",
+ "PAGE" => "لیستی از فعالیت های کاربر",
+ "TIME" => "زمان فعالیت"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "پاک سازی کش",
+ "CLEAR_CONFIRM" => "آیا مطمئن هستید که میخواهید کش سایت را پاک سازی کنید؟",
+ "CLEAR_CONFIRM_YES" => "بله، کش پاک سازی شود",
+ "CLEARED" => "کش با موفقیت پاک سازی شد"
+ ],
+
+ "DASHBOARD" => "کارتابل",
+ "DELETE_MASTER" => "شما نمیتوانید کاربر اصلی را حذف کنید",
+ "DELETION_SUCCESSFUL" => "{{user_name}} با موفقیت حذف شد.",
+ "DETAILS_UPDATED" => "جزئیات {{user_name}} با موفقیت ذخیره شد.",
+ "DISABLE_MASTER" => "شما نمیتوانید کاربر اصلی را غیر فعال کنید.",
+ "DISABLE_SUCCESSFUL" => "حساب کاربری {{user_name}} با موفقیت غیر فعال شد.",
+
+ "ENABLE_SUCCESSFUL" => "حساب کاربری {{user_name}} با موفقیت فعال شد.",
+
+ "GROUP" => [
+ 1 => "گروه",
+ 2 => "گروه ها",
+
+ "CREATE" => "اضافه کردن گروه",
+ "CREATION_SUCCESSFUL" => "گروه {{name}} با موفقیت اضافه شد",
+ "DELETE" => "حذف گروه",
+ "DELETE_CONFIRM" => "آیا مطمئن هستید که میخواهید گروه {{name}} را حذف کنید؟",
+ "DELETE_DEFAULT" => "شما نمیتوانید گروه {{name}} را حذف کنید چون به عنوان گروه پیش فرض برای کاربران جدید انتخاب شده است.",
+ "DELETE_YES" => "بله، گروه حذف شود",
+ "DELETION_SUCCESSFUL" => "گروه {{name}} با موفقیت حذف شد.",
+ "EDIT" => "ویرایش گروه",
+ "ICON" => "آیکن گروه",
+ "ICON_EXPLAIN" => "آیکن برای اعضای گروه",
+ "INFO_PAGE" => "صفحه توضیحات گروه برای {{name}}",
+ "MANAGE" => "مدیریت گروه",
+ "NAME" => "نام گروه",
+ "NAME_EXPLAIN" => "لطفا نام گروه را وارد کنید",
+ "NOT_EMPTY" => "نمیتوان این کار را کرد چون هنوز کاربرانی عضو گروه {{name}} هستند.",
+ "PAGE_DESCRIPTION" => "لیست گروه های وب سایت شما. امکان مدیریت این گروه ها در این صفحه وجود دارد.",
+ "SUMMARY" => "توضیحات گروه",
+ "UPDATE" => "اطلاعات گروه {{name}} به روز رسانی شد."
+ ],
+
+ "MANUALLY_ACTIVATED" => "حساب کاربری {{user_name}} بصورت دستی فعال شد.",
+ "MASTER_ACCOUNT_EXISTS" => "حساب کاربری اصلی وجود دارد!",
+ "MIGRATION" => [
+ "REQUIRED" => "به روز رسانی پایگاه داده ها باید انجام شود"
+ ],
+
+ "PERMISSION" => [
+ 1 => "دسترسی",
+ 2 => "دسترسی ها",
+
+ "ASSIGN_NEW" => "دادن دسترسی",
+ "HOOK_CONDITION" => "قلاب/شرط",
+ "ID" => "آی دی دسترسی",
+ "INFO_PAGE" => "توضیحات دسترسی {{name}}",
+ "MANAGE" => "مدیریت دسترسی ها",
+ "NOTE_READ_ONLY" => "توجه بفرماییددسترسی ها بخشی از کد میباشند و آن ها را نمیتوان از اینترفیس تغییر داد. برای این تغییرات، مبایستی که مدیر، از دیتابیس مایگریشن استفاده کند. ",
+ "PAGE_DESCRIPTION" => "لیست دسترسی های وب سایت شما. امکان مدیریت این دسترسی ها در این صفحه وجود دارد.",
+ "SUMMARY" => "توضیحات دسترسی ها",
+ "UPDATE" => "به روز رسانی دسترسی ها",
+ "VIA_ROLES" => "از طریق وظیفه ها دسترسی دارد"
+ ],
+
+ "ROLE" => [
+ 1 => "وظیفه",
+ 2 => "وظیفه ها",
+
+ "ASSIGN_NEW" => "دادن وظیفه",
+ "CREATE" => "ساخت وظیفه",
+ "CREATION_SUCCESSFUL" => "وظیفه {{name}} با موفقیت ساخته شد",
+ "DELETE" => "حذف وظیفه",
+ "DELETE_CONFIRM" => "اطمینان دارید که میخواهید وظیفه {{name}} را حذف کنید؟",
+ "DELETE_DEFAULT" => "شما نمیتوانید وظیفه {{name}} را حذف کنید زیرا کاربرانی که تازه ثبت نام کنند، این وظیفه را دریافت خواهند کرد.",
+ "DELETE_YES" => "بله، وظیفه حذف شود",
+ "DELETION_SUCCESSFUL" => "وظیفه {{name}} با موفقیت حذف شد",
+ "EDIT" => "ویرایش وظیفه",
+ "HAS_USERS" => "نمیتوانید این کار را انجام دهید زیرا کاربرانی وظیفه {{name}} را هنوز دارند.",
+ "INFO_PAGE" => "صفحه توضیحات وظیفه {{name}}",
+ "MANAGE" => "مدیریت وظیفه ها",
+ "NAME" => "نام",
+ "NAME_EXPLAIN" => "لطفا برای وظیفه نامی انتخاب کنید",
+ "NAME_IN_USE" => "وظیفه ای با نام {{name}} موجود است",
+ "PAGE_DESCRIPTION" => "لیست وظیفه های وب سایت شما. امکان مدیریت این وظیفه ها در این صفحه وجود دارد.",
+ "PERMISSIONS_UPDATED" => "دسترسی ها برای وظیفه {{name}} به روز رسانی شد",
+ "SUMMARY" => "خلاصه وظیفه",
+ "UPDATED" => "اطلاعات وظیفه {{name}} به روز رسانی شد"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "توضیحات سیستم",
+
+ "DB_NAME" => "نام پایگاه داده",
+ "DB_VERSION" => "نسخه پایگاه داده",
+ "DIRECTORY" => "دایرکتوری پروژه",
+ "PHP_VERSION" => "نسخه پی اچ پی",
+ "SERVER" => "نرمافزار وب سرور",
+ "SPRINKLES" => "اسپرینکل های بارگذاری شده",
+ "UF_VERSION" => "نسخه یوزرفروستینگ",
+ "URL" => "آدرس رووت وب سایت"
+ ],
+
+ "TOGGLE_COLUMNS" => "تغییر ستون",
+
+ "USER" => [
+ 1 => "کاربر",
+ 2 => "کاربران",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "تغییر گذرواژه کاربر",
+ "SEND_PASSWORD_LINK" => "برای کاربر ایمیلی ارسال شود تا گذرواژه خود را تغییر دهد",
+ "SET_PASSWORD" => "گذرواژه کاربر را انتخاب کنید"
+ ],
+
+ "ACTIVATE" => "کاربر فعال",
+ "CREATE" => "اضافه کردن کاربر",
+ "CREATED" => "کاربر {{user_name}} با موفقیت اضافه شد",
+ "DELETE" => "حذف کاربر",
+ "DELETE_CONFIRM" => "آیا اطمینان دارید که میخواهید کاربر {{name}} را حذف کنید؟",
+ "DELETE_YES" => "بله، کاربر حذف شود",
+ "DISABLE" => "غیر فعال سازی کاربر",
+ "EDIT" => "ویرایش کاربر",
+ "ENABLE" => "فعال سازی کاربر",
+ "INFO_PAGE" => "صفحه توضیحات کاربر {{name}}",
+ "LATEST" => "آخرین کاربران",
+ "PAGE_DESCRIPTION" => "لیستی از کاربران سایت. این صفحه به شما امکان ویرایش، فعال سازی و غیر فعال سازی کاربران را می دهد.",
+ "SUMMARY" => "خلاصه حساب",
+ "VIEW_ALL" => "تماشای همه ی کاربران",
+ "WITH_PERMISSION" => "کاربرانی که این دسترسی را دارند"
+ ],
+ "X_USER" => [
+ 0 => "هیچ کاربری",
+ 1 => "{{plural}} کاربر",
+ 2 => "{{plural}} کاربر"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/fr_FR/messages.php b/login/app/sprinkles/admin/locale/fr_FR/messages.php
new file mode 100755
index 0000000..82bdf3e
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/fr_FR/messages.php
@@ -0,0 +1,147 @@
+ [
+ 1 => "Activité",
+ 2 => "Activités",
+
+ "LAST" => "Dernière activité",
+ "PAGE" => "Une liste des activités des utilisateurs",
+ "TIME" => "Date de l'activité"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Vider le cache",
+ "CLEAR_CONFIRM" => "Voulez-vous vraiment supprimer le cache du site?",
+ "CLEAR_CONFIRM_YES" => "Oui, vider le cache",
+ "CLEARED" => "Cache effacé avec succès !"
+ ],
+
+ "DASHBOARD" => "Tableau de bord",
+ "DELETE_MASTER" => "Vous ne pouvez pas supprimer le compte principal !",
+ "DELETION_SUCCESSFUL" => "L'utilisateur {{user_name}} a été supprimé avec succès.",
+ "DETAILS_UPDATED" => "Les détails du compte de {{user_name}} ont été mis à jour",
+ "DISABLE_MASTER" => "Vous ne pouvez pas désactiver le compte principal !",
+ "DISABLE_SELF" => "Vous ne pouvez pas désactiver votre propre compte !",
+ "DISABLE_SUCCESSFUL" => "Le compte de l'utilisateur {{user_name}} a été désactivé avec succès.",
+
+ "ENABLE_SUCCESSFUL" => "Le compte de l'utilisateur {{user_name}} a été activé avec succès.",
+
+ "GROUP" => [
+ 1 => "Groupe",
+ 2 => "Groupes",
+
+ "CREATE" => "Créer un groupe",
+ "CREATION_SUCCESSFUL" => "Successfully created group {{name}}",
+ "DELETE" => "Supprimer le groupe",
+ "DELETE_CONFIRM" => "Êtes-vous certain de vouloir supprimer le groupe {{name}}?",
+ "DELETE_DEFAULT" => "Vous ne pouvez pas supprimer le groupe {{name}} parce que c'est le groupe par défaut pour les utilisateurs nouvellement enregistrés.",
+ "DELETE_YES" => "Oui, supprimer le groupe",
+ "DELETION_SUCCESSFUL" => "Groupe {{name}} supprimé avec succès",
+ "EDIT" => "Modifier le groupe",
+ "ICON" => "Icône",
+ "ICON_EXPLAIN" => "Icône des membres du groupe",
+ "INFO_PAGE" => "Informations sur le groupe {{name}}",
+ "MANAGE" => "Gérer le groupe",
+ "NAME" => "Nom du groupe",
+ "NAME_EXPLAIN" => "Spécifiez le nom du groupe",
+ "NOT_EMPTY" => "Vous ne pouvez pas le faire car il y a encore des utilisateurs associés au groupe {{name}}.",
+ "PAGE_DESCRIPTION" => "Une liste des groupes pour votre site. Fournit des outils de gestion pour éditer et supprimer des groupes.",
+ "UPDATE" => "Les détails du groupe {{name}} ont été enregistrés"
+ ],
+
+ "MANUALLY_ACTIVATED" => "Le compte de {{user_name}} a été activé manuellement",
+ "MASTER_ACCOUNT_EXISTS" => "Le compte principal existe déjà !",
+ "MIGRATION" => [
+ "REQUIRED" => "Mise à jour de la base de données requise"
+ ],
+
+ "PERMISSION" => [
+ 1 => "Autorisation",
+ 2 => "Autorisations",
+
+ "ASSIGN_NEW" => "Assigner une nouvelle autorisation",
+ "HOOK_CONDITION" => "Hook/Conditions",
+ "MANAGE" => "Gestion des autorisations",
+ "PAGE_DESCRIPTION" => "Une liste des autorisations pour votre site. Fournit des outils de gestion pour modifier et supprimer des autorisations.",
+ "UPDATE" => "Mettre à jour les autorisations"
+ ],
+
+ "ROLE" => [
+ 1 => "Rôle",
+ 2 => "Rôles",
+
+ "ASSIGN_NEW" => "Assigner un nouveau rôle",
+ "CREATE" => "Créer un rôle",
+ "CREATION_SUCCESSFUL" => "Rôle {{name}} créé avec succès",
+ "DELETE" => "Supprimer le rôle",
+ "DELETE_CONFIRM" => "Êtes-vous certain de vouloir supprimer le rôle {{name}}?",
+ "DELETE_DEFAULT" => "Vous ne pouvez pas supprimer le rôle {{name}} parce que c'est un rôle par défaut pour les utilisateurs nouvellement enregistrés.",
+ "DELETE_YES" => "Oui, supprimer le rôle",
+ "DELETION_SUCCESSFUL" => "Rôle {{name}} supprimé avec succès",
+ "EDIT" => "Modifier le rôle",
+ "HAS_USERS" => "Vous ne pouvez pas le faire parce qu'il y a encore des utilisateurs qui ont le rôle {{name}}.",
+ "INFO_PAGE" => "Page d'information pour le rôle {{name}}",
+ "MANAGE" => "Gérer les rôles",
+ "NAME" => "Nom du rôle",
+ "NAME_EXPLAIN" => "Spécifiez le nom du rôle",
+ "NAME_IN_USE" => "Un rôle nommé {{name}} existe déjà",
+ "PAGE_DESCRIPTION" => "Une liste des rôles de votre site. Fournit des outils de gestion pour modifier et supprimer des rôles.",
+ "PERMISSIONS_UPDATED" => "Autorisations mises à jour pour le rôle {{name}}",
+ "UPDATED" => "Détails mis à jour pour le rôle {{name}}"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "Informations sur le système",
+
+ "DB_NAME" => "Base de donnée",
+ "DB_VERSION" => "Version DB",
+ "DIRECTORY" => "Répertoire du projet",
+ "PHP_VERSION" => "Version de PHP",
+ "SERVER" => "Logiciel server",
+ "SPRINKLES" => "Sprinkles chargés",
+ "UF_VERSION" => "Version de UserFrosting",
+ "URL" => "Url racine"
+ ],
+
+ "USER" => [
+ 1 => "Utilisateur",
+ 2 => "Utilisateurs",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Changer le mot de passe",
+ "SEND_PASSWORD_LINK" => "Envoyer à l'utilisateur un lien qui lui permettra de choisir son propre mot de passe",
+ "SET_PASSWORD" => "Définissez le mot de passe de l'utilisateur comme"
+ ],
+
+ "ACTIVATE" => "Autoriser l'utilisateur",
+ "CREATE" => "Créer un utilisateur",
+ "CREATED" => "L'utilisateur {{user_name}} a été créé avec succès",
+ "DELETE" => "Supprimer l'utilisateur",
+ "DELETE_CONFIRM" => "Êtes-vous certain de vouloir supprimer l'utilisateur {{name}}?",
+ "DELETE_YES" => "Oui, supprimer l'utilisateur",
+ "DISABLE" => "Désactiver l'utilisateur",
+ "EDIT" => "Modifier l'utilisateur",
+ "ENABLE" => "Activer l'utilisateur",
+ "INFO_PAGE" => "Page d'information de l'utilisateur pour {{name}}",
+ "PAGE_DESCRIPTION" => "Une liste des utilisateurs de votre site. Fournit des outils de gestion incluant la possibilité de modifier les détails de l'utilisateur, d'activer manuellement les utilisateurs, d'activer / désactiver les utilisateurs et plus.",
+ "LATEST" => "Derniers utilisateurs",
+ "VIEW_ALL" => "Voir tous les utilisateurs"
+ ],
+ "X_USER" => [
+ 0 => "Aucun utilisateur",
+ 1 => "{{plural}} utilisateur",
+ 2 => "{{plural}} utilisateurs"
+ ]
+];
\ No newline at end of file
diff --git a/login/app/sprinkles/admin/locale/it_IT/messages.php b/login/app/sprinkles/admin/locale/it_IT/messages.php
new file mode 100755
index 0000000..c40d5b3
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/it_IT/messages.php
@@ -0,0 +1,160 @@
+ [
+ 1 => "Attività",
+ 2 => "Attività",
+
+ "LAST" => "Ultima attività",
+ "PAGE" => "Un elenco delle attività degli utenti",
+ "TIME" => "Tempo di attività"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Cancellare la cache",
+ "CLEAR_CONFIRM" => "Sei sicuro di voler cancellare la cache del sito?",
+ "CLEAR_CONFIRM_YES" => "Sì, cancellare la cache",
+ "CLEARED" => "La cache è stata eliminata correttamente!"
+ ],
+
+ "DASHBOARD" => "Pannello di Controllo",
+ "NO_FEATURES_YET" => "Non sembra che alcune funzioni siano state create per questo account ... ancora. Forse non sono ancora state implementate, o forse qualcuno ha dimenticato di dare accesso. In entrambi i casi, siamo contenti di averti qui!",
+ "DELETE_MASTER" => "Non puoi eliminare l'account principale!",
+ "DELETION_SUCCESSFUL" => "Hai eliminato utente {{user_name}}.",
+ "DETAILS_UPDATED" => "Dettagli degli account aggiornati per l'utente {{user_name}}",
+ "DISABLE_MASTER" => "Non puoi disattivare l'account principale!",
+ "DISABLE_SELF" => "Non puoi disattivare il tuo account!",
+ "DISABLE_SUCCESSFUL" => "Account per l'utente {{user_name}} disattivato con successo!",
+ "ENABLE_SUCCESSFUL" => "Account per l'utente {{user_name}} attivato con successo.",
+
+ "GROUP" => [
+ 1 => "Gruppo",
+ 2 => "Gruppi",
+
+ "CREATE" => "Creare un gruppo",
+ "CREATION_SUCCESSFUL" => "Ha creato con successo il gruppo {{name}}",
+ "DELETE" => "Elimina gruppo",
+ "DELETE_CONFIRM" => "Sei sicuro di voler eliminare il gruppo {{name}}?",
+ "DELETE_DEFAULT" => "Non puoi eliminare il gruppo {{name}} perché è il gruppo predefinito per gli utenti appena registrati.",
+ "DELETE_YES" => "Sì, elimini il gruppo",
+ "DELETION_SUCCESSFUL" => "Eliminato il gruppo {{name}} con successo",
+ "EDIT" => "Modifica gruppo",
+ "ICON" => "Icona del gruppo",
+ "ICON_EXPLAIN" => "Icona per i membri del gruppo",
+ "INFO_PAGE" => "Pagina informazioni di gruppo per {{name}}",
+ "MANAGE" => "Gestisci gruppo",
+ "NAME" => "Nome del gruppo",
+ "NAME_EXPLAIN" => "Inserisci un nome per il gruppo",
+ "NOT_EMPTY" => "Non puoi farlo perché ci sono ancora utenti associati al gruppo {{name}}.",
+ "PAGE_DESCRIPTION" => "Un elenco dei gruppi per il tuo sito. Fornisce strumenti di gestione per la modifica e l'eliminazione di gruppi.",
+ "SUMMARY" => "Riepilogo del gruppo",
+ "UPDATE" => "Dettagli aggiornati per il gruppo {{name}}."
+ ],
+
+ "MANUALLY_ACTIVATED" => "{{user_name}} è stato attivato manualmente",
+ "MASTER_ACCOUNT_EXISTS" => "L'account primario esiste già!",
+ "MIGRATION" => [
+ "REQUIRED" => "È necessario aggiornare il database"
+ ],
+
+ "PERMISSION" => [
+ 1 => "Autorizzazione",
+ 2 => "Autorizzazioni",
+
+ "ASSIGN_NEW" => "Assegna nuova autorizzazione",
+ "HOOK_CONDITION" => "Hook/Condizioni",
+ "ID" => "ID di autorizzazione",
+ "INFO_PAGE" => "Pagina di informazioni sulle autorizzazioni per {{name}}",
+ "MANAGE" => "Gestione delle autorizzazioni",
+ "NOTE_READ_ONLY" => "Si prega di notare: le autorizzazioni sono considerate \"parte del codice\" e non possono essere modificate tramite l'interfaccia. Per aggiungere, rimuovere o modificare le autorizzazioni, i gestori del sito devono utilizzare migrazione del database.",
+ "PAGE_DESCRIPTION" => "Un elenco delle autorizzazioni per il tuo sito. Fornisce strumenti di gestione per la modifica e l'eliminazione delle autorizzazioni.",
+ "SUMMARY" => "Sommario delle Autorizzazioni",
+ "UPDATE" => "Aggiorna le autorizzazioni",
+ "VIA_ROLES" => "Ha autorizzazione tramite ruoli"
+ ],
+
+ "ROLE" => [
+ 1 => "Ruolo",
+ 2 => "Ruoli",
+
+ "ASSIGN_NEW" => "Assegna un nuovo ruolo",
+ "CREATE" => "Crea ruolo",
+ "CREATION_SUCCESSFUL" => "Creato con successo il ruolo {{name}}",
+ "DELETE" => "Elimina il ruolo",
+ "DELETE_CONFIRM" => "Sei sicuro di voler eliminare il ruolo {{name}}?",
+ "DELETE_DEFAULT" => "Non puoi eliminare il ruolo {{name}} perché è un ruolo predefinito per gli utenti appena registrati.",
+ "DELETE_YES" => "Sì, elimini il ruolo",
+ "DELETION_SUCCESSFUL" => "Eliminato il ruolo {{name}}",
+ "EDIT" => "Modifica ruolo",
+ "HAS_USERS" => "Non puoi farlo perché ci sono ancora utenti che hanno il ruolo {{name}}.",
+ "INFO_PAGE" => "Pagina di informazioni sui ruoli per {{name}}",
+ "MANAGE" => "Gestisci Ruoli",
+ "NAME" => "Nome",
+ "NAME_EXPLAIN" => "Inserisci un nome per il ruolo",
+ "NAME_IN_USE" => "Esiste già un ruolo denominato {{name}}",
+ "PAGE_DESCRIPTION" => "Un elenco dei ruoli per il tuo sito. Fornisce strumenti di gestione per la modifica e l'eliminazione di ruoli.",
+ "PERMISSIONS_UPDATED" => "Autorizzazioni aggiornate per ruolo {{name}}",
+ "SUMMARY" => "Riepilogo dei Ruoli",
+ "UPDATED" => "Dettagli aggiornati per ruolo {{name}}"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "Informazioni sul sistema",
+
+ "DB_NAME" => "Nome del database",
+ "DB_VERSION" => "Versione del database",
+ "DIRECTORY" => "Directory del progetto",
+ "PHP_VERSION" => "Versione PHP",
+ "SERVER" => "Software del webserver",
+ "SPRINKLES" => "Sprinkles caricati",
+ "UF_VERSION" => "Versione UserFrosting",
+ "URL" => "Url della radice del sito"
+ ],
+
+ "TOGGLE_COLUMNS" => "Scambia le colonne",
+
+ "USER" => [
+ 1 => "Utente",
+ 2 => "Utenti",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Cambia Password Utente",
+ "SEND_PASSWORD_LINK" => "Inviare all'utente un collegamento che permetterà loro di scegliere la propria password",
+ "SET_PASSWORD" => "Impostare la password dell'utente come"
+ ],
+
+ "ACTIVATE" => "Attiva l'utente",
+ "CREATE" => "Creare un utente",
+ "CREATED" => "Account per l'utente {{user_name}} è stato creato.",
+ "DELETE" => "Elimina utente",
+ "DELETE_CONFIRM" => "Sei sicuro di voler eliminare l'utente {{name}}?",
+ "DELETE_YES" => "Sì, elimina l'utente",
+ "DISABLE" => "Disabilita l'utente",
+ "EDIT" => "Modifica utente",
+ "ENABLE" => "Abilita l'utente",
+ "INFO_PAGE" => "Pagina informazioni utente per {{name}}",
+ "LATEST" => "Ultimi Utenti",
+ "PAGE_DESCRIPTION" => "Un elenco degli utenti del tuo sito. Fornisce strumenti di gestione, tra cui la possibilità di modificare i dettagli utente, attivare manualmente gli utenti, abilitare / disabilitare gli utenti e altro ancora.",
+ "SUMMARY" => "Riepilogo account",
+ "VIEW_ALL" => "Visualizza tutti gli utenti",
+ "WITH_PERMISSION" => "Utenti con questa autorizzazione"
+ ],
+ "X_USER" => [
+ 0 => "Nessun utente",
+ 1 => "{{plural}} utente",
+ 2 => "{{plural}} utenti"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/pt_PT/messages.php b/login/app/sprinkles/admin/locale/pt_PT/messages.php
new file mode 100755
index 0000000..0faf818
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/pt_PT/messages.php
@@ -0,0 +1,139 @@
+ [
+ 1 => "Atividade",
+ 2 => "Atividades",
+
+ "LAST" => "Última atividade",
+ "PAGE" => "Lista de atividade dos utilizadores",
+ "TIME" => "Tempo da Atividade"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Limpar cache",
+ "CLEAR_CONFIRM" => "Tem a certeza que pretende limpar a cache do site?",
+ "CLEAR_CONFIRM_YES" => "Sim, limpar cache",
+ "CLEARED" => "Cache limpa com sucesso!"
+ ],
+
+ "DASHBOARD" => "Painel de Controlo",
+ "DELETE_MASTER" => "Não pode apagar a conta principal!",
+ "DELETION_SUCCESSFUL" => "Utilizador {{user_name}} foi removido com sucesso.",
+ "DETAILS_UPDATED" => "Detalhes de conta atualizados para o utilizador {{user_name}}",
+ "DISABLE_MASTER" => "Não pode desativar a conta principal!",
+ "DISABLE_SUCCESSFUL" => "Conta do utilizador {{user_name}} foi desativada com sucesso.",
+
+ "ENABLE_SUCCESSFUL" => "Conta do utilizador {{user_name}} foi ativada com sucesso.",
+
+ "GROUP" => [
+ 1 => "Grupo",
+ 2 => "Grupos",
+
+ "CREATE" => "Criar grupo",
+ "CREATION_SUCCESSFUL" => "Grupo criado com sucesso",
+ "DELETE" => "Remover grupo",
+ "DELETION_SUCCESSFUL" => "Grupo removido com sucesso",
+ "DELETE_CONFIRM" => "Tem a certeza que pretende remover o grupo {{name}}?",
+ "DELETE_YES" => "Sim, remover grupo",
+ "EDIT" => "Editar grupo",
+ "ICON" => "Icon do grupo",
+ "ICON_EXPLAIN" => "Icon para membros do grupo",
+ "INFO_PAGE" => "Página informativa do grupo {{name}}",
+ //"MANAGE" => "Manage group",
+ "NAME" => "Nome do grupo",
+ "NAME_EXPLAIN" => "Por favor introduza um nome para o grupo",
+ "PAGE_DESCRIPTION" => "Lista de grupos do site. Contém opções para editar e remover grupos."
+ ],
+
+ "MANUALLY_ACTIVATED" => "A conta de {{user_name}} foi ativada manualmente.",
+ "MASTER_ACCOUNT_EXISTS" => "A contra principal já existe!",
+ "MIGRATION" => [
+ "REQUIRED" => "É necessário uma atualização da base de dados."
+ ],
+
+ "PERMISSION" => [
+ 1 => "Permissão",
+ 2 => "Permissões",
+
+ "ASSIGN_NEW" => "Atribuir nova permissão",
+ "HOOK_CONDITION" => "Hook/Condições",
+ "MANAGE" => "Gerir permissões",
+ "PAGE_DESCRIPTION" => "Lista de permissões do site. Contém opções para editar e remover permissões.",
+ "UPDATE" => "Atualizar permissões"
+ ],
+
+ "ROLE" => [
+ 1 => "Cargo",
+ 2 => "Cargos",
+
+ "ASSIGN_NEW" => "Atribuir novo cargo",
+ "CREATE" => "Criar cargo",
+ "CREATION_SUCCESSFUL" => "Cargo criado com sucesso",
+ "DELETE" => "Remover cargo",
+ "DELETION_SUCCESSFUL" => "Cargo removido com sucesso",
+ "DELETE_CONFIRM" => "Tem a certeza que pretende remover o cargo {{name}}?",
+ "DELETE_YES" => "Sim, remover cargo",
+ "EDIT" => "Editar cargo",
+ "INFO_PAGE" => "Página informativa do cargo {{name}}",
+ "MANAGE" => "Gerir cargos",
+ "NAME" => "Nome",
+ "NAME_EXPLAIN" => "Por favor introduza um nome para o cargo",
+ "PAGE_DESCRIPTION" => "Lista de cargos do site. Contém opções para editar e remover cargos.",
+ "UPDATE" => "Atualizar cargos",
+ "UPDATED" => "Cargo {{name}} atualizado"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "Informação do sistema",
+
+ "DB_NAME" => "Nome da base de dados",
+ "DB_VERSION" => "Versão da base de dados",
+ "DIRECTORY" => "Diretório do projeto",
+ "PHP_VERSION" => "Versão PHP",
+ "SERVER" => "Software do servidor web",
+ "SPRINKLES" => "Sprinkles carregados",
+ "UF_VERSION" => "Versão do UserFrosting",
+ "URL" => "Raiz (url) do site"
+ ],
+
+ "USER" => [
+ 1 => "Utilizador",
+ 2 => "Utilizadores",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Alterar password",
+ "SEND_PASSWORD_LINK" => "Enviar um link ao utilizador que lhe permita escolher a sua password",
+ "SET_PASSWORD" => "Definir a password do utilizador como"
+ ],
+
+ "ACTIVATE" => "Ativar utilizador",
+ "CREATE" => "Criar utilizador",
+ "DELETE" => "Remover utilizador",
+ "DELETE_CONFIRM" => "Tem a certeza que pretende remover o utilizador {{name}}?",
+ "DELETE_YES" => "Sim, remover utilizador",
+ "DISABLE" => "Desativar utilizador",
+ "EDIT" => "Editar utilizador",
+ "ENABLE" => "Ativar utilizador",
+ "INFO_PAGE" => "Página informativa do utilizador {{name}}",
+ "PAGE_DESCRIPTION" => "Lista de utilizadores do site. Contém opções para editar detalhes, ativar/desativar utilizadores e outras.",
+ "LATEST" => "Últimos Utilizadores",
+ "VIEW_ALL" => "Ver todos os utilizadores"
+ ],
+ "X_USER" => [
+ 0 => "Nenhum utilizador",
+ 1 => "{{plural}} utilizador",
+ 2 => "{{plural}} utilizadores"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/ru_RU/messages.php b/login/app/sprinkles/admin/locale/ru_RU/messages.php
new file mode 100755
index 0000000..d6f6e1a
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/ru_RU/messages.php
@@ -0,0 +1,160 @@
+ [
+ 1 => "Активность",
+ 2 => "Активность",
+
+ "LAST" => "Последняя активность",
+ "PAGE" => "Список действий пользователя",
+ "TIME" => "Время действия"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Очистить кэш",
+ "CLEAR_CONFIRM" => "Уверены, что хотите очистить кэш сайта?",
+ "CLEAR_CONFIRM_YES" => "Да, очистить кэш",
+ "CLEARED" => "Кэш успешно очищен !"
+ ],
+
+ "DASHBOARD" => "Панель управления",
+ "NO_FEATURES_YET" => "Похоже, некоторые функции не были настроены для этого аккаунта... пока. Возможно, они еще не реализованы, или, может быть, кто-то забыл дать вам доступ. В любом случае, мы рады вас видеть здесь!",
+ "DELETE_MASTER" => "Нельзя удалить главный аккаунт!",
+ "DELETION_SUCCESSFUL" => "Пользователь {{user_name}} был успешно удален.",
+ "DETAILS_UPDATED" => "Данные для аккаунта {{user_name}} обновлены",
+ "DISABLE_MASTER" => "Нельзя отключить главный аккаунт!",
+ "DISABLE_SELF" => "Вы не можете отключить собственный аккаунт!",
+ "DISABLE_SUCCESSFUL" => "Пользователь {{user_name}} был успешно отключен.",
+
+ "ENABLE_SUCCESSFUL" => "Пользователь {{user_name}} был успешно включен.",
+
+ "GROUP" => [
+ 1 => "Группа",
+ 2 => "Группы",
+
+ "CREATE" => "Создать группу",
+ "CREATION_SUCCESSFUL" => "Успешно создана группа {{name}}",
+ "DELETE" => "Удалить группу",
+ "DELETE_CONFIRM" => "Вы уверены, что вы хотите удалить группу {{name}}?",
+ "DELETE_DEFAULT" => "Нельзя удалить группу {{name}}, потому что это группа по умолчанию для новых пользователей.",
+ "DELETE_YES" => "Да, удалить группу",
+ "DELETION_SUCCESSFUL" => "Успешно удалена группа {{name}}",
+ "EDIT" => "Редактор группы",
+ "ICON" => "Значок группы",
+ "ICON_EXPLAIN" => "Значок для членов группы",
+ "INFO_PAGE" => "Описание группы {{name}}",
+ "MANAGE" => "Управление группой",
+ "NAME" => "Имя группы",
+ "NAME_EXPLAIN" => "Пожалуйста, введите имя группы",
+ "NOT_EMPTY" => "Вы не можете сделать это, потому что до сих пор есть пользователи, связанные с группой {{name}}.",
+ "PAGE_DESCRIPTION" => "Список групп для вашего сайта. Предоставляет инструменты управления для редактирования и удаления групп.",
+ "SUMMARY" => "Резюме группы",
+ "UPDATE" => "Информация обновлена для группы {{name}}"
+ ],
+
+ "MANUALLY_ACTIVATED" => "{{user_name}} аккаунт был активирован вручную",
+ "MASTER_ACCOUNT_EXISTS" => "Мастер-аккаунт уже существует!",
+ "MIGRATION" => [
+ "REQUIRED" => "Необходимо обновление базы данных"
+ ],
+
+ "PERMISSION" => [
+ 1 => "Доступ",
+ 2 => "Права доступа",
+
+ "ASSIGN_NEW" => "Назначить новые права",
+ "HOOK_CONDITION" => "Привязки/Условия",
+ "ID" => "ID доступа",
+ "INFO_PAGE" => "Информация о доступах для '{{name}}'",
+ "MANAGE" => "Управление правами",
+ "NOTE_READ_ONLY" => "Пожалуйста, обратите внимание:права и доступы считаются \"частью кода\" и не могут быть изменены через интерфейс. Чтобы добавить, удалить или изменить доступы, нужно будет использовать сайт базы данных миграции.",
+ "PAGE_DESCRIPTION" => "Список прав доступа для вашего сайта. Предоставляет инструменты управления для редактирования и удаления прав.",
+ "SUMMARY" => "Сводка доступов",
+ "UPDATE" => "Обновление прав",
+ "VIA_ROLES" => "Имеет права через роли"
+ ],
+
+ "ROLE" => [
+ 1 => "Роль",
+ 2 => "Роли",
+
+ "ASSIGN_NEW" => "Назначение новой роли",
+ "CREATE" => "Создать роль",
+ "CREATION_SUCCESSFUL" => "Успешно создана роль {{name}}",
+ "DELETE" => "Удалить роль",
+ "DELETE_CONFIRM" => "Вы уверены, что вы хотите удалить роль {{name}}?",
+ "DELETE_DEFAULT" => "Невозможно удалить роль {{name}}, потому что это роль по умолчанию для новых пользователей.",
+ "DELETE_YES" => "Да, удалить роль",
+ "DELETION_SUCCESSFUL" => "Успешно удалена роль {{name}}",
+ "EDIT" => "Изменить роль",
+ "HAS_USERS" => "Вы не можете сделать это, потому что до сих пор есть пользователи, имеющие роль {{name}}.",
+ "INFO_PAGE" => "Информации роли для {{name}}",
+ "MANAGE" => "Управление ролями",
+ "NAME" => "Имя",
+ "NAME_EXPLAIN" => "Пожалуйста, укажите имя для роли",
+ "NAME_IN_USE" => "Роль с именем {{name}} уже существует",
+ "PAGE_DESCRIPTION" => "Список ролей для вашего сайта. Предоставляет инструменты управления для редактирования и удаления ролей.",
+ "PERMISSIONS_UPDATED" => "Права обновлены для роли {{name}}",
+ "SUMMARY" => "Основная информация",
+ "UPDATED" => "Информация для роли {{name}}"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "Системная информация",
+
+ "DB_NAME" => "Имя базы данных",
+ "DB_VERSION" => "Версия базы данных",
+ "DIRECTORY" => "Каталог проекта",
+ "PHP_VERSION" => "Версия PHP",
+ "SERVER" => "ПО Сервера",
+ "SPRINKLES" => "Загружены модули",
+ "UF_VERSION" => "Версия UserFrosting",
+ "URL" => "URL-адрес сайта"
+ ],
+
+ "TOGGLE_COLUMNS" => "Переключатель столбцов",
+
+ "USER" => [
+ 1 => "Пользователь",
+ 2 => "Пользователи",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Сменить пароль пользователя",
+ "SEND_PASSWORD_LINK" => "Отправить пользователю ссылку, которая позволит выбрать свой собственный пароль",
+ "SET_PASSWORD" => "Установите пароль пользователя как"
+ ],
+
+ "ACTIVATE" => "Активировать пользователя",
+ "CREATE" => "Создать пользователя",
+ "CREATED" => "Пользователь {{user_name}} был успешно создан",
+ "DELETE" => "Удалить пользователя",
+ "DELETE_CONFIRM" => "Вы уверены, что вы хотите удалить пользователя {{name}}?",
+ "DELETE_YES" => "Да, удалить пользователя",
+ "DELETED" => "Пользователь удален",
+ "DISABLE" => "Отключить пользователя",
+ "EDIT" => "Изменить пользователя",
+ "ENABLE" => "Включить пользователя",
+ "INFO_PAGE" => "Информация о пользователе для {{name}}",
+ "LATEST" => "Последние пользователи",
+ "PAGE_DESCRIPTION" => "Список пользователей для вашего сайта. Предоставляет средства управления, включая возможность редактирования сведений о пользователях, ручной активации пользователей, включения/отключения пользователей и многое другое.",
+ "SUMMARY" => "Сводка аккаунта",
+ "VIEW_ALL" => "Все пользователи",
+ "WITH_PERMISSION" => "Пользователи с этим доступом"
+ ],
+ "X_USER" => [
+ 0 => "Нет пользователей",
+ 1 => "{{plural}} пользователя",
+ 2 => "{{plural}} пользователей"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/th_TH/messages.php b/login/app/sprinkles/admin/locale/th_TH/messages.php
new file mode 100755
index 0000000..546232d
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/th_TH/messages.php
@@ -0,0 +1,134 @@
+ [
+ 1 => "กิจกรรม",
+ 2 => "กิจกรรม",
+
+ "LAST" => "กิจกรรมล่าสุด",
+ "PAGE" => "รายการกิจกรรมของผู้ใช้",
+ "TIME" => "เวลาที่ทำกิจกรรม"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "ล้างแคช",
+ "CLEAR_CONFIRM" => "คุณแน่ใจหรือที่จะล้างแคชของเว็บ?",
+ "CLEAR_CONFIRM_YES" => "ใช่ ล้างแคชเลย",
+ "CLEARED" => "ล้างแคชเรียบร้อยแล้ว!"
+ ],
+
+ "DASHBOARD" => "แผงควบคุม",
+ "DELETE_MASTER" => "คุณไม่สามารถลบบัญชีหลักได้!",
+ "DELETION_SUCCESSFUL" => "ลบผู้ใช้ {{user_name}} เรียบร้อยแล้ว",
+ "DETAILS_UPDATED" => "ปรับปรุงรายระเอียดบัญชีให้กับ {{user_name}} แล้ว",
+ "DISABLE_MASTER" => "คุณไม่สามารถปิดการใช้งานบัญชีหลัก!",
+ "DISABLE_SUCCESSFUL" => "ปิดการใช้งานบัญชีของผู้ใช้ {{user_name}} เรียบร้อยแล้ว",
+
+ "ENABLE_SUCCESSFUL" => "เปิดการใช้งานบัญชีของผู้ใช้ {{user_name}} เรียบร้อยแล้ว",
+
+ "GROUP" => [
+ 1 => "กลุ่ม",
+ 2 => "กลุ่ม",
+
+ "CREATE" => "สร้างกลุ่ม",
+ "DELETE" => "ลบกลุ่ม",
+ "DELETE_CONFIRM" => "คุณแน่ใจหรือที่จะลบกลุ่ม {{name}}?",
+ "DELETE_YES" => "ใช่ ลบกลุ่มนี้เลย",
+ "EDIT" => "แก้ไขกลุ่ม",
+ "ICON" => "ไอคอนกลุ่ม",
+ "ICON_EXPLAIN" => "ไอคอนสำหรับสมาชิกกลุ่ม",
+ "INFO_PAGE" => "หน้าข้อมูลกลุ่มสำหรับ {{name}}",
+ //"MANAGE" => "Manage group",
+ "NAME" => "ชื่อกลุ่ม",
+ "NAME_EXPLAIN" => "กรุณาตั้งชื่อสำหรับกลุ่มนี้",
+ "PAGE_DESCRIPTION" => "รายชื่อกลุ่มในเว็บของคุณ ประกอบไปด้วยเครื่องมือในการจัดการสำหรับการแก้ไขและลบกลุ่ม"
+ ],
+
+ "MANUALLY_ACTIVATED" => "บัญชีของ {{user_name}} ได้เปิดใช้งานเองแล้ว",
+ "MASTER_ACCOUNT_EXISTS" => "มีบัญชีหลักอยู่แล้ว!",
+ "MIGRATION" => [
+ "REQUIRED" => "ต้องการการปรับปรุงฐานข้อมูล"
+ ],
+
+ "PERMISSION" => [
+ 1 => "สิทธิการเข้าถึง",
+ 2 => "สิทธิการเข้าถึง",
+
+ "ASSIGN_NEW" => "กำหนดสิทธิการเข้าถึงใหม่",
+ "HOOK_CONDITION" => "ข้อกำหนด/เงื่อนไข",
+ "MANAGE" => "จัดการสิทธิการเข้าถึง",
+ "PAGE_DESCRIPTION" => "รายการสิทธิการเข้าถึงในเว็บของคุณ ประกอบไปด้วยเครื่องมือในการจัดการสำหรับการแก้ไขและลบสิทธิการเข้าถึง",
+ "UPDATE" => "ปรับปรุงสิทธิการเข้าถึง"
+ ],
+
+ "ROLE" => [
+ 1 => "ตำแหน่ง",
+ 2 => "ตำแหน่ง",
+
+ "ASSIGN_NEW" => "กำหนดตำแหน่งใหม่",
+ "CREATE" => "สร้างตำแหน่ง",
+ "DELETE" => "ลบตำแหน่ง",
+ "DELETE_CONFIRM" => "คุณแน่ใจหรือที่จะลบตำแหน่ง {{name}}?",
+ "DELETE_YES" => "ใช่ ลบตำแหน่งนี้เลย",
+ "EDIT" => "แก้ไขตำแหน่ง",
+ "INFO_PAGE" => "หน้าข้อมูลตำแหน่งสำหรับ {{name}}",
+ "MANAGE" => "จัดการตำแหน่ง",
+ "NAME" => "ชื่อ",
+ "NAME_EXPLAIN" => "กรุณาตั้งชื่อสำหรับตำแหน่งนี้",
+ "PAGE_DESCRIPTION" => "รายชื่อตำแหน่งในเว็บของคุณ ประกอบไปด้วยเครื่องมือในการจัดการสำหรับแก้ไขและลบตำแหน่ง",
+ "UPDATED" => "ปรับปรุงตำแหน่ง"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "ข้อมูลระบบ",
+
+ "DB_NAME" => "ชื่อฐานข้อมูล",
+ "DB_VERSION" => "เวอร์ชั่นฐานข้อมูล",
+ "DIRECTORY" => "ไดเรกทอรีของโปรเจค",
+ "PHP_VERSION" => "เวอร์ชั่น PHP",
+ "SERVER" => "ซอฟต์แวร์เว็บเซิร์ฟเวอร์",
+ "SPRINKLES" => "Sprinkles ที่ถูกโหลด",
+ "UF_VERSION" => "เวอร์ชั่น UserFrosting",
+ "URL" => "URL ของรากเว็บไซต์"
+ ],
+
+ "USER" => [
+ 1 => "ผู้ใช้",
+ 2 => "ผู้ใช้",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "เปลี่ยนรหัสผ่านผู้ใช้",
+ "SEND_PASSWORD_LINK" => "ส่งลิงก์ที่จะอนุญาตให้ผู้ใช้เลือกรหัสผ่านเองให้กับผู้ใช้",
+ "SET_PASSWORD" => "ตั้งรหัสผ่านของผู้ใช้เป็น"
+ ],
+
+ "ACTIVATE" => "เปิดใช้งานผู้ใช้",
+ "CREATE" => "สร้างผู้ใช้",
+ "DELETE" => "ลบผู้ใช้",
+ "DELETE_CONFIRM" => "คุณแน่ใจหรือที่จะลบผู้ใช้ {{name}}?",
+ "DELETE_YES" => "ใช่ ลบผู้ใช้นี้เลย",
+ "DISABLE" => "ปิดการใช้งานผู้ใช้",
+ "EDIT" => "แก้ไขผู้ใช้",
+ "ENABLE" => "เปิดการใช้งานผู้ใช้",
+ "INFO_PAGE" => "หน้าข้อมูลของผู้ใช้ {{name}}",
+ "PAGE_DESCRIPTION" => "รายชื่อผู้ใช้ในเว็บของคุณ ประกอบไปด้วยเครื่องมือสำหรับการจัดการ รวมทั้งความสามารถในการแก้ไขรายละเอียดผู้ใช้ การเปิดใช้งานผู้ใช้ การเปิด/ปิดบัญชีผู้ใช้และอื่น ๆ",
+ "LATEST" => "ผู้ใช้ล่าสุด",
+ "VIEW_ALL" => "ดูผู้ใช้ทั้งหมด"
+ ],
+ "X_USER" => [
+ 0 => "ไม่มีผู้ใช้",
+ 1 => "{{plural}} ผู้ใช้",
+ 2 => "{{plural}} ผู้ใช้"
+ ]
+];
\ No newline at end of file
diff --git a/login/app/sprinkles/admin/locale/tr/messages.php b/login/app/sprinkles/admin/locale/tr/messages.php
new file mode 100755
index 0000000..25d5633
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/tr/messages.php
@@ -0,0 +1,160 @@
+ [
+ 1 => "Etkinlik",
+ 2 => "Etkinlikler",
+
+ "LAST" => "Son Etkinlik",
+ "PAGE" => "Kullanıcı etkinliklerinin listesi",
+ "TIME" => "Aktivite zamanı"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "Önbelleği temizle",
+ "CLEAR_CONFIRM" => "Site önbelleğini temizlemek istediğine emin misin?",
+ "CLEAR_CONFIRM_YES" => "Evet, önbelleği temizle",
+ "CLEARED" => "Önbellek temizlenmesi başarıyla tamamlandı!"
+ ],
+
+ "DASHBOARD" => "Pano",
+ "NO_FEATURES_YET" => "Bu hesap için herhangi bir özellik ayarlanmış gibi görünmüyor... Henüz. Belki de henüz uygulanmadı, veya belki birisi size erişim vermeyi unutttu. Her iki durumda da sizi aramızda gördüğümüze sevindik!",
+ "DELETE_MASTER" => "Ana hesabı silemezsiniz!",
+ "DELETION_SUCCESSFUL" => "Kullanıcı{{user_name}} silme işlemi başarıyla tamamlandı.",
+ "DETAILS_UPDATED" => "Kullanıcı{{user_name}} için güncel hesap detayları",
+ "DISABLE_MASTER" => "Ana hesabı devre dışı bırakamazsınız!",
+ "DISABLE_SELF" => "Kendi hesabınızın etkinliğini sonlandıramazsınız!",
+ "DISABLE_SUCCESSFUL" => "Kullanıcı hesabın {{user_name}}başarıyla devre dışı bırakıldı.",
+
+ "ENABLE_SUCCESSFUL" => "Kullanıcı hesabın{{user_name}}başarıyla etkinleştirildi.",
+
+ "GROUP" => [
+ 1 => "Grup",
+ 2 => "Grıplar",
+
+ "CREATE" => "Grup oluşturmak",
+ "CREATION_SUCCESSFUL" => "Grup oluşturma başarılı{{name}}",
+ "DELETE" => "Grubu sil",
+ "DELETE_CONFIRM" => "Grubu silmek istediğine emin misin{{name}}?",
+ "DELETE_DEFAULT" => "Grubu silemezsin{{name}} çünkü o yeni kayıtlanan kullanıcılar için varsayılan grup.",
+ "DELETE_YES" => "Evet, grubu sil",
+ "DELETION_SUCCESSFUL" => "Grup silme başarılı{{name}}",
+ "EDIT" => "Grubu düzenle",
+ "ICON" => "Grup ikonu",
+ "ICON_EXPLAIN" => "Grup iyileri için ikon",
+ "INFO_PAGE" => "Grup bilgisi sayfası {{name}} için",
+ "MANAGE" => "Grubu yönet",
+ "NAME" => "Grup adı",
+ "NAME_EXPLAIN" => "Lütfen grup için bir isim giriniz",
+ "NOT_EMPTY" => "Bunu yapamazsınız çünkü hala grupla ilişkili kullanıcılar var{{name}}.",
+ "PAGE_DESCRIPTION" => "Siten için grupların bir listesi. Grupları silmek ve düzenlemek için yönetim araçları sağlar.",
+ "SUMMARY" => "Grup özeti",
+ "UPDATE" => "Grup için detaylar güncellendi{{name}}"
+ ],
+
+ "MANUALLY_ACTIVATED" => "{{user_name}}'ın hesabı el ile aktifleştirildi",
+ "MASTER_ACCOUNT_EXISTS" => "Ana hesap zaten mevcut!",
+ "MIGRATION" => [
+ "REQUIRED" => "Veritabanını güncellemek gerek"
+ ],
+
+ "PERMISSION" => [
+ 1 => "İzin",
+ 2 => "İzinler",
+
+ "ASSIGN_NEW" => "Yeni izin ata",
+ "HOOK_CONDITION" => "Kanca/Koşullar",
+ "ID" => "İzin Kimliği",
+ "INFO_PAGE" => "{{name}} için izin bilgi sayfası",
+ "MANAGE" => "İzinleri yönet",
+ "NOTE_READ_ONLY" => "Lütfen Dikkat izinler ''bir kodun parçası'' olarak kabul edilir ve arayüz aracılığıyla değiştirilemez. İzln eklemek, kaldırmak ya da değiştirmek için site bakımcıları bir veritabanı geçişi kullanmalıdır",
+ "PAGE_DESCRIPTION" => "Siteniz için izinlerin bir listesi. Düzenleme yapmak ve izinleri kaldırmak yönetim araçları temin eder.",
+ "SUMMARY" => "İzin Özeti",
+ "UPDATE" => "İzinlerin Güncellenmesi",
+ "VIA_ROLES" => "Roller ile izin alımı"
+ ],
+
+ "ROLE" => [
+ 1 => "Rol",
+ 2 => "Roller",
+
+ "ASSIGN_NEW" => "Yeni rol ata",
+ "CREATE" => "Rol oluştur",
+ "CREATION_SUCCESSFUL" => "Rol oluşturma başarılı {{name}}",
+ "DELETE" => "Rolü sil",
+ "DELETE_CONFIRM" => "Rolü silmek istediğine emin misin {{name}}?",
+ "DELETE_DEFAULT" => "Rolü silemezsin {{name}} çünkü o kaydolmuş kullanıcılar için varsayılan bir rol.",
+ "DELETE_YES" => "Evet, rolü sil",
+ "DELETION_SUCCESSFUL" => "Rol başarıyla silindi{{name}}",
+ "EDIT" => "Rolü düzenle",
+ "HAS_USERS" => "Bunu yapamazsın çünkü hala bu rol ile bağlantılı kullanıcılar var{{name}}.",
+ "INFO_PAGE" => "{{name}} için rol bilgi sayfası",
+ "MANAGE" => "Rolleri yönet",
+ "NAME" => "Ad",
+ "NAME_EXPLAIN" => "Lütfen rol için bir ad giriniz",
+ "NAME_IN_USE" => "{{name}} adında bir rol zaten mevcut",
+ "PAGE_DESCRIPTION" => "Siteniz için rollerin bir listesi. Düzenlemek ve rolleri silmek için yönetim araçları sağlar.",
+ "PERMISSIONS_UPDATED" => "Rol için izinler güncellendi{{name}}",
+ "SUMMARY" => "Rol özeti",
+ "UPDATED" => "Rol için detaylar güncellendi{{name}}"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "Sistem bilgisi",
+
+ "DB_NAME" => "Veritabanı adı",
+ "DB_VERSION" => "Veritabanı sürümü",
+ "DIRECTORY" => "Proje dizini",
+ "PHP_VERSION" => "PHP sürümü",
+ "SERVER" => "Web sunucu yazılımı",
+ "SPRINKLES" => "Yüklü serpintiler",
+ "UF_VERSION" => "UserFrosting sürümü",
+ "URL" => "Site kök url"
+ ],
+
+ "TOGGLE_COLUMNS" => "Sütünları değiştirme",
+
+ "USER" => [
+ 1 => "Kullanıcı",
+ 2 => "Kullanıcılar",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "Kullanıcı şifresini değiştir",
+ "SEND_PASSWORD_LINK" => "Kullanıcıya kendi şifresini seçebileceği bir bağlantı gönder",
+ "SET_PASSWORD" => "Kullanıcının şifresi olarak ayarla"
+ ],
+
+ "ACTIVATE" => "Aktif Kullanıcı",
+ "CREATE" => "Kullanıcı oluştur",
+ "CREATED" => "Kullanıcı {{user_name}} başarıyla oluşturuldu",
+ "DELETE" => "Kullanıcıyı sil",
+ "DELETE_CONFIRM" => "Kullanıcıyı silmek istediğinden emin misin?{{name}}?",
+ "DELETE_YES" => "Evet, kullanıcıyı sil",
+ "DELETED" => "Kullanıcı silindi",
+ "DISABLE" => "Kullanıcı devre dışı",
+ "EDIT" => "Kullanıcıyı düzenle",
+ "ENABLE" => "Kullanıcı etkin",
+ "INFO_PAGE" => "{{name}} için kullanıcı bilgisi",
+ "LATEST" => "Son Kullanıcılar",
+ "PAGE_DESCRIPTION" => "Siten için kullanıcıların listesi. Kullanıcı detaylarını düzenlemek, elle kullanıcıları aktifleştirmek, kullanıcıları etkinleştirme/devre dışı bırakma, ve daha fazlası için yönetimsel araçlar sağlar.",
+ "SUMMARY" => "Hesap özeti",
+ "VIEW_ALL" => "Tüm kullanıcıları göster",
+ "WITH_PERMISSION" => "Bu izni olan kullanıcılar"
+ ],
+ "X_USER" => [
+ 0 => "Kullanıcı yok",
+ 1 => "{{plural}} kullanıcı",
+ 2 => "{{plural}} kullanıcılar"
+ ]
+];
diff --git a/login/app/sprinkles/admin/locale/zh_CN/messages.php b/login/app/sprinkles/admin/locale/zh_CN/messages.php
new file mode 100755
index 0000000..2adc8c8
--- /dev/null
+++ b/login/app/sprinkles/admin/locale/zh_CN/messages.php
@@ -0,0 +1,161 @@
+ [
+ 1 => "活动",
+ 2 => "活动",
+
+ "LAST" => "最后活动",
+ "PAGE" => "用户活动列表",
+ "TIME" => "活动时间"
+ ],
+
+ "ADMIN" => [
+ "PANEL" => "控制台"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "清除缓存",
+ "CLEAR_CONFIRM" => "你确定要清除此网站的缓存?",
+ "CLEAR_CONFIRM_YES" => "是的, 清除缓存",
+ "CLEARED" => "缓存成功清除 !"
+ ],
+
+ "DASHBOARD" => "仪表盘",
+ "DELETE_MASTER" => "你不能删除超级账户!",
+ "DELETION_SUCCESSFUL" => "用户 {{user_name}} 删除成功.",
+ "DETAILS_UPDATED" => "用户 {{user_name}} 更新成功",
+ "DISABLE_MASTER" => "你不能禁用超级用户!",
+ "DISABLE_SELF" => "不能禁用你自己的账户!",
+ "DISABLE_SUCCESSFUL" => "用户名{{user_name}} 成功禁用.",
+
+ "ENABLE_SUCCESSFUL" => "用户名 {{user_name}} 启用成功.",
+
+ "GROUP" => [
+ 1 => "组",
+ 2 => "组",
+
+ "CREATE" => "新建组",
+ "CREATION_SUCCESSFUL" => "成功创建组 {{name}}",
+ "DELETE" => "删除组",
+ "DELETE_CONFIRM" => "确定要删除组 {{name}}?",
+ "DELETE_DEFAULT" => "你无法删除组 {{name}} 因为这是新注册用户的默认组.",
+ "DELETE_YES" => "是, 删除组",
+ "DELETION_SUCCESSFUL" => "成功删除组{{name}}",
+ "EDIT" => "编辑组",
+ "ICON" => "组图标",
+ "ICON_EXPLAIN" => "组成员图标",
+ "INFO_PAGE" => "{{name}} 组的信息页",
+ "NAME" => "组名",
+ "NAME_EXPLAIN" => "请为组取一个名字",
+ "NOT_EMPTY" => "你不能这样做,因为还有用户在组 {{name}}.",
+ "PAGE_DESCRIPTION" => "网站组列表. 请管理编辑和删除组的工具.",
+ "SUMMARY" => "组简介",
+ "UPDATE" => "组{{name}}的信息已经更新"
+ ],
+
+ "MANUALLY_ACTIVATED" => "{{user_name}} 账户已手动激活",
+ "MASTER_ACCOUNT_EXISTS" => "超级账户已存在!",
+ "MIGRATION" => [
+ "REQUIRED" => "数据库需要更新"
+ ],
+
+ "PERMISSION" => [
+ 1 => "权限",
+ 2 => "权限",
+
+ "ASSIGN_NEW" => "分配新权限",
+ "HOOK_CONDITION" => "Hook/条件",
+ "ID" => "权限ID",
+ "INFO_PAGE" => "用户 '{{name}}' 的权限页",
+ "MANAGE" => "管理权限",
+ "NOTE_READ_ONLY" => "请注意: 权限是 \"part of the code\" 不能通过界面修改. 如果要添加、删除、编辑权限, 网站维护者需要 进行数据库迁移.",
+ "PAGE_DESCRIPTION" => "网站的权限列表. 提供编辑和删除权限的工具.",
+ "SUMMARY" => "权限概要",
+ "UPDATE" => "更新权限",
+ "VIA_ROLES" => "权限通过用户了"
+ ],
+
+ "ROLE" => [
+ 1 => "角色",
+ 2 => "角色",
+
+ "ASSIGN_NEW" => "分配角色",
+ "CREATE" => "创建角色",
+ "CREATION_SUCCESSFUL" => "成功创建角色 {{name}}",
+ "DELETE" => "删除角色",
+ "DELETE_CONFIRM" => "你确定要删除角色 {{name}}?",
+ "DELETE_DEFAULT" => "你无法删除角色{{name}} 因为这是新注册用户的默认角色.",
+ "DELETE_YES" => "是, 删除角色",
+ "DELETION_SUCCESSFUL" => "成功删除角色 {{name}}",
+ "EDIT" => "编辑角色",
+ "HAS_USERS" => "你不能这样做,因为还有用户拥有角色 {{name}}.",
+ "INFO_PAGE" => "{{name}}角色的权限页",
+ "MANAGE" => "管理角色",
+ "NAME" => "名字",
+ "NAME_EXPLAIN" => "请为角色输入名字",
+ "NAME_IN_USE" => "角色名 {{name}} 以存在",
+ "PAGE_DESCRIPTION" => "网站的角色列表. 请管理编辑和删除角色的工具.",
+ "PERMISSIONS_UPDATED" => "角色 {{name}} 的权限更新成功",
+ "SUMMARY" => "角色概要",
+ "UPDATED" => "角色 {{name}} 更新成功"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "系统信息",
+
+ "DB_NAME" => "数据库",
+ "DB_VERSION" => "数据库版本",
+ "DIRECTORY" => "工程目录",
+ "PHP_VERSION" => "PHP 版本",
+ "SERVER" => "网站服务器",
+ "SPRINKLES" => "加载的 sprinkles",
+ "UF_VERSION" => "UserFrosting 版本",
+ "URL" => "网站根目录"
+ ],
+
+ "TOGGLE_COLUMNS" => "加载列",
+
+ "USER" => [
+ 1 => "用户",
+ 2 => "用户",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "修改用户密码",
+ "SEND_PASSWORD_LINK" => "发送允许该用户修改密码的链接",
+ "SET_PASSWORD" => "设置用户的密码为"
+ ],
+
+ "ACTIVATE" => "激活用户",
+ "CREATE" => "新建用户",
+ "CREATED" => "用户 {{user_name}} 新建成功",
+ "DELETE" => "删除用户",
+ "DELETE_CONFIRM" => "确定要删除用户 {{name}}?",
+ "DELETE_YES" => "是, 删除用户",
+ "DISABLE" => "禁用用户",
+ "EDIT" => "编辑用户",
+ "ENABLE" => "启用用户",
+ "INFO_PAGE" => "用户 {{name}} 的信息页",
+ "LATEST" => "最新用户",
+ "PAGE_DESCRIPTION" => "网站用户列表. 提供编辑用户信息, 手动激活用户, 启用/禁用 用户等工具.",
+ "SUMMARY" => "账户概要",
+ "VIEW_ALL" => "浏览所有用户",
+ "WITH_PERMISSION" => "有此权限的用户"
+ ],
+ "X_USER" => [
+ 0 => "没有用户",
+ 1 => "{{plural}} 用户",
+ 2 => "{{plural}} 用户"
+ ]
+];
diff --git a/login/app/sprinkles/admin/routes/activities.php b/login/app/sprinkles/admin/routes/activities.php
new file mode 100755
index 0000000..b324553
--- /dev/null
+++ b/login/app/sprinkles/admin/routes/activities.php
@@ -0,0 +1,19 @@
+group('/activities', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\ActivityController:pageList')
+ ->setName('uri_activities');
+})->add('authGuard');
+
+$app->group('/api/activities', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\ActivityController:getList');
+})->add('authGuard');
diff --git a/login/app/sprinkles/admin/routes/admin.php b/login/app/sprinkles/admin/routes/admin.php
new file mode 100755
index 0000000..1a8c31a
--- /dev/null
+++ b/login/app/sprinkles/admin/routes/admin.php
@@ -0,0 +1,23 @@
+group('/dashboard', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\AdminController:pageDashboard')
+ ->setName('dashboard');
+})->add('authGuard');
+
+$app->group('/api/dashboard', function () {
+ $this->post('/clear-cache', 'UserFrosting\Sprinkle\Admin\Controller\AdminController:clearCache');
+})->add('authGuard');
+
+$app->group('/modals/dashboard', function () {
+ $this->get('/clear-cache', 'UserFrosting\Sprinkle\Admin\Controller\AdminController:getModalConfirmClearCache');
+})->add('authGuard');
diff --git a/login/app/sprinkles/admin/routes/groups.php b/login/app/sprinkles/admin/routes/groups.php
new file mode 100755
index 0000000..e861960
--- /dev/null
+++ b/login/app/sprinkles/admin/routes/groups.php
@@ -0,0 +1,39 @@
+group('/groups', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:pageList')
+ ->setName('uri_groups');
+
+ $this->get('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:pageInfo');
+})->add('authGuard');
+
+$app->group('/api/groups', function () {
+ $this->delete('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:delete');
+
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getList');
+
+ $this->get('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getInfo');
+
+ $this->get('/g/{slug}/users', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getUsers');
+
+ $this->post('', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:create');
+
+ $this->put('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:updateInfo');
+})->add('authGuard');
+
+$app->group('/modals/groups', function () {
+ $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getModalConfirmDelete');
+
+ $this->get('/create', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getModalCreate');
+
+ $this->get('/edit', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getModalEdit');
+})->add('authGuard');
diff --git a/login/app/sprinkles/admin/routes/permissions.php b/login/app/sprinkles/admin/routes/permissions.php
new file mode 100755
index 0000000..4df04c8
--- /dev/null
+++ b/login/app/sprinkles/admin/routes/permissions.php
@@ -0,0 +1,25 @@
+group('/permissions', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:pageList')
+ ->setName('uri_permissions');
+
+ $this->get('/p/{id}', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:pageInfo');
+})->add('authGuard');
+
+$app->group('/api/permissions', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:getList');
+
+ $this->get('/p/{id}', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:getInfo');
+
+ $this->get('/p/{id}/users', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:getUsers');
+})->add('authGuard');
diff --git a/login/app/sprinkles/admin/routes/roles.php b/login/app/sprinkles/admin/routes/roles.php
new file mode 100755
index 0000000..1de12e8
--- /dev/null
+++ b/login/app/sprinkles/admin/routes/roles.php
@@ -0,0 +1,45 @@
+group('/roles', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:pageList')
+ ->setName('uri_roles');
+
+ $this->get('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:pageInfo');
+})->add('authGuard');
+
+$app->group('/api/roles', function () {
+ $this->delete('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:delete');
+
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getList');
+
+ $this->get('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getInfo');
+
+ $this->get('/r/{slug}/permissions', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getPermissions');
+
+ $this->get('/r/{slug}/users', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getUsers');
+
+ $this->post('', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:create');
+
+ $this->put('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:updateInfo');
+
+ $this->put('/r/{slug}/{field}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:updateField');
+})->add('authGuard');
+
+$app->group('/modals/roles', function () {
+ $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalConfirmDelete');
+
+ $this->get('/create', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalCreate');
+
+ $this->get('/edit', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalEdit');
+
+ $this->get('/permissions', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalEditPermissions');
+})->add('authGuard');
diff --git a/login/app/sprinkles/admin/routes/users.php b/login/app/sprinkles/admin/routes/users.php
new file mode 100755
index 0000000..f1b2243
--- /dev/null
+++ b/login/app/sprinkles/admin/routes/users.php
@@ -0,0 +1,51 @@
+group('/users', function () {
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\UserController:pageList')
+ ->setName('uri_users');
+
+ $this->get('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:pageInfo');
+})->add('authGuard');
+
+$app->group('/api/users', function () {
+ $this->delete('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:delete');
+
+ $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getList');
+
+ $this->get('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getInfo');
+
+ $this->get('/u/{user_name}/activities', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getActivities');
+
+ $this->get('/u/{user_name}/roles', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getRoles');
+
+ $this->get('/u/{user_name}/permissions', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getPermissions');
+
+ $this->post('', 'UserFrosting\Sprinkle\Admin\Controller\UserController:create');
+
+ $this->post('/u/{user_name}/password-reset', 'UserFrosting\Sprinkle\Admin\Controller\UserController:createPasswordReset');
+
+ $this->put('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:updateInfo');
+
+ $this->put('/u/{user_name}/{field}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:updateField');
+})->add('authGuard');
+
+$app->group('/modals/users', function () {
+ $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalConfirmDelete');
+
+ $this->get('/create', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalCreate');
+
+ $this->get('/edit', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalEdit');
+
+ $this->get('/password', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalEditPassword');
+
+ $this->get('/roles', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalEditRoles');
+})->add('authGuard');
diff --git a/login/app/sprinkles/admin/schema/requests/group/create.yaml b/login/app/sprinkles/admin/schema/requests/group/create.yaml
new file mode 100755
index 0000000..8f5261c
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/group/create.yaml
@@ -0,0 +1,35 @@
+---
+name:
+ validators:
+ required:
+ label: "&NAME"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&NAME"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+slug:
+ validators:
+ required:
+ label: "&SLUG"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&SLUG"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+icon:
+ validators:
+ length:
+ label: "&ICON"
+ min: 1
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+ required:
+ message: VALIDATE.REQUIRED
+description:
diff --git a/login/app/sprinkles/admin/schema/requests/group/edit-info.yaml b/login/app/sprinkles/admin/schema/requests/group/edit-info.yaml
new file mode 100755
index 0000000..6aa3f28
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/group/edit-info.yaml
@@ -0,0 +1,27 @@
+---
+name:
+ validators:
+ length:
+ label: "&NAME"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+slug:
+ validators:
+ length:
+ label: "&SLUG"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+icon:
+ validators:
+ length:
+ label: "&ICON"
+ min: 1
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+description:
diff --git a/login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml b/login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml
new file mode 100755
index 0000000..2aa41b5
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml
@@ -0,0 +1,6 @@
+---
+slug:
+ validators:
+ required:
+ label: "&SLUG"
+ message: VALIDATE.REQUIRED
diff --git a/login/app/sprinkles/admin/schema/requests/role/create.yaml b/login/app/sprinkles/admin/schema/requests/role/create.yaml
new file mode 100755
index 0000000..8004184
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/role/create.yaml
@@ -0,0 +1,26 @@
+---
+name:
+ validators:
+ required:
+ label: "&NAME"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&NAME"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+slug:
+ validators:
+ required:
+ label: "&SLUG"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&SLUG"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+description:
diff --git a/login/app/sprinkles/admin/schema/requests/role/edit-field.yaml b/login/app/sprinkles/admin/schema/requests/role/edit-field.yaml
new file mode 100755
index 0000000..05c1b2d
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/role/edit-field.yaml
@@ -0,0 +1,24 @@
+---
+name:
+ validators:
+ length:
+ label: "&NAME"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+slug:
+ validators:
+ length:
+ label: "&SLUG"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+description:
+permissions:
+ validators:
+ array:
+ message: VALIDATE.ARRAY
diff --git a/login/app/sprinkles/admin/schema/requests/role/edit-info.yaml b/login/app/sprinkles/admin/schema/requests/role/edit-info.yaml
new file mode 100755
index 0000000..1fa36c8
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/role/edit-info.yaml
@@ -0,0 +1,20 @@
+---
+name:
+ validators:
+ length:
+ label: "&NAME"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+slug:
+ validators:
+ length:
+ label: "&SLUG"
+ min: 1
+ max: 255
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+description:
diff --git a/login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml b/login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml
new file mode 100755
index 0000000..2aa41b5
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml
@@ -0,0 +1,6 @@
+---
+slug:
+ validators:
+ required:
+ label: "&SLUG"
+ message: VALIDATE.REQUIRED
diff --git a/login/app/sprinkles/admin/schema/requests/user/create.yaml b/login/app/sprinkles/admin/schema/requests/user/create.yaml
new file mode 100755
index 0000000..7e575bc
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/user/create.yaml
@@ -0,0 +1,72 @@
+---
+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
+ transformations:
+ - trim
+last_name:
+ validators:
+ length:
+ label: "&LAST_NAME"
+ min: 1
+ max: 30
+ message: VALIDATE.LENGTH_RANGE
+ transformations:
+ - trim
+email:
+ validators:
+ required:
+ label: "&EMAIL"
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&EMAIL"
+ min: 1
+ max: 150
+ message: VALIDATE.LENGTH_RANGE
+ email:
+ message: VALIDATE.INVALID_EMAIL
+locale:
+ default: en_US
+ validators:
+ required:
+ label: "&LOCALE"
+ domain: server
+ message: VALIDATE.REQUIRED
+ length:
+ label: "&LOCALE"
+ min: 1
+ max: 10
+ domain: server
+ message: VALIDATE.LENGTH_RANGE
+group_id:
+ validators:
+ integer:
+ label: "&GROUP"
+ domain: server
+ message: VALIDATE.INTEGER
diff --git a/login/app/sprinkles/admin/schema/requests/user/edit-field.yaml b/login/app/sprinkles/admin/schema/requests/user/edit-field.yaml
new file mode 100755
index 0000000..ab3b3aa
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/user/edit-field.yaml
@@ -0,0 +1,60 @@
+---
+first_name:
+ validators:
+ length:
+ label: "&FIRST_NAME"
+ min: 1
+ max: 20
+ message: VALIDATE.LENGTH_RANGE
+last_name:
+ validators:
+ length:
+ label: "&LAST_NAME"
+ min: 1
+ max: 30
+ message: VALIDATE.LENGTH_RANGE
+email:
+ validators:
+ length:
+ label: "&EMAIL"
+ min: 1
+ max: 150
+ message: VALIDATE.LENGTH_RANGE
+ email:
+ message: VALIDATE.INVALID_EMAIL
+locale:
+ validators:
+ length:
+ label: "&LOCALE"
+ min: 1
+ max: 10
+ message: VALIDATE.LENGTH_RANGE
+group_id:
+ validators:
+ integer:
+ message: VALIDATE.INTEGER
+flag_enabled:
+ validators:
+ member_of:
+ values:
+ - '0'
+ - '1'
+ message: VALIDATE.BOOLEAN
+flag_verified:
+ validators:
+ member_of:
+ values:
+ - '0'
+ - '1'
+ message: VALIDATE.BOOLEAN
+password:
+ validators:
+ length:
+ label: "&PASSWORD"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+roles:
+ validators:
+ array:
+ message: VALIDATE.ARRAY
diff --git a/login/app/sprinkles/admin/schema/requests/user/edit-info.yaml b/login/app/sprinkles/admin/schema/requests/user/edit-info.yaml
new file mode 100755
index 0000000..30ae920
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/user/edit-info.yaml
@@ -0,0 +1,36 @@
+---
+first_name:
+ validators:
+ length:
+ label: "&FIRST_NAME"
+ min: 1
+ max: 20
+ message: VALIDATE.LENGTH_RANGE
+last_name:
+ validators:
+ length:
+ label: "&LAST_NAME"
+ min: 1
+ max: 30
+ message: VALIDATE.LENGTH_RANGE
+email:
+ validators:
+ length:
+ label: "&EMAIL"
+ min: 1
+ max: 150
+ message: VALIDATE.LENGTH_RANGE
+ email:
+ message: VALIDATE.INVALID_EMAIL
+locale:
+ validators:
+ length:
+ label: "&LOCALE"
+ min: 1
+ max: 10
+ message: VALIDATE.LENGTH_RANGE
+group_id:
+ validators:
+ integer:
+ label: "&GROUP"
+ message: VALIDATE.INTEGER
diff --git a/login/app/sprinkles/admin/schema/requests/user/edit-password.yaml b/login/app/sprinkles/admin/schema/requests/user/edit-password.yaml
new file mode 100755
index 0000000..1d751ff
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/user/edit-password.yaml
@@ -0,0 +1,30 @@
+---
+value:
+ validators:
+ required:
+ domain: client
+ label: "&PASSWORD"
+ message: VALIDATE.REQUIRED
+ length:
+ domain: client
+ label: "&PASSWORD"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
+passwordc:
+ validators:
+ required:
+ domain: client
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.REQUIRED
+ matches:
+ domain: client
+ field: value
+ label: "&PASSWORD.CONFIRM"
+ message: VALIDATE.PASSWORD_MISMATCH
+ length:
+ domain: client
+ label: "&PASSWORD.CONFIRM"
+ min: 12
+ max: 100
+ message: VALIDATE.LENGTH_RANGE
diff --git a/login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml b/login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml
new file mode 100755
index 0000000..97170dd
--- /dev/null
+++ b/login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml
@@ -0,0 +1,6 @@
+---
+user_name:
+ validators:
+ required:
+ label: "&USERNAME"
+ message: VALIDATE.REQUIRED
diff --git a/login/app/sprinkles/admin/src/Admin.php b/login/app/sprinkles/admin/src/Admin.php
new file mode 100755
index 0000000..8a6dcc1
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Admin.php
@@ -0,0 +1,20 @@
+getQueryParams();
+
+ /** @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_activities')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) {
+ return $query->with('user');
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders the activity listing page.
+ *
+ * This page renders a table of user activities.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $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_activities')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/activities.html.twig');
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Controller/AdminController.php b/login/app/sprinkles/admin/src/Controller/AdminController.php
new file mode 100755
index 0000000..da4da8a
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Controller/AdminController.php
@@ -0,0 +1,150 @@
+ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_dashboard')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Probably a better way to do this
+ $users = $classMapper->staticMethod('user', 'orderBy', 'created_at', 'desc')
+ ->take(8)
+ ->get();
+
+ // Transform the `create_at` date in "x days ago" type of string
+ $users->transform(function ($item, $key) {
+ $item->registered = Carbon::parse($item->created_at)->diffForHumans();
+ return $item;
+ });
+
+ /** @var Config $config */
+ $config = $this->ci->config;
+
+ /** @var Config $config */
+ $cache = $this->ci->cache;
+
+ // Get each sprinkle db version
+ $sprinkles = $this->ci->sprinkleManager->getSprinkleNames();
+
+ return $this->ci->view->render($response, 'pages/dashboard.html.twig', [
+ 'counter' => [
+ 'users' => $classMapper->staticMethod('user', 'count'),
+ 'roles' => $classMapper->staticMethod('role', 'count'),
+ 'groups' => $classMapper->staticMethod('group', 'count')
+ ],
+ 'info' => [
+ 'version' => [
+ 'UF' => \UserFrosting\VERSION,
+ 'php' => phpversion(),
+ 'database' => EnvironmentInfo::database()
+ ],
+ 'database' => [
+ 'name' => $config['db.default.database']
+ ],
+ 'environment' => $this->ci->environment,
+ 'path' => [
+ 'project' => \UserFrosting\ROOT_DIR
+ ]
+ ],
+ 'sprinkles' => $sprinkles,
+ 'users' => $users
+ ]);
+ }
+
+ /**
+ * Clear the site cache.
+ *
+ * This route requires authentication.
+ * Request type: POST
+ */
+ public function clearCache($request, $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, 'clear_cache')) {
+ throw new ForbiddenException();
+ }
+
+ // Flush cache
+ $this->ci->cache->flush();
+
+ /** @var MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'CACHE.CLEARED');
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Renders the modal form to confirm cache deletion.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalConfirmClearCache($request, $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, 'clear_cache')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-clear-cache.html.twig', [
+ 'form' => [
+ 'action' => 'api/dashboard/clear-cache',
+ ]
+ ]);
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Controller/GroupController.php b/login/app/sprinkles/admin/src/Controller/GroupController.php
new file mode 100755
index 0000000..7ca94b1
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Controller/GroupController.php
@@ -0,0 +1,725 @@
+getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_group')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/group/create.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\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if name or slug already exists
+ if ($classMapper->staticMethod('group', 'where', 'name', $data['name'])->first()) {
+ $ms->addMessageTranslated('danger', 'GROUP.NAME.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($classMapper->staticMethod('group', 'where', 'slug', $data['slug'])->first()) {
+ $ms->addMessageTranslated('danger', 'GROUP.SLUG.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // All checks passed! log events/activities and create group
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) {
+ // Create the group
+ $group = $classMapper->createInstance('group', $data);
+
+ // Store new group to database
+ $group->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} created group {$group->name}.", [
+ 'type' => 'group_create',
+ 'user_id' => $currentUser->id
+ ]);
+
+ $ms->addMessageTranslated('success', 'GROUP.CREATION_SUCCESSFUL', $data);
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to delete an existing group.
+ *
+ * Deletes the specified group.
+ * Before doing so, checks that:
+ * 1. The user has permission to delete this group;
+ * 2. The group is not currently set as the default for new users;
+ * 3. The group is empty (does not have any users);
+ * 4. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: DELETE
+ */
+ public function delete($request, $response, $args)
+ {
+ $group = $this->getGroupFromParams($args);
+
+ // If the group doesn't exist, return 404
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_group', [
+ 'group' => $group
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Check that we are not deleting the default group
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if ($group->slug == $config['site.registration.user_defaults.group']) {
+ $e = new BadRequestException();
+ $e->addUserMessage('GROUP.DELETE_DEFAULT', $group->toArray());
+ throw $e;
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if there are any users in this group
+ $countGroupUsers = $classMapper->staticMethod('user', 'where', 'group_id', $group->id)->count();
+ if ($countGroupUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('GROUP.NOT_EMPTY', $group->toArray());
+ throw $e;
+ }
+
+ $groupName = $group->name;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($group, $groupName, $currentUser) {
+ $group->delete();
+ unset($group);
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted group {$groupName}.", [
+ 'type' => 'group_delete',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'GROUP.DELETION_SUCCESSFUL', [
+ 'name' => $groupName
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns info for a single group.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $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_groups')) {
+ throw new ForbiddenException();
+ }
+
+ $slug = $args['slug'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $group = $classMapper->staticMethod('group', 'where', 'slug', $slug)->first();
+
+ // If the group doesn't exist, return 404
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get group
+ $result = $group->toArray();
+
+ // 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($result, 200, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Returns a list of Groups
+ *
+ * Generates a list of groups, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_groups')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('group_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ public function getModalConfirmDelete($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $group = $this->getGroupFromParams($params);
+
+ // If the group no longer exists, forward to main group listing page
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_group', [
+ 'group' => $group
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if there are any users in this group
+ $countGroupUsers = $classMapper->staticMethod('user', 'where', 'group_id', $group->id)->count();
+ if ($countGroupUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('GROUP.NOT_EMPTY', $group->toArray());
+ throw $e;
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-delete-group.html.twig', [
+ 'group' => $group,
+ 'form' => [
+ 'action' => "api/groups/g/{$group->slug}",
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for creating a new group.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalCreate($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_group')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Create a dummy group to prepopulate fields
+ $group = $classMapper->createInstance('group', []);
+
+ $group->icon = 'fa fa-user';
+
+ $fieldNames = ['name', 'slug', 'icon', 'description'];
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/group/create.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/group.html.twig', [
+ 'group' => $group,
+ 'form' => [
+ 'action' => 'api/groups',
+ 'method' => 'POST',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('CREATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing an existing group.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEdit($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $group = $this->getGroupFromParams($params);
+
+ // If the group doesn't exist, return 404
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "icon", "description" for this group
+ $fieldNames = ['name', 'slug', 'icon', 'description'];
+ if (!$authorizer->checkAccess($currentUser, 'update_group_field', [
+ 'group' => $group,
+ 'fields' => $fieldNames
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Generate form
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/group/edit-info.yaml');
+ $validator = new JqueryValidationAdapter($schema, $translator);
+
+ return $this->ci->view->render($response, 'modals/group.html.twig', [
+ 'group' => $group,
+ 'form' => [
+ 'action' => "api/groups/g/{$group->slug}",
+ 'method' => 'PUT',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('UPDATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ public function getUsers($request, $response, $args)
+ {
+ $group = $this->getGroupFromParams($args);
+
+ // If the group no longer exists, forward to main group listing page
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_group_field', [
+ 'group' => $group,
+ 'property' => 'users'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($group) {
+ return $query->where('group_id', $group->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders a page displaying a group's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view the requested group's info.
+ * It checks each field individually, showing only those that you have permission to view.
+ * This will also try to show buttons for deleting, and editing the group.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args)
+ {
+ $group = $this->getGroupFromParams($args);
+
+ // If the group no longer exists, forward to main group listing page
+ if (!$group) {
+ $redirectPage = $this->ci->router->pathFor('uri_groups');
+ return $response->withRedirect($redirectPage, 404);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_group', [
+ 'group' => $group
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Determine fields that currentUser is authorized to view
+ $fieldNames = ['name', 'slug', 'icon', 'description'];
+
+ // Generate form
+ $fields = [
+ 'hidden' => []
+ ];
+
+ foreach ($fieldNames as $field) {
+ if (!$authorizer->checkAccess($currentUser, 'view_group_field', [
+ 'group' => $group,
+ 'property' => $field
+ ])) {
+ $fields['hidden'][] = $field;
+ }
+ }
+
+ // Determine buttons to display
+ $editButtons = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'update_group_field', [
+ 'group' => $group,
+ 'fields' => ['name', 'slug', 'icon', 'description']
+ ])) {
+ $editButtons['hidden'][] = 'edit';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'delete_group', [
+ 'group' => $group
+ ])) {
+ $editButtons['hidden'][] = 'delete';
+ }
+
+ return $this->ci->view->render($response, 'pages/group.html.twig', [
+ 'group' => $group,
+ 'fields' => $fields,
+ 'tools' => $editButtons
+ ]);
+ }
+
+ /**
+ * Renders the group listing page.
+ *
+ * This page renders a table of groups, with dropdown menus for admin actions for each group.
+ * Actions typically include: edit group, delete group.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_groups')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/groups.html.twig');
+ }
+
+ /**
+ * Processes the request to update an existing group's details.
+ *
+ * Processes the request from the group update form, checking that:
+ * 1. The group name/slug are not already in use;
+ * 2. The user has the necessary permissions to update the posted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: PUT
+ * @see getModalGroupEdit
+ */
+ public function updateInfo($request, $response, $args)
+ {
+ // Get the group based on slug in URL
+ $group = $this->getGroupFromParams($args);
+
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters: (name, slug, icon, description)
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/group/edit-info.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;
+ }
+
+ // Determine targeted fields
+ $fieldNames = [];
+ foreach ($data as $name => $value) {
+ $fieldNames[] = $name;
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit submitted fields for this group
+ if (!$authorizer->checkAccess($currentUser, 'update_group_field', [
+ 'group' => $group,
+ 'fields' => array_values(array_unique($fieldNames))
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if name or slug already exists
+ if (
+ isset($data['name']) &&
+ $data['name'] != $group->name &&
+ $classMapper->staticMethod('group', 'where', 'name', $data['name'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'GROUP.NAME.IN_USE', $data);
+ $error = true;
+ }
+
+ if (
+ isset($data['slug']) &&
+ $data['slug'] != $group->slug &&
+ $classMapper->staticMethod('group', 'where', 'slug', $data['slug'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'GROUP.SLUG.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($data, $group, $currentUser) {
+ // Update the group and generate success messages
+ foreach ($data as $name => $value) {
+ if ($value != $group->$name) {
+ $group->$name = $value;
+ }
+ }
+
+ $group->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for group {$group->name}.", [
+ 'type' => 'group_update_info',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ $ms->addMessageTranslated('success', 'GROUP.UPDATE', [
+ 'name' => $group->name
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ protected function getGroupFromParams($params)
+ {
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/group/get-by-slug.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and throw exception 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();
+ foreach ($validator->errors() as $idx => $field) {
+ foreach($field as $eidx => $error) {
+ $e->addUserMessage($error);
+ }
+ }
+ throw $e;
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Get the group
+ $group = $classMapper->staticMethod('group', 'where', 'slug', $data['slug'])
+ ->first();
+
+ return $group;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Controller/PermissionController.php b/login/app/sprinkles/admin/src/Controller/PermissionController.php
new file mode 100755
index 0000000..660e296
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Controller/PermissionController.php
@@ -0,0 +1,206 @@
+ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ $permissionId = $args['id'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $permission = $classMapper->staticMethod('permission', 'find', $permissionId);
+
+ // If the permission doesn't exist, return 404
+ if (!$permission) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get permission
+ $result = $permission->load('users')->toArray();
+
+ // 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($result, 200, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Returns a list of Permissions
+ *
+ * Generates a list of permissions, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @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_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('permission_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Returns a list of Users for a specified Permission.
+ *
+ * Generates a list of users, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getUsers($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @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_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $params['permission_id'] = $args['id'];
+
+ $sprunje = $classMapper->createInstance('permission_user_sprunje', $classMapper, $params);
+
+ $response = $sprunje->toResponse($response);
+
+ // 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;
+ }
+
+ /**
+ * Renders a page displaying a permission's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view permissions.
+ * Note that permissions cannot be modified through the interface. This is because
+ * permissions are tighly coupled to the code and should only be modified by developers.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $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_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ $permissionId = $args['id'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $permission = $classMapper->staticMethod('permission', 'find', $permissionId);
+
+ // If the permission doesn't exist, return 404
+ if (!$permission) {
+ throw new NotFoundException($request, $response);
+ }
+
+ return $this->ci->view->render($response, 'pages/permission.html.twig', [
+ 'permission' => $permission
+ ]);
+ }
+
+ /**
+ * Renders the permission listing page.
+ *
+ * This page renders a table of permissions, with dropdown menus for admin actions for each permission.
+ * Actions typically include: edit permission, delete permission.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $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_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/permissions.html.twig');
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Controller/RoleController.php b/login/app/sprinkles/admin/src/Controller/RoleController.php
new file mode 100755
index 0000000..ab86c88
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Controller/RoleController.php
@@ -0,0 +1,930 @@
+getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_role')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/create.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\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if name or slug already exists
+ if ($classMapper->staticMethod('role', 'where', 'name', $data['name'])->first()) {
+ $ms->addMessageTranslated('danger', 'ROLE.NAME_IN_USE', $data);
+ $error = true;
+ }
+
+ if ($classMapper->staticMethod('role', 'where', 'slug', $data['slug'])->first()) {
+ $ms->addMessageTranslated('danger', 'SLUG_IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // All checks passed! log events/activities and create role
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) {
+ // Create the role
+ $role = $classMapper->createInstance('role', $data);
+
+ // Store new role to database
+ $role->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} created role {$role->name}.", [
+ 'type' => 'role_create',
+ 'user_id' => $currentUser->id
+ ]);
+
+ $ms->addMessageTranslated('success', 'ROLE.CREATION_SUCCESSFUL', $data);
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to delete an existing role.
+ *
+ * Deletes the specified role.
+ * Before doing so, checks that:
+ * 1. The user has permission to delete this role;
+ * 2. The role is not a default for new users;
+ * 3. The role does not have any associated users;
+ * 4. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: DELETE
+ */
+ public function delete($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_role', [
+ 'role' => $role
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check that we are not deleting a default role
+ $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs');
+
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if (in_array($role->slug, $defaultRoleSlugs)) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.DELETE_DEFAULT');
+ throw $e;
+ }
+
+ // Check if there are any users associated with this role
+ $countUsers = $role->users()->count();
+ if ($countUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.HAS_USERS');
+ throw $e;
+ }
+
+ $roleName = $role->name;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($role, $roleName, $currentUser) {
+ $role->delete();
+ unset($role);
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted role {$roleName}.", [
+ 'type' => 'role_delete',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'ROLE.DELETION_SUCCESSFUL', [
+ 'name' => $roleName
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns info for a single role, along with associated permissions.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $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_roles')) {
+ throw new ForbiddenException();
+ }
+
+ $slug = $args['slug'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $role = $classMapper->staticMethod('role', 'where', 'slug', $slug)->first();
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get role
+ $result = $role->load('permissions')->toArray();
+
+ // 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($result, 200, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Returns a list of Roles
+ *
+ * Generates a list of roles, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_roles')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ public function getModalConfirmDelete($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $role = $this->getRoleFromParams($params);
+
+ // If the role no longer exists, forward to main role listing page
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_role', [
+ 'role' => $role
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check that we are not deleting a default role
+ $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs');
+
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if (in_array($role->slug, $defaultRoleSlugs)) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.DELETE_DEFAULT', $role->toArray());
+ throw $e;
+ }
+
+ // Check if there are any users associated with this role
+ $countUsers = $role->users()->count();
+ if ($countUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.HAS_USERS', $role->toArray());
+ throw $e;
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-delete-role.html.twig', [
+ 'role' => $role,
+ 'form' => [
+ 'action' => "api/roles/r/{$role->slug}",
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for creating a new role.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalCreate($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_role')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Create a dummy role to prepopulate fields
+ $role = $classMapper->createInstance('role', []);
+
+ $fieldNames = ['name', 'slug', 'description'];
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/role/create.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/role.html.twig', [
+ 'role' => $role,
+ 'form' => [
+ 'action' => 'api/roles',
+ 'method' => 'POST',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('CREATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing an existing role.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEdit($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $role = $this->getRoleFromParams($params);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "description" for this role
+ $fieldNames = ['name', 'slug', 'description'];
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => $fieldNames
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Generate form
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/role/edit-info.yaml');
+ $validator = new JqueryValidationAdapter($schema, $translator);
+
+ return $this->ci->view->render($response, 'modals/role.html.twig', [
+ 'role' => $role,
+ 'form' => [
+ 'action' => "api/roles/r/{$role->slug}",
+ 'method' => 'PUT',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('UPDATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing a role's permissions.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEditPermissions($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $role = $this->getRoleFromParams($params);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "permissions" field for this role
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => ['permissions']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'modals/role-manage-permissions.html.twig', [
+ 'role' => $role
+ ]);
+ }
+
+ /**
+ * Returns a list of Permissions for a specified Role.
+ *
+ * Generates a list of permissions, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getPermissions($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role no longer exists, forward to main role listing page
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_role_field', [
+ 'role' => $role,
+ 'property' => 'permissions'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('permission_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($role) {
+ return $query->forRole($role->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Returns users associated with a single role.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getUsers($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_role_field', [
+ 'role' => $role,
+ 'property' => 'users'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($role) {
+ return $query->forRole($role->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders a page displaying a role's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view the requested role's info.
+ * It checks each field individually, showing only those that you have permission to view.
+ * This will also try to show buttons for deleting and editing the role.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role no longer exists, forward to main role listing page
+ if (!$role) {
+ $redirectPage = $this->ci->router->pathFor('uri_roles');
+ return $response->withRedirect($redirectPage, 404);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_role', [
+ 'role' => $role
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Determine fields that currentUser is authorized to view
+ $fieldNames = ['name', 'slug', 'description'];
+
+ // Generate form
+ $fields = [
+ 'hidden' => []
+ ];
+
+ foreach ($fieldNames as $field) {
+ if (!$authorizer->checkAccess($currentUser, 'view_role_field', [
+ 'role' => $role,
+ 'property' => $field
+ ])) {
+ $fields['hidden'][] = $field;
+ }
+ }
+
+ // Determine buttons to display
+ $editButtons = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => ['name', 'slug', 'description']
+ ])) {
+ $editButtons['hidden'][] = 'edit';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'delete_role', [
+ 'role' => $role
+ ])) {
+ $editButtons['hidden'][] = 'delete';
+ }
+
+ return $this->ci->view->render($response, 'pages/role.html.twig', [
+ 'role' => $role,
+ 'fields' => $fields,
+ 'tools' => $editButtons
+ ]);
+ }
+
+ /**
+ * Renders the role listing page.
+ *
+ * This page renders a table of roles, with dropdown menus for admin actions for each role.
+ * Actions typically include: edit role, delete role.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_roles')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/roles.html.twig');
+ }
+
+ /**
+ * Processes the request to update an existing role's details.
+ *
+ * Processes the request from the role update form, checking that:
+ * 1. The role name/slug are not already in use;
+ * 2. The user has the necessary permissions to update the posted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: PUT
+ * @see getModalRoleEdit
+ */
+ public function updateInfo($request, $response, $args)
+ {
+ // Get the role based on slug in the URL
+ $role = $this->getRoleFromParams($args);
+
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters: (name, slug, description)
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/edit-info.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;
+ }
+
+ // Determine targeted fields
+ $fieldNames = [];
+ foreach ($data as $name => $value) {
+ $fieldNames[] = $name;
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit submitted fields for this role
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => array_values(array_unique($fieldNames))
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if name or slug already exists
+ if (
+ isset($data['name']) &&
+ $data['name'] != $role->name &&
+ $classMapper->staticMethod('role', 'where', 'name', $data['name'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'ROLE.NAME_IN_USE', $data);
+ $error = true;
+ }
+
+ if (
+ isset($data['slug']) &&
+ $data['slug'] != $role->slug &&
+ $classMapper->staticMethod('role', 'where', 'slug', $data['slug'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'SLUG_IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($data, $role, $currentUser) {
+ // Update the role and generate success messages
+ foreach ($data as $name => $value) {
+ if ($value != $role->$name){
+ $role->$name = $value;
+ }
+ }
+
+ $role->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for role {$role->name}.", [
+ 'type' => 'role_update_info',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ $ms->addMessageTranslated('success', 'ROLE.UPDATED', [
+ 'name' => $role->name
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to update a specific field for an existing role, including permissions.
+ *
+ * Processes the request from the role update form, checking that:
+ * 1. The logged-in user has the necessary permissions to update the putted field(s);
+ * 2. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: PUT
+ */
+ public function updateField($request, $response, $args)
+ {
+ // Get the username from the URL
+ $role = $this->getRoleFromParams($args);
+
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get key->value pair from URL and request body
+ $fieldName = $args['field'];
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit the specified field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => [$fieldName]
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters: value
+ $put = $request->getParsedBody();
+
+ if (!isset($put['value'])) {
+ throw new BadRequestException();
+ }
+
+ $params = [
+ $fieldName => $put['value']
+ ];
+
+ // Validate key -> value pair
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/edit-field.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and throw exception 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();
+ foreach ($validator->errors() as $idx => $field) {
+ foreach($field as $eidx => $error) {
+ $e->addUserMessage($error);
+ }
+ }
+ throw $e;
+ }
+
+ // Get validated and transformed value
+ $fieldValue = $data[$fieldName];
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $ms = $this->ci->alerts;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($fieldName, $fieldValue, $role, $currentUser) {
+ if ($fieldName == 'permissions') {
+ $newPermissions = collect($fieldValue)->pluck('permission_id')->all();
+ $role->permissions()->sync($newPermissions);
+ } else {
+ $role->$fieldName = $fieldValue;
+ $role->save();
+ }
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for role {$role->name}.", [
+ 'type' => 'role_update_field',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ // Add success messages
+ if ($fieldName == 'permissions') {
+ $ms->addMessageTranslated('success', 'ROLE.PERMISSIONS_UPDATED', [
+ 'name' => $role->name
+ ]);
+ } else {
+ $ms->addMessageTranslated('success', 'ROLE.UPDATED', [
+ 'name' => $role->name
+ ]);
+ }
+
+ return $response->withStatus(200);
+ }
+
+ protected function getRoleFromParams($params)
+ {
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/get-by-slug.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and throw exception 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();
+ foreach ($validator->errors() as $idx => $field) {
+ foreach($field as $eidx => $error) {
+ $e->addUserMessage($error);
+ }
+ }
+ throw $e;
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Get the role
+ $role = $classMapper->staticMethod('role', 'where', 'slug', $data['slug'])
+ ->first();
+
+ return $role;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Controller/UserController.php b/login/app/sprinkles/admin/src/Controller/UserController.php
new file mode 100755
index 0000000..ff41009
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Controller/UserController.php
@@ -0,0 +1,1278 @@
+getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_user')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/create.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\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // 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;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // If currentUser does not have permission to set the group, but they try to set it to something other than their own group,
+ // throw an exception.
+ if (!$authorizer->checkAccess($currentUser, 'create_user_field', [
+ 'fields' => ['group']
+ ])) {
+ if (isset($data['group_id']) && $data['group_id'] != $currentUser->group_id) {
+ throw new ForbiddenException();
+ }
+ }
+
+ // In any case, set the group id if not otherwise set
+ if (!isset($data['group_id'])) {
+ $data['group_id'] = $currentUser->group_id;
+ }
+
+ $data['flag_verified'] = 1;
+ // Set password as empty on initial creation. We will then send email so new user can set it themselves via a verification token
+ $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, $currentUser) {
+ // Create the user
+ $user = $classMapper->createInstance('user', $data);
+
+ // Store new user to database
+ $user->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} created a new account for {$user->user_name}.", [
+ 'type' => 'account_create',
+ 'user_id' => $currentUser->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);
+
+ // Try to generate a new password request
+ $passwordRequest = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.create']);
+
+ // Create and send welcome email with password set link
+ $message = new TwigMailMessage($this->ci->view, 'mail/password-create.html.twig');
+
+ $message->from($config['address_book.admin'])
+ ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
+ ->addParams([
+ 'user' => $user,
+ 'create_password_expiration' => $config['password_reset.timeouts.create'] / 3600 . ' hours',
+ 'token' => $passwordRequest->getToken()
+ ]);
+
+ $this->ci->mailer->send($message);
+
+ $ms->addMessageTranslated('success', 'USER.CREATED', $data);
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to send a user a password reset email.
+ *
+ * Processes the request from the user update form, checking that:
+ * 1. The target user's new email address, if specified, is not already in use;
+ * 2. The logged-in user has the necessary permissions to update the posted field(s);
+ * 3. We're not trying to disable the master account;
+ * 4. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: POST
+ */
+ public function createPasswordReset($request, $response, $args)
+ {
+ // Get the username from the URL
+ $user = $this->getUserFromParams($args);
+
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "password" for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['password']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($user, $config) {
+
+ // Create a password reset and shoot off an email
+ $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']);
+
+ // Create and send welcome email with password set link
+ $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);
+ });
+
+ $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', [
+ 'email' => $user->email
+ ]);
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to delete an existing user.
+ *
+ * Deletes the specified user, removing any existing associations.
+ * Before doing so, checks that:
+ * 1. You are not trying to delete the master account;
+ * 2. You have permission to delete the target user's account.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: DELETE
+ */
+ public function delete($request, $response, $args)
+ {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Check that we are not deleting the master account
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if ($user->id == $config['reserved_user_ids.master']) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DELETE_MASTER');
+ throw $e;
+ }
+
+ $userName = $user->user_name;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($user, $userName, $currentUser) {
+ $user->delete();
+ unset($user);
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted the account for {$userName}.", [
+ 'type' => 'account_delete',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'DELETION_SUCCESSFUL', [
+ 'user_name' => $userName
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns activity history for a single user.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getActivities($request, $response, $args)
+ {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'activities'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params);
+
+ $sprunje->extendQuery(function ($query) use ($user) {
+ return $query->where('user_id', $user->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Returns info for a single user.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $response, $args)
+ {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Join user's most recent activity
+ $user = $classMapper->createInstance('user')
+ ->where('user_name', $user->user_name)
+ ->joinLastActivity()
+ ->with('lastActivity', 'group')
+ ->first();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $result = $user->toArray();
+
+ // 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($result, 200, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Returns a list of Users
+ *
+ * Generates a list of users, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_users')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders the modal form to confirm user deletion.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalConfirmDelete($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Check that we are not deleting the master account
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if ($user->id == $config['reserved_user_ids.master']) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DELETE_MASTER');
+ throw $e;
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-delete-user.html.twig', [
+ 'user' => $user,
+ 'form' => [
+ 'action' => "api/users/u/{$user->user_name}",
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for creating a new user.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * If the currently logged-in user has permission to modify user group membership, then the group toggle will be displayed.
+ * Otherwise, the user will be added to the default group and receive the default roles automatically.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalCreate($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_user')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Determine form fields to hide/disable
+ // TODO: come back to this when we finish implementing theming
+ $fields = [
+ 'hidden' => ['theme'],
+ 'disabled' => []
+ ];
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ // Determine if currentUser has permission to modify the group. If so, show the 'group' dropdown.
+ // Otherwise, set to the currentUser's group and disable the dropdown.
+ if ($authorizer->checkAccess($currentUser, 'create_user_field', [
+ 'fields' => ['group']
+ ])) {
+ // Get a list of all groups
+ $groups = $classMapper->staticMethod('group', 'all');
+ } else {
+ // Get the current user's group
+ $groups = $currentUser->group()->get();
+ $fields['disabled'][] = 'group';
+ }
+
+ // Create a dummy user to prepopulate fields
+ $data = [
+ 'group_id' => $currentUser->group_id,
+ 'locale' => $config['site.registration.user_defaults.locale'],
+ 'theme' => ''
+ ];
+
+ $user = $classMapper->createInstance('user', $data);
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/user/create.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/user.html.twig', [
+ 'user' => $user,
+ 'groups' => $groups,
+ 'locales' => $locales,
+ 'form' => [
+ 'action' => 'api/users',
+ 'method' => 'POST',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('CREATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing an existing user.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEdit($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Get the user to edit
+ $user = $classMapper->staticMethod('user', 'where', 'user_name', $user->user_name)
+ ->with('group')
+ ->first();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "email", "locale" for this user
+ $fieldNames = ['name', 'email', 'locale'];
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => $fieldNames
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Get a list of all groups
+ $groups = $classMapper->staticMethod('group', 'all');
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ // Generate form
+ $fields = [
+ 'hidden' => ['theme'],
+ 'disabled' => ['user_name']
+ ];
+
+ // Disable group field if currentUser doesn't have permission to modify group
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['group']
+ ])) {
+ $fields['disabled'][] = 'group';
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/user/edit-info.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ $translator = $this->ci->translator;
+
+ return $this->ci->view->render($response, 'modals/user.html.twig', [
+ 'user' => $user,
+ 'groups' => $groups,
+ 'locales' => $locales,
+ 'form' => [
+ 'action' => "api/users/u/{$user->user_name}",
+ 'method' => 'PUT',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('UPDATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing a user's password.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEditPassword($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "password" field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['password']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/user/edit-password.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/user-set-password.html.twig', [
+ 'user' => $user,
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing a user's roles.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEditRoles($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "roles" field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['roles']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'modals/user-manage-roles.html.twig', [
+ 'user' => $user
+ ]);
+ }
+
+ /**
+ * Returns a list of effective Permissions for a specified User.
+ *
+ * Generates a list of permissions, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getPermissions($request, $response, $args)
+ {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @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, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'permissions'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $params['user_id'] = $user->id;
+ $sprunje = $classMapper->createInstance('user_permission_sprunje', $classMapper, $params);
+
+ $response = $sprunje->toResponse($response);
+
+ // 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;
+ }
+
+ /**
+ * Returns roles associated with a single user.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getRoles($request, $response, $args)
+ {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'roles'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($user) {
+ return $query->forUser($user->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders a page displaying a user's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view the requested user's info.
+ * It checks each field individually, showing only those that you have permission to view.
+ * This will also try to show buttons for activating, disabling/enabling, deleting, and editing the user.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args)
+ {
+ $user = $this->getUserFromParams($args);
+
+ // If the user no longer exists, forward to main user listing page
+ if (!$user) {
+ $usersPage = $this->ci->router->pathFor('uri_users');
+ return $response->withRedirect($usersPage, 404);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ // Determine fields that currentUser is authorized to view
+ $fieldNames = ['user_name', 'name', 'email', 'locale', 'group', 'roles'];
+
+ // Generate form
+ $fields = [
+ // Always hide these
+ 'hidden' => ['theme']
+ ];
+
+ // Determine which fields should be hidden
+ foreach ($fieldNames as $field) {
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => $field
+ ])) {
+ $fields['hidden'][] = $field;
+ }
+ }
+
+ // Determine buttons to display
+ $editButtons = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['name', 'email', 'locale']
+ ])) {
+ $editButtons['hidden'][] = 'edit';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['flag_enabled']
+ ])) {
+ $editButtons['hidden'][] = 'enable';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['flag_verified']
+ ])) {
+ $editButtons['hidden'][] = 'activate';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['password']
+ ])) {
+ $editButtons['hidden'][] = 'password';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['roles']
+ ])) {
+ $editButtons['hidden'][] = 'roles';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'delete_user', [
+ 'user' => $user
+ ])) {
+ $editButtons['hidden'][] = 'delete';
+ }
+
+ // Determine widgets to display
+ $widgets = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'permissions'
+ ])) {
+ $widgets['hidden'][] = 'permissions';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'activities'
+ ])) {
+ $widgets['hidden'][] = 'activities';
+ }
+
+ return $this->ci->view->render($response, 'pages/user.html.twig', [
+ 'user' => $user,
+ 'locales' => $locales,
+ 'fields' => $fields,
+ 'tools' => $editButtons,
+ 'widgets' => $widgets
+ ]);
+ }
+
+ /**
+ * Renders the user listing page.
+ *
+ * This page renders a table of users, with dropdown menus for admin actions for each user.
+ * Actions typically include: edit user details, activate user, enable/disable user, delete user.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_users')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/users.html.twig');
+ }
+
+ /**
+ * Processes the request to update an existing user's basic details (first_name, last_name, email, locale, group_id)
+ *
+ * Processes the request from the user update form, checking that:
+ * 1. The target user's new email address, if specified, is not already in use;
+ * 2. The logged-in user has the necessary permissions to update the putted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: PUT
+ */
+ public function updateInfo($request, $response, $args)
+ {
+ // Get the username from the URL
+ $user = $this->getUserFromParams($args);
+
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/edit-info.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;
+ }
+
+ // Determine targeted fields
+ $fieldNames = [];
+ foreach ($data as $name => $value) {
+ if ($name == 'first_name' || $name == 'last_name') {
+ $fieldNames[] = 'name';
+ } elseif ($name == 'group_id') {
+ $fieldNames[] = 'group';
+ } else {
+ $fieldNames[] = $name;
+ }
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit submitted fields for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => array_values(array_unique($fieldNames))
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Only the master account can edit the master account!
+ if (
+ ($user->id == $config['reserved_user_ids.master']) &&
+ ($currentUser->id != $config['reserved_user_ids.master'])
+ ) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if email already exists
+ if (
+ isset($data['email']) &&
+ $data['email'] != $user->email &&
+ $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')
+ ) {
+ $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($data, $user, $currentUser) {
+ // Update the user and generate success messages
+ foreach ($data as $name => $value) {
+ if ($value != $user->$name) {
+ $user->$name = $value;
+ }
+ }
+
+ $user->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated basic account info for user {$user->user_name}.", [
+ 'type' => 'account_update_info',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [
+ 'user_name' => $user->user_name
+ ]);
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to update a specific field for an existing user.
+ *
+ * Supports editing all user fields, including password, enabled/disabled status and verification status.
+ * Processes the request from the user update form, checking that:
+ * 1. The logged-in user has the necessary permissions to update the putted field(s);
+ * 2. We're not trying to disable the master account;
+ * 3. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: PUT
+ */
+ public function updateField($request, $response, $args)
+ {
+ // Get the username from the URL
+ $user = $this->getUserFromParams($args);
+
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get key->value pair from URL and request body
+ $fieldName = $args['field'];
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit the specified field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => [$fieldName]
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Only the master account can edit the master account!
+ if (
+ ($user->id == $config['reserved_user_ids.master']) &&
+ ($currentUser->id != $config['reserved_user_ids.master'])
+ ) {
+ throw new ForbiddenException();
+ }
+
+ // Get PUT parameters: value
+ $put = $request->getParsedBody();
+
+ if (!isset($put['value'])) {
+ throw new BadRequestException();
+ }
+
+ // Create and validate key -> value pair
+ $params = [
+ $fieldName => $put['value']
+ ];
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/edit-field.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and throw exception 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();
+ foreach ($validator->errors() as $idx => $field) {
+ foreach($field as $eidx => $error) {
+ $e->addUserMessage($error);
+ }
+ }
+ throw $e;
+ }
+
+ // Get validated and transformed value
+ $fieldValue = $data[$fieldName];
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Special checks and transformations for certain fields
+ if ($fieldName == 'flag_enabled') {
+ // Check that we are not disabling the master account
+ if (
+ ($user->id == $config['reserved_user_ids.master']) &&
+ ($fieldValue == '0')
+ ) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DISABLE_MASTER');
+ throw $e;
+ } elseif (
+ ($user->id == $currentUser->id) &&
+ ($fieldValue == '0')
+ ) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DISABLE_SELF');
+ throw $e;
+ }
+ } elseif ($fieldName == 'password') {
+ $fieldValue = Password::hash($fieldValue);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($fieldName, $fieldValue, $user, $currentUser) {
+ if ($fieldName == 'roles') {
+ $newRoles = collect($fieldValue)->pluck('role_id')->all();
+ $user->roles()->sync($newRoles);
+ } else {
+ $user->$fieldName = $fieldValue;
+ $user->save();
+ }
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for user {$user->user_name}.", [
+ 'type' => 'account_update_field',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ // Add success messages
+ if ($fieldName == 'flag_enabled') {
+ if ($fieldValue == '1') {
+ $ms->addMessageTranslated('success', 'ENABLE_SUCCESSFUL', [
+ 'user_name' => $user->user_name
+ ]);
+ } else {
+ $ms->addMessageTranslated('success', 'DISABLE_SUCCESSFUL', [
+ 'user_name' => $user->user_name
+ ]);
+ }
+ } elseif ($fieldName == 'flag_verified') {
+ $ms->addMessageTranslated('success', 'MANUALLY_ACTIVATED', [
+ 'user_name' => $user->user_name
+ ]);
+ } else {
+ $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [
+ 'user_name' => $user->user_name
+ ]);
+ }
+
+ return $response->withStatus(200);
+ }
+
+ protected function getUserFromParams($params)
+ {
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/get-by-username.yaml');
+
+ // Whitelist and set parameter defaults
+ $transformer = new RequestDataTransformer($schema);
+ $data = $transformer->transform($params);
+
+ // Validate, and throw exception 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();
+ foreach ($validator->errors() as $idx => $field) {
+ foreach($field as $eidx => $error) {
+ $e->addUserMessage($error);
+ }
+ }
+ throw $e;
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Get the user to delete
+ $user = $classMapper->staticMethod('user', 'where', 'user_name', $data['user_name'])
+ ->first();
+
+ return $user;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php b/login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php
new file mode 100755
index 0000000..061d90c
--- /dev/null
+++ b/login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php
@@ -0,0 +1,84 @@
+extend('classMapper', function ($classMapper, $c) {
+ $classMapper->setClassMapping('activity_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\ActivitySprunje');
+ $classMapper->setClassMapping('group_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\GroupSprunje');
+ $classMapper->setClassMapping('permission_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\PermissionSprunje');
+ $classMapper->setClassMapping('permission_user_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\PermissionUserSprunje');
+ $classMapper->setClassMapping('role_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\RoleSprunje');
+ $classMapper->setClassMapping('user_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\UserSprunje');
+ $classMapper->setClassMapping('user_permission_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\UserPermissionSprunje');
+ return $classMapper;
+ });
+
+ /**
+ * Returns a callback that handles setting the `UF-Redirect` header after a successful login.
+ *
+ * Overrides the service definition in the account Sprinkle.
+ */
+ $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_dashboard')) {
+ return $response->withHeader('UF-Redirect', $c->router->pathFor('dashboard'));
+ } elseif ($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'));
+ }
+ };
+ };
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php b/login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php
new file mode 100755
index 0000000..da4f0e3
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php
@@ -0,0 +1,80 @@
+classMapper->createInstance('activity');
+
+ return $query->joinUser();
+ }
+
+ /**
+ * Filter LIKE the user info.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterUser($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('users.first_name', $value)
+ ->orLike('users.last_name', $value)
+ ->orLike('users.email', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Sort based on user last name.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortUser($query, $direction)
+ {
+ $query->orderBy('users.last_name', $direction);
+ return $this;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php b/login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php
new file mode 100755
index 0000000..7c75691
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php
@@ -0,0 +1,42 @@
+classMapper->createInstance('group')->newQuery();
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php b/login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php
new file mode 100755
index 0000000..c1803f1
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php
@@ -0,0 +1,93 @@
+classMapper->createInstance('permission')->newQuery();
+ }
+
+ /**
+ * Filter LIKE the slug, conditions, or description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterInfo($query, $value)
+ {
+ return $this->filterProperties($query, $value);
+ }
+
+ /**
+ * Filter LIKE the slug, conditions, or description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterProperties($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('slug', $value)
+ ->orLike('conditions', $value)
+ ->orLike('description', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Sort based on slug.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortProperties($query, $direction)
+ {
+ $query->orderBy('slug', $direction);
+ return $this;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php b/login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php
new file mode 100755
index 0000000..242681d
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php
@@ -0,0 +1,48 @@
+options['permission_id'])) {
+ throw new BadRequestException();
+ }
+
+ $permission = $this->classMapper->staticMethod('permission', 'find', $this->options['permission_id']);
+
+ // If the permission doesn't exist, return 404
+ if (!$permission) {
+ throw new NotFoundException;
+ }
+
+ // Get permission users
+ $query = $permission->users()->withVia('roles_via');
+
+ return $query;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php b/login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php
new file mode 100755
index 0000000..c5e0f8b
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php
@@ -0,0 +1,67 @@
+classMapper->createInstance('role')->newQuery();
+ }
+
+ /**
+ * Filter LIKE name OR description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterInfo($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('name', $value)
+ ->orLike('description', $value);
+ }
+ });
+ return $this;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php b/login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php
new file mode 100755
index 0000000..6142e74
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php
@@ -0,0 +1,48 @@
+options['user_id'])) {
+ throw new BadRequestException();
+ }
+
+ $user = $this->classMapper->staticMethod('user', 'find', $this->options['user_id']);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException;
+ }
+
+ // Get user permissions
+ $query = $user->permissions()->withVia('roles_via');
+
+ return $query;
+ }
+}
diff --git a/login/app/sprinkles/admin/src/Sprunje/UserSprunje.php b/login/app/sprinkles/admin/src/Sprunje/UserSprunje.php
new file mode 100755
index 0000000..12378f9
--- /dev/null
+++ b/login/app/sprinkles/admin/src/Sprunje/UserSprunje.php
@@ -0,0 +1,185 @@
+classMapper->createInstance('user');
+
+ // Join user's most recent activity
+ return $query->joinLastActivity()->with('lastActivity');
+ }
+
+ /**
+ * Filter LIKE the last activity description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterLastActivity($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('activities.description', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Filter LIKE the first name, last name, or email.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterName($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('first_name', $value)
+ ->orLike('last_name', $value)
+ ->orLike('email', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Filter by status (active, disabled, unactivated)
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterStatus($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ if ($value == 'disabled') {
+ $query->orWhere('flag_enabled', 0);
+ } elseif ($value == 'unactivated') {
+ $query->orWhere('flag_verified', 0);
+ } elseif ($value == 'active') {
+ $query->orWhere(function ($query) {
+ $query->where('flag_enabled', 1)->where('flag_verified', 1);
+ });
+ }
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Return a list of possible user statuses.
+ *
+ * @return array
+ */
+ protected function listStatus()
+ {
+ return [
+ [
+ 'value' => 'active',
+ 'text' => Translator::translate('ACTIVE')
+ ],
+ [
+ 'value' => 'unactivated',
+ 'text' => Translator::translate('UNACTIVATED')
+ ],
+ [
+ 'value' => 'disabled',
+ 'text' => Translator::translate('DISABLED')
+ ]
+ ];
+ }
+
+ /**
+ * Sort based on last activity time.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortLastActivity($query, $direction)
+ {
+ $query->orderBy('activities.occurred_at', $direction);
+ return $this;
+ }
+
+ /**
+ * Sort based on last name.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortName($query, $direction)
+ {
+ $query->orderBy('last_name', $direction);
+ return $this;
+ }
+
+ /**
+ * Sort active, unactivated, disabled
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortStatus($query, $direction)
+ {
+ $query->orderBy('flag_enabled', $direction)->orderBy('flag_verified', $direction);
+ return $this;
+ }
+}
diff --git a/login/app/sprinkles/admin/templates/forms/group.html.twig b/login/app/sprinkles/admin/templates/forms/group.html.twig
new file mode 100755
index 0000000..36d6632
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/forms/group.html.twig
@@ -0,0 +1,69 @@
+
+
+
diff --git a/login/app/sprinkles/admin/templates/forms/role.html.twig b/login/app/sprinkles/admin/templates/forms/role.html.twig
new file mode 100755
index 0000000..46a4477
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/forms/role.html.twig
@@ -0,0 +1,56 @@
+
+
+
diff --git a/login/app/sprinkles/admin/templates/forms/user.html.twig b/login/app/sprinkles/admin/templates/forms/user.html.twig
new file mode 100755
index 0000000..3ee7fc9
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/forms/user.html.twig
@@ -0,0 +1,125 @@
+
+
+
diff --git a/login/app/sprinkles/admin/templates/mail/password-create.html.twig b/login/app/sprinkles/admin/templates/mail/password-create.html.twig
new file mode 100755
index 0000000..854eb77
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/mail/password-create.html.twig
@@ -0,0 +1,19 @@
+{% block subject %}
+ {{site.title}} - please set a password for your new account
+{% endblock %}
+
+{% block body %}
+
+ Dear {{user.first_name}},
+
+
+ Someone has created an account for you with {{site.title}} ({{site.uri.public}}). Your username is {{user.user_name}}.
+
+
+ To access your account, you must first create a password by visiting: {{site.uri.public}}/account/set-password/confirm?token={{token}}. This link has been generated especially for you, and will expire in {{create_password_expiration}}. Do not share it with anyone!
+
+ {% endif %}
+
+ {# additional elements (e.g., menus, messages) before the user account drop down #}
+ {% block dashboard_navbar_extra %} {% endblock %}
+
+ {# User Account: style can be found in dropdown.less #}
+ {% include "navigation/user-card.html.twig" %}
+
+{% endblock %}
+{% block scripts_page %}
+
+
+
+
+ {{ assets.js('js/form-widgets') | raw }}
+
+
+ {{ assets.js('js/pages/users') | raw }}
+
+{% endblock %}
diff --git a/login/app/sprinkles/admin/templates/tables/activities.html.twig b/login/app/sprinkles/admin/templates/tables/activities.html.twig
new file mode 100755
index 0000000..d70541b
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/tables/activities.html.twig
@@ -0,0 +1,73 @@
+{# This partial template renders a table of user activities, to be populated with rows via an AJAX request.
+ # This extends a generic template for paginated tables.
+ #
+ # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
+ # to render the table cells with the data from the AJAX request.
+#}
+
+{% extends "tables/table-paginated.html.twig" %}
+
+{% block table %}
+
+
+
+
{{translate('ACTIVITY.TIME')}}
+ {% if 'user' in table.columns %}
+
{{translate('USER')}}
+ {% endif %}
+
{{translate("DESCRIPTION")}}
+
+
+
+
+
+{% endblock %}
+
+{% block table_cell_templates %}
+ {# This contains a series of
+
+
+
+
+ {% endverbatim %}
+{% endblock %}
diff --git a/login/app/sprinkles/admin/templates/tables/groups.html.twig b/login/app/sprinkles/admin/templates/tables/groups.html.twig
new file mode 100755
index 0000000..2c5a84a
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/tables/groups.html.twig
@@ -0,0 +1,69 @@
+{# This partial template renders a table of groups, to be populated with rows via an AJAX request.
+ # This extends a generic template for paginated tables.
+ #
+ # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
+ # to render the table cells with the data from the AJAX request.
+#}
+
+{% extends "tables/table-paginated.html.twig" %}
+
+{% block table %}
+
+
+
+
{{translate('GROUP')}}
+
{{translate("DESCRIPTION")}}
+
{{translate("ACTIONS")}}
+
+
+
+
+
+{% endblock %}
+
+{% block table_cell_templates %}
+ {# This contains a series of
+
+
+
+
+ {% endverbatim %}
+{% endblock %}
diff --git a/login/app/sprinkles/admin/templates/tables/permissions.html.twig b/login/app/sprinkles/admin/templates/tables/permissions.html.twig
new file mode 100755
index 0000000..92e236a
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/tables/permissions.html.twig
@@ -0,0 +1,66 @@
+{# This partial template renders a table of permissions, to be populated with rows via an AJAX request.
+ # This extends a generic template for paginated tables.
+ #
+ # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
+ # to render the table cells with the data from the AJAX request.
+#}
+
+{% extends "tables/table-paginated.html.twig" %}
+
+{% block table %}
+
+
+
+
{{translate('PERMISSION')}}
+
{{translate('SLUG_CONDITION')}}
+ {% if 'via_roles' in table.columns %}
+
{{translate('PERMISSION.VIA_ROLES')}}
+ {% endif %}
+
+
+
+
+
+{% endblock %}
+
+{% block table_cell_templates %}
+ {# This contains a series of
+
+
+
+
+ {% endverbatim %}
+{% endblock %}
diff --git a/login/app/sprinkles/admin/templates/tables/roles.html.twig b/login/app/sprinkles/admin/templates/tables/roles.html.twig
new file mode 100755
index 0000000..dbdb49e
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/tables/roles.html.twig
@@ -0,0 +1,74 @@
+{# This partial template renders a table of roles, to be populated with rows via an AJAX request.
+ # This extends a generic template for paginated tables.
+ #
+ # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
+ # to render the table cells with the data from the AJAX request.
+#}
+
+{% extends "tables/table-paginated.html.twig" %}
+
+{% block table %}
+
+
+
+
{{translate('ROLE')}}
+
{{translate('DESCRIPTION')}}
+
{{translate('ACTIONS')}}
+
+
+
+
+
+{% endblock %}
+
+{% block table_cell_templates %}
+ {# This contains a series of
+
+
+
+
+ {% endverbatim %}
+{% endblock %}
diff --git a/login/app/sprinkles/admin/templates/tables/users.html.twig b/login/app/sprinkles/admin/templates/tables/users.html.twig
new file mode 100755
index 0000000..1cebb47
--- /dev/null
+++ b/login/app/sprinkles/admin/templates/tables/users.html.twig
@@ -0,0 +1,149 @@
+{# This partial template renders a table of users, to be populated with rows via an AJAX request.
+ # This extends a generic template for paginated tables.
+ #
+ # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
+ # to render the table cells with the data from the AJAX request.
+#}
+
+{% extends "tables/table-paginated.html.twig" %}
+
+{% block table %}
+
+
+
+
{{translate('USER')}}
+ {% if 'last_activity' in table.columns %}
+
{{translate("ACTIVITY.LAST")}}
+ {% endif %}
+ {% if 'via_roles' in table.columns %}
+
');
+
+ return this.each(function () {
+ //if a source is specified
+ if (settings.source === "") {
+ if (window.console) {
+ window.console.log("Please specify a source first - boxRefresh()");
+ }
+ return;
+ }
+ //the box
+ var box = $(this);
+ //the button
+ var rBtn = box.find(settings.trigger).first();
+
+ //On trigger click
+ rBtn.on('click', function (e) {
+ e.preventDefault();
+ //Add loading overlay
+ start(box);
+
+ //Perform ajax call
+ box.find(".box-body").load(settings.source, function () {
+ done(box);
+ });
+ });
+ });
+
+ function start(box) {
+ //Add overlay and loading img
+ box.append(overlay);
+
+ settings.onLoadStart.call(box);
+ }
+
+ function done(box) {
+ //Remove overlay and loading img
+ box.find(overlay).remove();
+
+ settings.onLoadDone.call(box);
+ }
+
+ };
+
+})(jQuery);
+
+/*
+ * EXPLICIT BOX CONTROLS
+ * -----------------------
+ * This is a custom plugin to use with the component BOX. It allows you to activate
+ * a box inserted in the DOM after the app.js was loaded, toggle and remove box.
+ *
+ * @type plugin
+ * @usage $("#box-widget").activateBox();
+ * @usage $("#box-widget").toggleBox();
+ * @usage $("#box-widget").removeBox();
+ */
+(function ($) {
+
+ 'use strict';
+
+ $.fn.activateBox = function () {
+ $.AdminLTE.boxWidget.activate(this);
+ };
+
+ $.fn.toggleBox = function () {
+ var button = $($.AdminLTE.boxWidget.selectors.collapse, this);
+ $.AdminLTE.boxWidget.collapse(button);
+ };
+
+ $.fn.removeBox = function () {
+ var button = $($.AdminLTE.boxWidget.selectors.remove, this);
+ $.AdminLTE.boxWidget.remove(button);
+ };
+
+})(jQuery);
+
+/*
+ * TODO LIST CUSTOM PLUGIN
+ * -----------------------
+ * This plugin depends on iCheck plugin for checkbox and radio inputs
+ *
+ * @type plugin
+ * @usage $("#todo-widget").todolist( options );
+ */
+(function ($) {
+
+ 'use strict';
+
+ $.fn.todolist = function (options) {
+ // Render options
+ var settings = $.extend({
+ //When the user checks the input
+ onCheck: function (ele) {
+ return ele;
+ },
+ //When the user unchecks the input
+ onUncheck: function (ele) {
+ return ele;
+ }
+ }, options);
+
+ return this.each(function () {
+
+ if (typeof $.fn.iCheck != 'undefined') {
+ $('input', this).on('ifChecked', function () {
+ var ele = $(this).parents("li").first();
+ ele.toggleClass("done");
+ settings.onCheck.call(ele);
+ });
+
+ $('input', this).on('ifUnchecked', function () {
+ var ele = $(this).parents("li").first();
+ ele.toggleClass("done");
+ settings.onUncheck.call(ele);
+ });
+ } else {
+ $('input', this).on('change', function () {
+ var ele = $(this).parents("li").first();
+ ele.toggleClass("done");
+ if ($('input', ele).is(":checked")) {
+ settings.onCheck.call(ele);
+ } else {
+ settings.onUncheck.call(ele);
+ }
+ });
+ }
+ });
+ };
+}(jQuery));
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/attrchange.js b/login/app/sprinkles/core/assets/userfrosting/js/attrchange.js
new file mode 100755
index 0000000..00878d3
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/attrchange.js
@@ -0,0 +1,124 @@
+/*
+A simple jQuery function that can add listeners on attribute change.
+http://meetselva.github.io/attrchange/
+
+About License:
+Copyright (C) 2013-2014 Selvakumar Arumugam
+You may use attrchange plugin under the terms of the MIT Licese.
+https://github.com/meetselva/attrchange/blob/master/MIT-License.txt
+ */
+(function($) {
+ function isDOMAttrModifiedSupported() {
+ var p = document.createElement('p');
+ var flag = false;
+
+ if (p.addEventListener) {
+ p.addEventListener('DOMAttrModified', function() {
+ flag = true
+ }, false);
+ } else if (p.attachEvent) {
+ p.attachEvent('onDOMAttrModified', function() {
+ flag = true
+ });
+ } else { return false; }
+ p.setAttribute('id', 'target');
+ return flag;
+ }
+
+ function checkAttributes(chkAttr, e) {
+ if (chkAttr) {
+ var attributes = this.data('attr-old-value');
+
+ if (e.attributeName.indexOf('style') >= 0) {
+ if (!attributes['style'])
+ attributes['style'] = {}; //initialize
+ var keys = e.attributeName.split('.');
+ e.attributeName = keys[0];
+ e.oldValue = attributes['style'][keys[1]]; //old value
+ e.newValue = keys[1] + ':'
+ + this.prop("style")[$.camelCase(keys[1])]; //new value
+ attributes['style'][keys[1]] = e.newValue;
+ } else {
+ e.oldValue = attributes[e.attributeName];
+ e.newValue = this.attr(e.attributeName);
+ attributes[e.attributeName] = e.newValue;
+ }
+
+ this.data('attr-old-value', attributes); //update the old value object
+ }
+ }
+
+ //initialize Mutation Observer
+ var MutationObserver = window.MutationObserver
+ || window.WebKitMutationObserver;
+
+ $.fn.attrchange = function(a, b) {
+ if (typeof a == 'object') {//core
+ var cfg = {
+ trackValues : false,
+ callback : $.noop
+ };
+ //backward compatibility
+ if (typeof a === "function") { cfg.callback = a; } else { $.extend(cfg, a); }
+
+ if (cfg.trackValues) { //get attributes old value
+ this.each(function(i, el) {
+ var attributes = {};
+ for ( var attr, i = 0, attrs = el.attributes, l = attrs.length; i < l; i++) {
+ attr = attrs.item(i);
+ attributes[attr.nodeName] = attr.value;
+ }
+ $(this).data('attr-old-value', attributes);
+ });
+ }
+
+ if (MutationObserver) { //Modern Browsers supporting MutationObserver
+ var mOptions = {
+ subtree : false,
+ attributes : true,
+ attributeOldValue : cfg.trackValues
+ };
+ var observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(e) {
+ var _this = e.target;
+ //get new value if trackValues is true
+ if (cfg.trackValues) {
+ e.newValue = $(_this).attr(e.attributeName);
+ }
+ if ($(_this).data('attrchange-status') === 'connected') { //execute if connected
+ cfg.callback.call(_this, e);
+ }
+ });
+ });
+
+ return this.data('attrchange-method', 'Mutation Observer').data('attrchange-status', 'connected')
+ .data('attrchange-obs', observer).each(function() {
+ observer.observe(this, mOptions);
+ });
+ } else if (isDOMAttrModifiedSupported()) { //Opera
+ //Good old Mutation Events
+ return this.data('attrchange-method', 'DOMAttrModified').data('attrchange-status', 'connected').on('DOMAttrModified', function(event) {
+ if (event.originalEvent) { event = event.originalEvent; }//jQuery normalization is not required
+ event.attributeName = event.attrName; //property names to be consistent with MutationObserver
+ event.oldValue = event.prevValue; //property names to be consistent with MutationObserver
+ if ($(this).data('attrchange-status') === 'connected') { //disconnected logically
+ cfg.callback.call(this, event);
+ }
+ });
+ } else if ('onpropertychange' in document.body) { //works only in IE
+ return this.data('attrchange-method', 'propertychange').data('attrchange-status', 'connected').on('propertychange', function(e) {
+ e.attributeName = window.event.propertyName;
+ //to set the attr old value
+ checkAttributes.call($(this), cfg.trackValues, e);
+ if ($(this).data('attrchange-status') === 'connected') { //disconnected logically
+ cfg.callback.call(this, e);
+ }
+ });
+ }
+ return this;
+ } else if (typeof a == 'string' && $.fn.attrchange.hasOwnProperty('extensions') &&
+ $.fn.attrchange['extensions'].hasOwnProperty(a)) { //extensions/options
+ return $.fn.attrchange['extensions'][a].call(this, b);
+ }
+ }
+})(jQuery);
\ No newline at end of file
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js b/login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js
new file mode 100755
index 0000000..b008cf2
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js
@@ -0,0 +1,57 @@
+$.validator.addMethod("equals", function(value, element, params) {
+ params = $.extend(
+ true, // deep extend
+ {
+ value: '',
+ caseSensitive: false
+ }, params);
+
+ if (!params.caseSensitive) {
+ params.value = params.value.toLowerCase();
+ value = value.toLowerCase();
+ }
+ return this.optional(element) || value == params.value;
+}, "Value is not correct.");
+
+$.validator.addMethod("notEquals", function(value, element, params) {
+ params = $.extend(
+ true, // deep extend
+ {
+ value: '',
+ caseSensitive: false
+ }, params);
+
+ if (!params.caseSensitive) {
+ params.value = params.value.toLowerCase();
+ value = value.toLowerCase();
+ }
+ return this.optional(element) || value != params.value;
+}, "Value is not correct.");
+
+$.validator.addMethod("noLeadingWhitespace", function(value, element) {
+ return this.optional(element) || /^\S.*$/i.test(value);
+}, "No leading whitespace allowed");
+
+$.validator.addMethod("noTrailingWhitespace", function(value, element) {
+ return this.optional(element) || /^.*\S$/i.test(value);
+}, "No trailing whitespace allowed");
+
+jQuery.validator.addMethod("memberOf", function(value, element, arr) {
+ return $.inArray(value, arr) != -1;
+}, "Data provided must match one of the provided options.");
+
+jQuery.validator.addMethod("notMemberOf", function(value, element, arr) {
+ return $.inArray(value, arr) == -1;
+}, "Data provided must NOT match one of the provided options.");
+
+jQuery.validator.addMethod("matchFormField", function(value, element, field) {
+ return value === $(element).closest('form').find("input[name=" + field + "]").val();
+}, "The specified fields must match.");
+
+jQuery.validator.addMethod("notMatchFormField", function(value, element, field) {
+ return value !== $(element).closest('form').find("input[name=" + field + "]").val();
+}, "The specified fields must NOT match.");
+
+$.validator.addMethod("username", function(value, element) {
+ return this.optional(element) || /^([a-z0-9\.\-_])+$/.test(value);
+}, "The field may only contain lowercase letters, digits, '.', '-', and '_'.");
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js b/login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js
new file mode 100755
index 0000000..96f47bb
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js
@@ -0,0 +1,119 @@
+/**
+ * This file contains extra helper functions for Handlebars.js.
+ *
+ * @see http://handlebarsjs.com/#helpers
+ */
+
+ /**
+ * Improved comparison operator
+ * See https://stackoverflow.com/a/16315366/2970321
+ */
+Handlebars.registerHelper('ifx', function (v1, operator, v2, options) {
+ switch (operator) {
+ case '==':
+ return (v1 == v2) ? options.fn(this) : options.inverse(this);
+ case '===':
+ return (v1 === v2) ? options.fn(this) : options.inverse(this);
+ case '!=':
+ return (v1 != v2) ? options.fn(this) : options.inverse(this);
+ case '!==':
+ return (v1 !== v2) ? options.fn(this) : options.inverse(this);
+ case '<':
+ return (v1 < v2) ? options.fn(this) : options.inverse(this);
+ case '<=':
+ return (v1 <= v2) ? options.fn(this) : options.inverse(this);
+ case '>':
+ return (v1 > v2) ? options.fn(this) : options.inverse(this);
+ case '>=':
+ return (v1 >= v2) ? options.fn(this) : options.inverse(this);
+ case '&&':
+ return (v1 && v2) ? options.fn(this) : options.inverse(this);
+ case '||':
+ return (v1 || v2) ? options.fn(this) : options.inverse(this);
+ default:
+ return (v1 == v2) ? options.fn(this) : options.inverse(this);
+ }
+});
+
+/**
+ * Perform simple calculations.
+ *
+ * usage: {{calc x '+' 2}}
+ */
+Handlebars.registerHelper('calc', function (v1, operator, v2, options) {
+ lvalue = parseFloat(v1);
+ rvalue = parseFloat(v2);
+
+ return {
+ "+": lvalue + rvalue,
+ "-": lvalue - rvalue,
+ "*": lvalue * rvalue,
+ "/": lvalue / rvalue,
+ "%": lvalue % rvalue
+ }[operator];
+});
+
+/**
+ * format an ISO date using Moment.js
+ *
+ * moment syntax example: moment(Date("2011-07-18T15:50:52")).format("MMMM YYYY")
+ * usage: {{dateFormat creation_date format="MMMM YYYY"}}
+ * @requires momentjs http://momentjs.com/
+ */
+Handlebars.registerHelper('dateFormat', function(context, block) {
+ if (window.moment) {
+ var f = block.hash.format || "MMM Do, YYYY";
+ return moment(context).format(f);
+ } else {
+ // moment plugin not available. return data as is.
+ console.log("The moment.js plugin is not loaded. Please make sure you have included moment.js on this page.");
+ return context;
+ }
+});
+
+/**
+ * Format a phone number.
+ */
+Handlebars.registerHelper("phoneUSFormat", function(phoneNumber) {
+ if (typeof phoneNumber === 'undefined') {
+ return '';
+ }
+
+ phoneNumber = phoneNumber.toString();
+ return "(" + phoneNumber.substr(0,3) + ") " + phoneNumber.substr(3,3) + "-" + phoneNumber.substr(6,4);
+});
+
+/**
+ * Format currency (USD).
+ */
+Handlebars.registerHelper("currencyUsdFormat", function(amount) {
+ var parsedAmount = parseFloat(amount);
+ if (parsedAmount < 0) {
+ return "-$" + Math.abs(parsedAmount).toFixed(2);
+ } else {
+ return "$" + parsedAmount.toFixed(2);
+ }
+});
+
+/**
+ * Convert a string to a slug using speakingurl.js.
+ *
+ * @requires speakingurl https://pid.github.io/speakingurl/
+ */
+Handlebars.registerHelper('slug', function(text) {
+ return getSlug(text);
+});
+
+/**
+ * Equality helper for Handlebars
+ * http://stackoverflow.com/questions/8853396/logical-operator-in-a-handlebars-js-if-conditional/21915381#21915381
+ * @deprecated since 4.1 - use ifx instead
+ * usage: {{ifCond apple orange}}
+ */
+Handlebars.registerHelper('ifCond', function(v1, v2, options) {
+ if(v1 == v2) {
+ return options.fn(this);
+ }
+
+ return options.inverse(this);
+});
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/query-string.js b/login/app/sprinkles/core/assets/userfrosting/js/query-string.js
new file mode 100755
index 0000000..5e0d780
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/query-string.js
@@ -0,0 +1,65 @@
+/**
+ * @add jQuery.String
+ */
+$.String = $.extend($.String || {}, {
+ /**
+ * @function deparam
+ *
+ * Takes a string of name value pairs and returns a Object literal that represents those params.
+ *
+ * @param {String} params a string like "foo=bar&person[age]=3"
+ * @return {Object} A JavaScript Object that represents the params:
+ *
+ * {
+ * foo: "bar",
+ * person: {
+ * age: "3"
+ * }
+ * }
+ */
+ deparam: function(params){
+ var digitTest = /^\d+$/,
+ keyBreaker = /([^\[\]]+)|(\[\])/g,
+ plus = /\+/g,
+ paramTest = /([^?#]*)(#.*)?$/;
+
+ if(! params || ! paramTest.test(params) ) {
+ return {};
+ }
+
+
+ var data = {},
+ pairs = params.split('&'),
+ current;
+
+ for(var i=0; i < pairs.length; i++){
+ current = data;
+ var pair = pairs[i].split('=');
+
+ // if we find foo=1+1=2
+ if(pair.length != 2) {
+ pair = [pair[0], pair.slice(1).join("=")]
+ }
+
+ var key = decodeURIComponent(pair[0].replace(plus, " ")),
+ value = decodeURIComponent(pair[1].replace(plus, " ")),
+ parts = key.match(keyBreaker);
+
+ for ( var j = 0; j < parts.length - 1; j++ ) {
+ var part = parts[j];
+ if (!current[part] ) {
+ // if what we are pointing to looks like an array
+ current[part] = digitTest.test(parts[j+1]) || parts[j+1] == "[]" ? [] : {}
+ }
+ current = current[part];
+ }
+ lastPart = parts[parts.length - 1];
+ if(lastPart == "[]"){
+ current.push(value)
+ }else{
+ current[lastPart] = value;
+ }
+ }
+ return data;
+ }
+});
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js b/login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js
new file mode 100755
index 0000000..1be9c85
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js
@@ -0,0 +1,271 @@
+/*! Widget: sort2Hash (BETA) - updated 8/12/2017 (v2.28.15.uf) */
+/* Requires tablesorter v2.8+ and jQuery 1.7+
+ * by Rob Garrison
+ *
+ * Temporary patched version of widget to handle browser history issues (#712).
+ */
+;( function( $ ) {
+ 'use strict';
+ var ts = $.tablesorter || {},
+ s2h = ts.sort2Hash = {
+ init : function( c, wo ) {
+ var filter, temp, page, size,
+ table = c.table,
+ pager = c.pager,
+ hasSaveSort = ts.hasWidget( table, 'saveSort' ),
+ sort = s2h.decodeHash( c, wo, 'sort' );
+ if ( ( sort && !hasSaveSort ) || ( sort && hasSaveSort && wo.sort2Hash_overrideSaveSort ) ) {
+ s2h.convertString2Sort( c, wo, sort );
+ }
+ if ( ts.hasWidget( c.table, 'pager' ) ) {
+ temp = parseInt( s2h.decodeHash( c, wo, 'page' ), 10 );
+ page = pager.page = ( temp < 0 ? 0 : ( temp > pager.totalPages ? pager.totalPages - 1 : temp ) );
+ size = pager.size = parseInt( s2h.decodeHash( c, wo, 'size' ), 10 );
+ }
+ if ( ts.hasWidget( table, 'filter' ) ) {
+ filter = s2h.decodeHash( c, wo, 'filter' );
+ if ( filter ) {
+ filter = filter.split( wo.sort2Hash_separator );
+ c.$table.one( 'tablesorter-ready', function() {
+ setTimeout(function(){
+ c.$table.one( 'filterEnd', function() {
+ $(this).triggerHandler( 'pageAndSize', [ page, size ] );
+ });
+ // use the newest filter comparison code
+ if ( ts.filter.equalFilters ) {
+ temp = ts.filter.equalFilters( c, c.lastSearch, filter );
+ } else {
+ // quick n' dirty comparison... it will miss filter changes of
+ // the same value in a different column, see #1363
+ temp = ( c.lastSearch || [] ).join( '' ) !== ( filter || [] ).join( '' );
+ }
+ // don't set filters if they haven't changed
+ if ( !temp ) {
+ $.tablesorter.setFilters( table, filter, true );
+ }
+ }, 100 );
+ });
+ }
+ }
+ if ( !filter ) {
+ c.$table.one( 'tablesorter-ready', function() {
+ c.$table.triggerHandler( 'pageAndSize', [ page, size ] );
+ });
+ }
+
+ c.$table.on( 'sortEnd.sort2hash filterEnd.sort2hash pagerComplete.sort2Hash', function() {
+ if ( this.hasInitialized ) {
+ s2h.setHash( this.config, this.config.widgetOptions );
+ }
+ });
+ },
+
+ getTableId : function( c, wo ) {
+ // option > table id > table index on page
+ return wo.sort2Hash_tableId ||
+ c.table.id ||
+ 'table' + $( 'table' ).index( c.$table );
+ },
+ regexEscape : function( v ) {
+ return v.replace( /([\.\^\$\*\+\-\?\(\)\[\]\{\}\\\|])/g, '\\$1');
+ },
+ // convert 'first%20name,asc,last%20name,desc' into [[0,0], [1,1]]
+ convertString2Sort : function( c, wo, sortHash ) {
+ var regex, column, direction, temp, index, $cell,
+ arry = sortHash.split( wo.sort2Hash_separator ),
+ indx = 0,
+ len = arry.length,
+ sort = [];
+ while ( indx < len ) {
+ // column index or text
+ column = arry[ indx++ ];
+ temp = parseInt( column, 10 );
+ // ignore wo.sort2Hash_useHeaderText setting &
+ // just see if column contains a number
+ if ( isNaN( temp ) || temp > c.columns ) {
+ regex = new RegExp( '(' + s2h.regexEscape( column ) + ')', 'i' );
+ for ( index = 0; index < c.columns; index++ ) {
+ $cell = c.$headerIndexed[ index ];
+ if ( regex.test( $cell.attr( wo.sort2Hash_headerTextAttr ) ) ) {
+ column = index;
+ index = c.columns;
+ }
+ }
+ }
+ direction = arry[ indx++ ];
+ // ignore unpaired values
+ if ( typeof column !== 'undefined' && typeof direction !== 'undefined' ) {
+ // convert text to 0, 1
+ if ( isNaN( direction ) ) {
+ // default to ascending sort
+ direction = direction.indexOf( wo.sort2Hash_directionText[ 1 ] ) > -1 ? 1 : 0;
+ }
+ sort.push( [ column, direction ] );
+ }
+ }
+ if ( sort.length ) {
+ c.sortList = sort;
+ }
+ },
+
+ // convert [[0,0],[1,1]] to 'first%20name,asc,last%20name,desc'
+ convertSort2String : function( c, wo ) {
+ var index, txt, column, direction,
+ sort = [],
+ arry = c.sortList || [],
+ len = arry.length;
+ for ( index = 0; index < len; index++ ) {
+ column = arry[ index ][ 0 ];
+ txt = $.trim( c.$headerIndexed[ column ].attr( wo.sort2Hash_headerTextAttr ) );
+ sort.push( txt !== '' ? encodeURIComponent( txt ) : column );
+ direction = wo.sort2Hash_directionText[ arry[ index ][ 1 ] ];
+ sort.push( direction );
+ }
+ // join with separator
+ return sort.join( wo.sort2Hash_separator );
+ },
+
+ convertFilter2String : function( c, wo ) {
+ var index, txt, column, direction,
+ sort = [],
+ arry = c.sortList || [],
+ len = arry.length;
+ for ( index = 0; index < len; index++ ) {
+ column = arry[ index ][ 0 ];
+ txt = $.trim( c.$headerIndexed[ column ].attr( wo.sort2Hash_headerTextAttr ) );
+ column = typeof txt !== 'undefined' ? encodeURIComponent( txt ) : column;
+ sort.push( column );
+ direction = wo.sort2Hash_directionText[ arry[ index ][ 1 ] ];
+ sort.push( direction );
+ }
+ // join with separator
+ return sort.join( wo.sort2Hash_separator );
+ },
+
+ // Get URL Parameters (getParam)
+ // modified from http://www.netlobo.com/url_query_string_javascript.html
+ getParam : function ( name, hash, returnRegex ) {
+ if ( !hash ) { hash = window.location.hash; }
+ var regex = new RegExp( '[\\?&]' + s2h.regexEscape( name ) + '=([^]*)' ),
+ match = regex.exec( hash );
+ if ( returnRegex ) { return regex; }
+ return match === null ? '' : decodeURIComponent( match[ 1 ] );
+ },
+
+ // remove parameter from hash
+ removeParam : function( name, hash ) {
+ if ( !hash ) { hash = window.location.hash; }
+ var index,
+ regex = s2h.getParam( name, hash, true ),
+ result = [],
+ parts = hash.split( '&' ),
+ len = parts.length;
+ for ( index = 0; index < len; index++ ) {
+ // regex expects a leading '&'...
+ if ( !regex.test( '&' + parts[ index ] ) ) {
+ result.push( parts[ index ] );
+ }
+ }
+ return result.length ? result.join( '&' ) : '';
+ },
+
+ encodeHash : function( c, wo, component, value, rawValue ) {
+ var result = false,
+ tableId = s2h.getTableId( c, wo );
+ if ( typeof wo.sort2Hash_encodeHash === 'function' ) {
+ result = wo.sort2Hash_encodeHash( c, tableId, component, value, rawValue || value );
+ }
+ if ( result === false ) {
+ result = '&' + component + '[' + tableId + ']=' + value;
+ }
+ return result;
+ },
+
+ decodeHash : function( c, wo, component ) {
+ var result = false,
+ tableId = s2h.getTableId( c, wo );
+ if ( typeof wo.sort2Hash_decodeHash === 'function' ) {
+ // return a string
+ result = wo.sort2Hash_decodeHash( c, tableId, component );
+ }
+ if ( result === false ) {
+ result = s2h.getParam( component + '[' + tableId + ']' );
+ }
+ return result || '';
+ },
+
+ cleanHash : function( c, wo, component, hash ) {
+ var result = false,
+ tableId = s2h.getTableId( c, wo );
+ if ( typeof wo.sort2Hash_cleanHash === 'function' ) {
+ // can return an array or string
+ result = wo.sort2Hash_cleanHash( c, tableId, component, hash );
+ }
+ if ( result === false ) {
+ // parameter example: 'sort[table0]=0,0'
+ result = s2h.removeParam( component + '[' + tableId + ']', hash );
+ }
+ return result || '';
+ },
+
+ setHash : function( c, wo ) {
+ var str = '',
+ hash = window.location.hash,
+ hasPager = ts.hasWidget( c.table, 'pager' ),
+ hasFilter = ts.hasWidget( c.table, 'filter' ),
+ sortList = s2h.convertSort2String( c, wo ),
+ filters = ( hasFilter && c.lastSearch.join('') !== '' ? c.lastSearch : [] ),
+ filtersStr = encodeURIComponent( filters.join( c.widgetOptions.sort2Hash_separator ) ),
+ components = {
+ 'sort' : sortList ? s2h.encodeHash( c, wo, 'sort', sortList, c.sortList ) : '',
+ 'page' : hasPager ? s2h.encodeHash( c, wo, 'page', c.pager.page + 1 ) : '',
+ 'size' : hasPager ? s2h.encodeHash( c, wo, 'size', c.pager.size ) : '',
+ 'filter' : filtersStr ? s2h.encodeHash( c, wo, 'filter', filtersStr, filters ) : ''
+ };
+ // remove old hash
+ $.each( components, function( component, value ) {
+ hash = s2h.cleanHash( c, wo, component, hash );
+ str += value;
+ });
+
+ // Combine new hash with any existing hashes
+ var hashChar = c.widgetOptions.sort2Hash_hash;
+ var newHash = ( ( window.location.hash || '' ).replace( hashChar, '' ).length ? hash : wo.sort2Hash_hash ) + str;
+ var baseUrl = window.location.href.split(hashChar)[0];
+ // Ensure that there is a leading hash character
+ var firstChar = newHash[0];
+ if (firstChar != hashChar) {
+ newHash = hashChar + newHash;
+ }
+
+ // Update URL in browser
+ window.location.replace(baseUrl + newHash);
+ }
+ };
+
+ ts.addWidget({
+ id: 'sort2Hash',
+ priority: 60, // after saveSort & pager
+ options: {
+ sort2Hash_hash : '#', // hash prefix
+ sort2Hash_separator : '-', // don't '#' or '=' here
+ sort2Hash_headerTextAttr : 'data-header', // data attribute containing alternate header text
+ sort2Hash_directionText : [ 0, 1 ], // [ 'asc', 'desc' ],
+ sort2Hash_overrideSaveSort : false, // if true, override saveSort widget if saved sort available
+
+ // this option > table ID > table index on page
+ sort2Hash_tableId : null,
+ // custom hash processing functions
+ sort2Hash_encodeHash : null,
+ sort2Hash_decodeHash : null,
+ sort2Hash_cleanHash : null
+ },
+ init: function(table, thisWidget, c, wo) {
+ s2h.init( c, wo );
+ },
+ remove: function(table, c) {
+ c.$table.off( '.sort2hash' );
+ }
+ });
+
+})(jQuery);
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js
new file mode 100755
index 0000000..06a889c
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js
@@ -0,0 +1,285 @@
+/**
+ * ufAlerts jQuery plugin. Fetches and renders alerts from the UF alert stream.
+ *
+ * Based on template from https://github.com/jquery-boilerplate/jquery-boilerplate
+ *
+ * === USAGE ===
+
+ * ufAlerts can be initialized on any container element as follows:
+ *
+ * $('#myDiv').ufAlerts(options);
+ *
+ * `options` is an object containing any of the following parameters:
+ * @param {string} url The absolute URL from which to fetch flash alerts.
+ * @param {bool} scrollToTop Whether to automatically scroll back to the top of the page after rendering alerts.
+ * @param {string} alertMessageClass The CSS class(es) to be applied to each alert message.
+ * @param {string} alertTemplateId The CSS id(es) for the Handlebar alert template.
+ * @param {bool} agglomerate Set to true to render all alerts together, applying styling for the highest-priority alert being rendered.
+ *
+ * == EVENTS ==
+ *
+ * uf-form triggers the following events:
+ *
+ * `fetch.ufAlerts`: triggered when the alerts have been successfully fetched from the server.
+ * `render.ufAlerts`: triggered when all alerts have been rendered and the call to render() has completed.
+ *
+ * == METHODS ==
+ *
+ * `fetch()`: Asynchronously gets alerts from the server.
+ * `push(options)`: Adds a alert of a specified type (danger, warning, info, success) to the internal collection of alerts.
+ * `clear()`: Removes all messages from the internal collection.
+ * `render()`: Renders the collection of alerts to the container, awaiting results of `fetch()` if required.
+ *
+ * UserFrosting https://www.userfrosting.com
+ * @author Alexander Weissman
+ */
+;(function($, window, document, undefined) {
+ 'use strict';
+
+ // Define plugin name and defaults.
+ var pluginName = 'ufAlerts',
+ defaults = {
+ url : site.uri.public + '/alerts',
+ scrollToTop : true,
+ scrollWhenVisible : false,
+ agglomerate : false,
+ alertMessageClass : 'uf-alert-message',
+ alertTemplateId : 'uf-alert-template',
+ DEBUG : false
+ };
+
+ // Constructor
+ function Plugin (element, options) {
+ this.element = element[0];
+ this.$element = $(this.element);
+ this.settings = $.extend(true, {}, defaults, options);
+ this._defaults = defaults;
+ this._name = pluginName;
+
+ // Detect changes to element attributes
+ this.$element.attrchange({ callback: function (event) { this.element = event.target; }.bind(this) });
+
+ // Plugin variables
+ this.alerts = [];
+ this._newAlertsPromise = $.Deferred().resolve();
+ this._alertTemplateHtml = $('#' + this.settings.alertTemplateId).html();
+ this._alertTypePriorities = {
+ danger : 3,
+ warning: 2,
+ success: 1,
+ info : 0
+ };
+ this._alertTypeIcon = {
+ danger : 'fa-ban',
+ warning: 'fa-warning',
+ success: 'fa-check',
+ info : 'fa-info'
+ };
+
+ return this;
+ }
+
+ // Functions
+ $.extend(Plugin.prototype, {
+ /**
+ * Clear all alerts from the current uf-alerts collection.
+ */
+ clear: function() {
+ // See http://stackoverflow.com/a/1232046/2970321
+ this.alerts.length = 0;
+
+ if (this.settings.agglomerate) {
+ this.element.toggleClass('alert', false)
+ .toggleClass('alert-info', false)
+ .toggleClass('alert-success', false)
+ .toggleClass('alert-warning', false)
+ .toggleClass('alert-danger', false);
+ }
+
+ // Clear any alert HTML
+ this.$element.empty();
+
+ return this.$element;
+ },
+ /**
+ * Fetches alerts from the alert stream
+ */
+ fetch: function() {
+ // Set a promise, so that any chained calls after fetch can wait until the messages have been retrieved
+ this._newAlertsPromise = $.ajax({
+ url: this.settings.url,
+ cache: false
+ }).then(
+ // Success
+ this._fetchSuccess.bind(this),
+ // Failure
+ this._fetchFailure.bind(this)
+ );
+
+ return this.$element;
+ },
+ /**
+ * Success callback for fetch
+ */
+ _fetchSuccess: function(alerts) {
+ if (alerts != null) this.alerts = $.merge(this.alerts, alerts);
+ this.$element.trigger('fetch.' + this._name);
+ },
+ /**
+ * Failure callback for fetch
+ */
+ _fetchFailure: function(response) {
+ this.$element.trigger('error.' + this._name);
+ if ((typeof site !== 'undefined') && site.debug.ajax && response.responseText) {
+ document.write(response.responseText);
+ document.close();
+ } else {
+ if (this.settings.DEBUG) {
+ console.warn('Error (' + response.status + '): ' + response.responseText );
+ }
+ }
+ },
+ /**
+ * Push a given message to the current uf-alerts collection.
+ */
+ push: function(options) {
+ this.alerts.push({
+ type : options[0],
+ message: options[1]
+ });
+
+ return this.$element;
+ },
+ /**
+ * Renders the alerts.
+ */
+ render: function() {
+ // Wait for promise completion, only if promise is unresolved.
+ if (this._newAlertsPromise.state() == 'resolved' || this._newAlertsPromise.state() == 'rejected') {
+ this._render();
+ }
+ else {
+ $.when(this._newAlertsPromise).then(this._render.bind(this));
+ }
+
+ return this.$element;
+ },
+ /*
+ * Internal private method that physically handles rendering operation.
+ */
+ _render: function() {
+ // Holds generated HTML
+ var alertHtml = '';
+ // Only compile alerts if there are alerts to display
+ if (this.alerts.length > 0) {
+ // Prepare template
+ var alertTemplate = Handlebars.compile(this._alertTemplateHtml, {noEscape: true});
+ var i;
+ // If agglomeration is enabled, set the container to the highest priority alert type
+ if (this.settings.agglomerate) {
+ // Holds generated agglomerated alerts
+ var alertMessage = '
';
+
+ // Determine overall alert priority
+ var alertContainerType = 'info';
+ for (i = 0; i < this.alerts.length; i++) {
+ if (this._alertTypePriorities[this.alerts[i].type] > this._alertTypePriorities[alertContainerType]) {
+ alertContainerType = this.alerts[i].type;
+ }
+ }
+
+ // Compile each alert
+ var aggTemplate = Handlebars.compile('
{{ message }}
');
+ for (i = 0; i < this.alerts.length; i++) {
+ alertMessage += aggTemplate(this.alerts[i]);
+ }
+
+ alertMessage += '
';
+
+ // Generate complete alert HTML
+ alertHtml = alertTemplate({
+ type : alertContainerType,
+ message: alertMessage,
+ icon : this._alertTypeIcon[alertContainerType]
+ });
+ }
+ else {
+ // Compile each alert.
+ for (i = 0; i < this.alerts.length; i++) {
+ var alert = this.alerts[i];
+
+ // Inject icon
+ alert.icon = this._alertTypeIcon[alert.type];
+
+ // Compile alert
+ alertHtml += alertTemplate(alert);
+ }
+ }
+ }
+ // Show alerts
+ this.$element.html(alertHtml);
+
+ // Scroll to top of alert location is new alerts output, and auto scrolling is enabled
+ if (this.settings.scrollToTop && alertHtml !== '') {
+ // Don't scroll if already visible, unless scrollWhenVisible is true
+ if (!this._alertsVisible() || this.settings.scrollWhenVisible) {
+ $('html, body').animate({ scrollTop: this.$element.offset().top }, 'fast');
+ }
+ }
+
+ // Trigger render events
+ this.$element.trigger('render.' + this._name);
+ },
+ /**
+ * Returns true if alerts container is completely within the viewport.
+ */
+ _alertsVisible: function() {
+ var rect = this.element.getBoundingClientRect();
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+ );
+ },
+ /**
+ * Completely destroy the ufAlerts plugin on the element.
+ */
+ destroy: function() {
+ // Unbind any bound events
+ this.$element.off('.' + this._name);
+
+ // Remove plugin from element
+ this.$element.removeData(this._name);
+
+ return this.$element;
+ }
+ });
+
+ // Handles instantiation and access to non-private methods.
+ $.fn[pluginName] = function(methodOrOptions) {
+ // Grab plugin instance
+ var instance = $(this).data(pluginName);
+ // If undefined or object, initalise plugin.
+ if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
+ // Only initalise if not previously done.
+ if (!instance) {
+ $(this).data(pluginName, new Plugin(this, methodOrOptions));
+ }
+ return this;
+ }
+ // Otherwise ensure first parameter is a valid string, and is the name of an actual function.
+ else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
+ // Ensure not a private function
+ if (methodOrOptions.indexOf('_') !== 0) {
+ return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1));
+ }
+ else {
+ console.warn('Method ' + methodOrOptions + ' is private!');
+ }
+ }
+ else {
+ console.warn('Method ' + methodOrOptions + ' does not exist.');
+ }
+ };
+})(jQuery, window, document);
\ No newline at end of file
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js
new file mode 100755
index 0000000..ed3ecea
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js
@@ -0,0 +1,13 @@
+/**
+ * This plugin reloads the captcha in the specified element.
+ */
+(function( $ ) {
+ $.fn.captcha = function() {
+ // Set the new captcha image
+ $(this).attr('src', site.uri.public + "/account/captcha?" + new Date().getTime());
+
+ // Clear whatever the user entered for the captcha value last time
+ var target = $(this).data('target');
+ $(target).val("");
+ };
+}( jQuery ));
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js
new file mode 100755
index 0000000..a2afc6e
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js
@@ -0,0 +1,345 @@
+/**
+ * uf-collection plugin. Widget for attaching/detaching related items to a single parent item (e.g. roles for a user, etc).
+ *
+ * === USAGE ===
+ *
+ * uf-collection can be initialized on a div element as follows:
+ *
+ * $("#myCollection").ufCollection(options);
+ *
+ * `options` is an object containing any of the following parameters:
+ * @param {bool} useDropdown Set to true if rows should be added using a select2 dropdown, false for free text inputs (see https://ux.stackexchange.com/a/15637/53990).
+ * @param {Object} dropdown The options to pass to the select2 plugin for the add item dropdown.
+ * @param {string} dropdown.ajax.url The url from which to fetch options (as JSON data) in the dropdown selector menu.
+ * @param {bool} selectOnClose Set to true if you want the currently highlighted dropdown item to be automatically added when the dropdown is closed for any reason.
+ * @param {string} dropdown.theme The select2 theme to use for the dropdown menu. Defaults to "default".
+ * @param {string} dropdown.placeholder Placeholder text to use in the dropdown menu before a selection is made. Defaults to "Item".
+ * @param {string} dropdown.width Width of the dropdown selector, when used. Defaults to "100%".
+ * @param {callback} transformDropdownSelection Custom transformation on objects from the dropdown before passing them to render in the collection table.
+ * @param {Object} dropdownControl a jQuery selector specifying the dropdown select2 control. Defaults to looking for a .js-select-new element inside the parent object.
+ * @param {string} dropdownTemplate A Handlebars template to use for rendering the dropdown items.
+ * @param {Object} rowContainer a jQuery selector specifying the place where rows should be added. Defaults to looking for the first tbody element inside the parent object.
+ * @param {string} rowTemplate A Handlebars template to use for rendering each row in the table.
+ *
+ * == EVENTS ==
+ *
+ * ufCollection triggers the following events:
+ *
+ * `rowAdd.ufCollection`: triggered when a new row is added to the collection.
+ * `rowDelete.ufCollection`: triggered when a row is removed from the collection.
+ * `rowTouch.ufCollection`: triggered when any inputs in a row are brought into focus.
+ *
+ * UserFrosting https://www.userfrosting.com
+ * @author Alexander Weissman
+ */
+;(function($, window, document, undefined) {
+ "use strict";
+
+ // Define plugin name and defaults.
+ var pluginName = "ufCollection",
+ defaults = {
+ useDropdown: true,
+ dropdown: {
+ ajax : {
+ url : "",
+ dataType : "json",
+ delay : 250,
+ data : function (params) {
+ return {
+ filters: {
+ info : params.term
+ }
+ };
+ },
+ processResults : function (data, params) {
+ // Process the data into dropdown options
+ var suggestions = [];
+ if (data && data.rows) {
+ suggestions = data.rows;
+ }
+ return {
+ results: suggestions
+ };
+ },
+ cache : true
+ },
+ placeholder : "Item",
+ selectOnClose : false, // Make a selection when they click out of the box/press the next button
+ theme : "default",
+ width : "100%",
+ },
+ transformDropdownSelection: function (item) {
+ return item;
+ },
+ dropdownControl : null,
+ dropdownTemplate: "",
+ rowContainer : null,
+ rowTemplate : "",
+ DEBUG : false
+ };
+
+ // Constructor
+ function Plugin (element, options) {
+ this.element = element[0];
+ this.$element = $(this.element);
+ var lateDefaults = {
+ dropdownControl: this.$element.find('.js-select-new'),
+ rowContainer: this.$element.find('tbody').first()
+ };
+ this.settings = $.extend(true, {}, defaults, lateDefaults, options);
+ this._defaults = defaults;
+ this._name = pluginName;
+
+ // Detect changes to element attributes
+ this.$element.attrchange({ callback: function (event) { this.element = event.target; }.bind(this) });
+
+ // Internal counter for adding rows to the collection. Gets updated every time `_createRow` is called.
+ this._rownum = 0;
+
+ // Keep track of last added row
+ this._lastRow = null;
+
+ // Handlebars template method
+ this._dropdownTemplateCompiled = Handlebars.compile(this.settings.dropdownTemplate);
+
+ this._rowTemplateCompiled = Handlebars.compile(this.settings.rowTemplate);
+
+ // Add container class
+ this.$element.toggleClass("uf-collection", true);
+
+ // Add bindings for any rows already present in the DOM
+ $.each(this.settings.rowContainer.find('.uf-collection-row'), $.proxy(function(idx, row) {
+ this._onNewRow($(row));
+ this._lastRow = row;
+ }, this));
+
+ // If we're using dropdown options, create the select2 and add bindings to add a new row when an option is selected
+ if (this.settings.useDropdown) {
+ this._initDropdownField(this.settings.dropdownControl);
+
+ this.settings.dropdownControl.on("select2:select", $.proxy(function(e) {
+ var item = $(e.target).select2("data")[0];
+ // Apply any transformations before rendering as a row
+ var transformed = this.settings.transformDropdownSelection(item);
+ this._createRow(transformed);
+ }, this));
+ }
+ else {
+ // Otherwise, add a new virgin row
+ this._createVirginRow();
+ }
+
+ return this;
+ }
+
+ // Functions
+ $.extend(Plugin.prototype, {
+ /**
+ * Add a new row to the collection, optionally passing in prepopulated template data.
+ */
+ addRow: function(options) {
+ // Grab params, if any
+ var params = {};
+ if (typeof options !== 'undefined') {
+ params = options[0];
+ }
+
+ this._createRow(params);
+
+ return this.$element;
+ },
+ /**
+ * Add a new 'virgin' row to the collection, optionally passing in prepopulated template data.
+ * Virgin rows are rows that have not yet been brought into focus by the user.
+ * When a virgin row is brought into focus, it loses its virgin status and a new virgin row is created.
+ */
+ addVirginRow: function(options) {
+ // Grab params, if any
+ var params = {};
+ if (typeof options !== 'undefined') {
+ params = options[0];
+ }
+
+ this._createVirginRow(params);
+
+ return this.$element;
+ },
+ /**
+ * Delete a target row.
+ */
+ deleteRow: function(row) {
+ this._deleteRow(row);
+
+ return this.$element;
+ },
+ /**
+ * Get the dropdown control for the collection, if one exists.
+ */
+ getDropdown: function() {
+ return this.settings.dropdownControl;
+ },
+ /**
+ * Get the last row added in the collection.
+ */
+ getLastRow: function () {
+ return this._lastRow;
+ },
+ /**
+ * Touch a target row.
+ */
+ touchRow: function(row) {
+ this._touchRow(row);
+
+ return this.$element;
+ },
+ /**
+ * Create a new row and attach the handler for deletion to the js-delete-row button
+ */
+ _createRow: function(params) {
+ params = $.extend(true,
+ {
+ id: "",
+ rownum: this._rownum
+ }, params);
+
+ // Generate the row and append to table
+ var newRowTemplate = this._rowTemplateCompiled(params),
+ newRow;
+
+ // Add the new row before any virgin rows in the table.
+ var virginRows = this.settings.rowContainer.find('.uf-collection-row-virgin').length;
+ if (virginRows) {
+ newRow = $(newRowTemplate).insertBefore(this.settings.rowContainer.find('.uf-collection-row-virgin:first'));
+ } else {
+ newRow = $(newRowTemplate).appendTo(this.settings.rowContainer);
+ }
+
+ this._lastRow = newRow;
+
+ // Add bindings and fire event
+ this._onNewRow(newRow);
+
+ return newRow;
+ },
+ /**
+ * Create a new, blank row with the 'virgin' status.
+ */
+ _createVirginRow: function(params) {
+ // Generate the row and append to table
+ var newRow = this._createRow(params);
+
+ // Set the row's 'virgin' status
+ newRow.addClass('uf-collection-row-virgin');
+ newRow.find('.js-delete-row').hide();
+
+ return newRow;
+ },
+ /**
+ * Delete a row from the collection.
+ */
+ _deleteRow: function(row) {
+ row.remove();
+ this.$element.trigger('rowDelete.ufCollection');
+ },
+ /**
+ * Add delete and touch bindings for a row, increment the internal row counter, and fire the rowAdd event
+ */
+ _onNewRow: function(row) {
+ // Trigger to delete row
+ row.find('.js-delete-row').on('click', $.proxy(function(e) {
+ this._deleteRow($(e.target).closest('.uf-collection-row'));
+ }, this));
+
+ // Once the new row comes into focus for the first time, it has been "touched"
+ row.find(':input').on('focus', $.proxy(function() {
+ this._touchRow(row);
+ }, this));
+
+ this._rownum += 1;
+
+ // Fire event when row has been constructed
+ this.$element.trigger('rowAdd.ufCollection', row);
+ },
+ /**
+ * Remove a row's virgin status, show the delete button, and add a new virgin row if needed
+ */
+ _touchRow: function(row) {
+ row.removeClass('uf-collection-row-virgin');
+ row.find('.js-delete-row').show();
+
+ this.$element.trigger('rowTouch.ufCollection', row);
+
+ // If we're not using dropdowns, assert that the table doesn't already have a virgin row. If not, create a new virgin row.
+ if (!this.settings.useDropdown) {
+ var virginRows = this.settings.rowContainer.find('.uf-collection-row-virgin').length;
+ if (!virginRows) {
+ this._createVirginRow();
+ }
+ }
+ },
+ /**
+ * Initialize the select2 dropdown for this collection on a specified control element.
+ */
+ _initDropdownField: function(field) {
+ var options = this.settings.dropdown;
+
+ if (!("templateResult" in options)) {
+ options.templateResult = $.proxy(function(item) {
+ // Display loading text if the item is marked as "loading"
+ if (item.loading) return item.text;
+
+ // Must wrap this in a jQuery selector to render as HTML
+ return $(this._dropdownTemplateCompiled(item));
+ }, this);
+ }
+ // Legacy options (<= v4.0.9)
+ if ("dataUrl" in this.settings) {
+ options.ajax.url = this.settings.dataUrl;
+ }
+ if ("ajaxDelay" in this.settings) {
+ options.ajax.delay = this.settings.ajaxDelay;
+ }
+ if ("dropdownTheme" in this.settings) {
+ options.theme = this.settings.dropdownTheme;
+ }
+ if ("placeholder" in this.settings) {
+ options.placeholder = this.settings.placeholder;
+ }
+ if ("selectOnClose" in this.settings) {
+ options.selectOnClose = this.settings.selectOnClose;
+ }
+ if ("width" in this.settings) {
+ options.width = this.settings.width;
+ }
+
+ return field.select2(options);
+ }
+ });
+
+ // Handles instantiation and access to non-private methods.
+ $.fn[pluginName] = function(methodOrOptions) {
+ // Grab plugin instance
+ var instance = $(this).data(pluginName);
+ // If undefined or object, initalise plugin.
+ if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
+ // Only initalise if not previously done.
+ if (!instance) {
+ $(this).data(pluginName, new Plugin(this, methodOrOptions));
+ }
+ return this;
+ }
+ // Otherwise ensure first parameter is a valid string, and is the name of an actual function.
+ else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
+ // Ensure not a private function
+ if (methodOrOptions.indexOf('_') !== 0) {
+ return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1));
+ }
+ else {
+ console.warn( 'Method ' + methodOrOptions + ' is private!' );
+ }
+ }
+ else {
+ console.warn( 'Method ' + methodOrOptions + ' does not exist.' );
+ }
+ };
+})(jQuery, window, document);
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js
new file mode 100755
index 0000000..a47f6b8
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js
@@ -0,0 +1,50 @@
+/**
+ * Copies text or control fields to clipboard. Wrap a .js-copy-target and .js-copy-trigger inside a common .js-copy-container.
+ */
+
+if (typeof $.uf === 'undefined') {
+ $.uf = {};
+}
+
+$.uf.copy = function (button) {
+ var _this = this;
+
+ var clipboard = new Clipboard(button, {
+ text: function(trigger) {
+ var el = $(trigger).closest('.js-copy-container').find('.js-copy-target');
+ if (el.is(':input')) {
+ return el.val();
+ } else {
+ return el.html();
+ }
+ }
+ });
+
+ clipboard.on('success', function(e) {
+ setTooltip(e.trigger, 'Copied!');
+ hideTooltip(e.trigger);
+ });
+
+ clipboard.on('error', function(e) {
+ setTooltip(e.trigger, 'Failed!');
+ hideTooltip(e.trigger);
+ });
+
+ function setTooltip(btn, message) {
+ $(btn)
+ .attr('data-original-title', message)
+ .tooltip('show');
+ }
+
+ function hideTooltip(btn) {
+ setTimeout(function() {
+ $(btn).tooltip('hide')
+ .attr('data-original-title', "");
+ }, 1000);
+ }
+
+ // Tooltip
+ $(button).tooltip({
+ trigger: 'click'
+ });
+};
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-form.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-form.js
new file mode 100755
index 0000000..15952ab
--- /dev/null
+++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-form.js
@@ -0,0 +1,297 @@
+/**
+ * uf-form plugin. Handles validation and submission for basic UserFrosting forms.
+ *
+ * This plugin uses the jQueryvalidation plugin (https://jqueryvalidation.org/) to perform instant, client-side form validation.
+ * UserFrosting forms must be wrapped in a