diff options
Diffstat (limited to 'main/app/sprinkles/extend-user')
14 files changed, 716 insertions, 0 deletions
diff --git a/main/app/sprinkles/extend-user/.gitignore b/main/app/sprinkles/extend-user/.gitignore new file mode 100755 index 0000000..5a664d4 --- /dev/null +++ b/main/app/sprinkles/extend-user/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.idea
+*.komodoproject
diff --git a/main/app/sprinkles/extend-user/README.md b/main/app/sprinkles/extend-user/README.md new file mode 100755 index 0000000..60a4bcd --- /dev/null +++ b/main/app/sprinkles/extend-user/README.md @@ -0,0 +1,29 @@ +# User Extension Sprinkle (UserFrosting 4.1) + +Example sprinkle for extending the User class to contain additional fields. + +# Installation + +Edit UserFrosting `app/sprinkles.json` and add the following to the `require` list : `"userfrosting/extend-user": "~4.1.1"`. Also add `extend-user` to the `base` list. For example: + +``` +{ + "require": { + "userfrosting/extend-user": "~4.1.1" + }, + "base": [ + "core", + "account", + "admin", + "extend-user" + ] +} +``` + +### Update Composer + +- Run `composer update` from the root project directory. + +### Run migration + +- Run `php bakery bake` from the root project directory. diff --git a/main/app/sprinkles/extend-user/composer.json b/main/app/sprinkles/extend-user/composer.json new file mode 100755 index 0000000..4c8a0fa --- /dev/null +++ b/main/app/sprinkles/extend-user/composer.json @@ -0,0 +1,22 @@ +{ + "name": "userfrosting/extend-user", + "type": "userfrosting-sprinkle", + "description": "An example Sprinkle for extending the User model and table with additional fields or relationships.", + "homepage": "https://github.com/userfrosting/extend-user", + "license" : "MIT", + "authors" : [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + } + ], + "autoload": { + "psr-4": { + "UserFrosting\\Sprinkle\\ExtendUser\\": "src/" + } + }, + "extra": { + "installer-name": "extend-user" + } +} + diff --git a/main/app/sprinkles/extend-user/routes/member.php b/main/app/sprinkles/extend-user/routes/member.php new file mode 100755 index 0000000..19028ac --- /dev/null +++ b/main/app/sprinkles/extend-user/routes/member.php @@ -0,0 +1,7 @@ +<?php +/** + * Routes for administrative user management. Overrides routes defined in routes://admin/users.php + */ +$app->group('/admin/users', function () { + $this->get('/u/{user_name}', 'UserFrosting\Sprinkle\ExtendUser\Controller\MemberController:pageInfo'); +})->add('authGuard');
\ No newline at end of file diff --git a/main/app/sprinkles/extend-user/schema/requests/user/create.yaml b/main/app/sprinkles/extend-user/schema/requests/user/create.yaml new file mode 100755 index 0000000..2df2955 --- /dev/null +++ b/main/app/sprinkles/extend-user/schema/requests/user/create.yaml @@ -0,0 +1,86 @@ +--- +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 +city: + validators: + length: + label: City + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE +country: + validators: + length: + label: Country + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE diff --git a/main/app/sprinkles/extend-user/schema/requests/user/edit-info.yaml b/main/app/sprinkles/extend-user/schema/requests/user/edit-info.yaml new file mode 100755 index 0000000..edfae6e --- /dev/null +++ b/main/app/sprinkles/extend-user/schema/requests/user/edit-info.yaml @@ -0,0 +1,50 @@ +--- +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 +city: + validators: + length: + label: City + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE +country: + validators: + length: + label: Country + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE
\ No newline at end of file diff --git a/main/app/sprinkles/extend-user/src/Controller/MemberController.php b/main/app/sprinkles/extend-user/src/Controller/MemberController.php new file mode 100755 index 0000000..c584286 --- /dev/null +++ b/main/app/sprinkles/extend-user/src/Controller/MemberController.php @@ -0,0 +1,123 @@ +<?php +namespace UserFrosting\Sprinkle\ExtendUser\Controller; + +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use UserFrosting\Sprinkle\Admin\Controller\UserController; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\ForbiddenException; + +class MemberController extends UserController +{ + /** + * 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', 'address']; + + // 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'; + } + + return $this->ci->view->render($response, 'pages/user.html.twig', [ + 'user' => $user, + 'locales' => $locales, + 'fields' => $fields, + 'tools' => $editButtons + ]); + } +} diff --git a/main/app/sprinkles/extend-user/src/Database/Migrations/v400/MembersTable.php b/main/app/sprinkles/extend-user/src/Database/Migrations/v400/MembersTable.php new file mode 100755 index 0000000..a27d485 --- /dev/null +++ b/main/app/sprinkles/extend-user/src/Database/Migrations/v400/MembersTable.php @@ -0,0 +1,34 @@ +<?php +namespace UserFrosting\Sprinkle\ExtendUser\Database\Migrations\v400; + +use UserFrosting\System\Bakery\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; + +class MembersTable extends Migration +{ + public $dependencies = [ + '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\UsersTable' + ]; + + public function up() + { + if (!$this->schema->hasTable('members')) { + $this->schema->create('members', function (Blueprint $table) { + $table->increments('id'); + $table->string('city', 255)->nullable(); + $table->string('country', 255)->nullable(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->foreign('id')->references('id')->on('users'); + }); + } + } + + public function down() + { + $this->schema->drop('members'); + } +} diff --git a/main/app/sprinkles/extend-user/src/Database/Models/Member.php b/main/app/sprinkles/extend-user/src/Database/Models/Member.php new file mode 100755 index 0000000..98d9d70 --- /dev/null +++ b/main/app/sprinkles/extend-user/src/Database/Models/Member.php @@ -0,0 +1,124 @@ +<?php +namespace UserFrosting\Sprinkle\ExtendUser\Database\Models; + +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\ExtendUser\Database\Models\MemberAux; +use UserFrosting\Sprinkle\ExtendUser\Database\Scopes\MemberAuxScope; + +trait LinkMemberAux +{ + /** + * The "booting" method of the trait. + * + * @return void + */ + protected static function bootLinkMemberAux() + { + /** + * Create a new MemberAux if necessary, and save the associated member data every time. + */ + static::saved(function ($member) { + $member->createAuxIfNotExists(); + + if ($member->auxType) { + // Set the aux PK, if it hasn't been set yet + if (!$member->aux->id) { + $member->aux->id = $member->id; + } + + $member->aux->save(); + } + }); + } +} + +class Member extends User +{ + use LinkMemberAux; + + protected $fillable = [ + 'user_name', + 'first_name', + 'last_name', + 'email', + 'locale', + 'theme', + 'group_id', + 'flag_verified', + 'flag_enabled', + 'last_activity_id', + 'password', + 'deleted_at', + 'city', + 'country' + ]; + + protected $auxType = 'UserFrosting\Sprinkle\ExtendUser\Database\Models\MemberAux'; + + /** + * Required to be able to access the `aux` relationship in Twig without needing to do eager loading. + * @see http://stackoverflow.com/questions/29514081/cannot-access-eloquent-attributes-on-twig/35908957#35908957 + */ + public function __isset($name) + { + if (in_array($name, [ + 'aux' + ])) { + return true; + } else { + return parent::__isset($name); + } + } + + /** + * Globally joins the `members` table to access additional properties. + */ + protected static function boot() + { + parent::boot(); + + static::addGlobalScope(new MemberAuxScope); + } + + /** + * Custom mutator for Member property + */ + public function setCityAttribute($value) + { + $this->createAuxIfNotExists(); + + $this->aux->city = $value; + } + + /** + * Custom mutator for Member property + */ + public function setCountryAttribute($value) + { + $this->createAuxIfNotExists(); + + $this->aux->country = $value; + } + + /** + * Relationship for interacting with aux model (`members` table). + */ + public function aux() + { + return $this->hasOne($this->auxType, 'id'); + } + + /** + * If this instance doesn't already have a related aux model (either in the db on in the current object), then create one + */ + protected function createAuxIfNotExists() + { + if ($this->auxType && !count($this->aux)) { + // Create aux model and set primary key to be the same as the main user's + $aux = new $this->auxType; + + // Needed to immediately hydrate the relation. It will actually get saved in the bootLinkMemberAux method. + $this->setRelation('aux', $aux); + } + } +} diff --git a/main/app/sprinkles/extend-user/src/Database/Models/MemberAux.php b/main/app/sprinkles/extend-user/src/Database/Models/MemberAux.php new file mode 100755 index 0000000..c826409 --- /dev/null +++ b/main/app/sprinkles/extend-user/src/Database/Models/MemberAux.php @@ -0,0 +1,20 @@ +<?php + +namespace UserFrosting\Sprinkle\ExtendUser\Database\Models; + +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +class MemberAux extends Model +{ + public $timestamps = false; + + /** + * @var string The name of the table for the current model. + */ + protected $table = 'members'; + + protected $fillable = [ + 'city', + 'country' + ]; +} diff --git a/main/app/sprinkles/extend-user/src/Database/Scopes/MemberAuxScope.php b/main/app/sprinkles/extend-user/src/Database/Scopes/MemberAuxScope.php new file mode 100755 index 0000000..c732147 --- /dev/null +++ b/main/app/sprinkles/extend-user/src/Database/Scopes/MemberAuxScope.php @@ -0,0 +1,36 @@ +<?php + +namespace UserFrosting\Sprinkle\ExtendUser\Database\Scopes; + +use Illuminate\Database\Eloquent\Scope; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; + +class MemberAuxScope implements Scope +{ + /** + * Apply the scope to a given Eloquent query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $builder + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + public function apply(Builder $builder, Model $model) + { + $baseTable = $model->getTable(); + // Hardcode the table name here, or you can access it using the classMapper and `getTable` + $auxTable = 'members'; + + // Specify columns to load from base table and aux table + $builder->addSelect( + "$baseTable.*", + "$auxTable.city as city", + "$auxTable.country as country" + ); + + // Join on matching `member` records + $builder->leftJoin($auxTable, function ($join) use ($baseTable, $auxTable) { + $join->on("$auxTable.id", '=', "$baseTable.id"); + }); + } +} diff --git a/main/app/sprinkles/extend-user/src/ServicesProvider/ServicesProvider.php b/main/app/sprinkles/extend-user/src/ServicesProvider/ServicesProvider.php new file mode 100755 index 0000000..8ea3860 --- /dev/null +++ b/main/app/sprinkles/extend-user/src/ServicesProvider/ServicesProvider.php @@ -0,0 +1,26 @@ +<?php + +// In /app/sprinkles/site/src/ServicesProvider/ServicesProvider.php + +namespace UserFrosting\Sprinkle\ExtendUser\ServicesProvider; + +class ServicesProvider +{ + /** + * Register extended user fields services. + * + * @param Container $container A DI container implementing ArrayAccess and container-interop. + */ + public function register($container) + { + /** + * Extend the 'classMapper' service to register model classes. + * + * Mappings added: Member + */ + $container->extend('classMapper', function ($classMapper, $c) { + $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\ExtendUser\Database\Models\Member'); + return $classMapper; + }); + } +} diff --git a/main/app/sprinkles/extend-user/templates/forms/user.html.twig b/main/app/sprinkles/extend-user/templates/forms/user.html.twig new file mode 100755 index 0000000..b7c98a9 --- /dev/null +++ b/main/app/sprinkles/extend-user/templates/forms/user.html.twig @@ -0,0 +1,145 @@ +<form class="js-form" method="{{form.method | default('POST')}}" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <div class="row"> + {% block user_form %} + {% if 'user_name' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('USERNAME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="user_name" autocomplete="off" value="{{user.user_name}}" placeholder="{{translate('USERNAME')}}" {% if 'user_name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'group' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label for="input-group">{{translate('GROUP')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-users fa-fw"></i></span> + {% if 'group' in form.fields.disabled %} + <input type="text" class="form-control" name="theme" value="{{user.group.name}}" disabled> + {% else %} + <select id="input-group" class="form-control js-select2" name="group_id"> + {% for group in groups %} + <option value="{{group.id}}" {% if (group.id == user.group_id) %}selected{% endif %}>{{group.name}}</option> + {% endfor %} + </select> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'name' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('FIRST_NAME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="first_name" autocomplete="off" value="{{user.first_name}}" placeholder="{{translate('FIRST_NAME')}}" {% if 'name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('LAST_NAME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="last_name" autocomplete="off" value="{{user.last_name}}" placeholder="{{translate('LAST_NAME')}}" {% if 'name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'email' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('EMAIL')}}</label> + <div class="input-group js-copy-container"> + <span class="input-group-addon"><i class="fa fa-envelope fa-fw"></i></span> + <input type="text" class="form-control js-copy-target" name="email" autocomplete="off" value="{{user.email}}" placeholder="{{translate('EMAIL')}}" {% if 'email' in form.fields.disabled %}disabled{% endif %}> + {% if 'email' in form.fields.disabled %} + <span class="input-group-btn"> + <button class="btn btn-default js-copy-trigger" type="button"><i class="fa fa-clipboard"></i></button> + </span> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'theme' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label for="input-theme">{{translate('THEME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-puzzle-piece fa-fw"></i></span> + {% if 'theme' in form.fields.disabled %} + <input type="text" class="form-control" name="theme" value="{{themes[user.theme]}}" disabled> + {% else %} + <select id="input-theme" class="form-control js-select2" name="theme"> + {% for option, label in theme %} + <option value="{{option}}" {% if (option == user.theme) %}selected{% endif %}>{{label}}</option> + {% endfor %} + </select> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'locale' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label for="input-locale">{{translate('LOCALE')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-language fa-fw"></i></span> + {% if 'locale' in form.fields.disabled %} + <input type="text" class="form-control" name="theme" value="{{locales[user.locale]}}" disabled> + {% else %} + <select id="input-locale" class="form-control js-select2" name="locale"> + {% for option, label in locales %} + <option value="{{option}}" {% if (option == user.locale) %}selected{% endif %}>{{label}}</option> + {% endfor %} + </select> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'address' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>City</label> + <div class="input-group js-copy-container"> + <span class="input-group-addon"><i class="fa fa-map-pin"></i></span> + <input type="text" class="form-control" name="city" autocomplete="off" value="{{user.city}}" placeholder="City" {% if 'address' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + <div class="col-sm-6"> + <div class="form-group"> + <label>Country</label> + <div class="input-group js-copy-container"> + <span class="input-group-addon"><i class="fa fa-map-pin"></i></span> + <input type="text" class="form-control" name="country" autocomplete="off" value="{{user.country}}" placeholder="Country" {% if 'address' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% endblock %} + </div><br> + <div class="row"> + <div class="col-xs-8 col-sm-4"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{form.submit_text}}</button> + </div> + <div class="col-xs-4 col-sm-3 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate('CANCEL')}}</button> + </div> + </div> +</form> +<!-- Include validation rules --> +<script> + {% include "pages/partials/page.js.twig" %} +</script> diff --git a/main/app/sprinkles/extend-user/templates/pages/user.html.twig b/main/app/sprinkles/extend-user/templates/pages/user.html.twig new file mode 100755 index 0000000..46e79aa --- /dev/null +++ b/main/app/sprinkles/extend-user/templates/pages/user.html.twig @@ -0,0 +1,11 @@ +{% extends "@admin/pages/user.html.twig" %} + +{% block user_profile %} + {% if 'locale' not in fields.hidden %} + <hr> + <strong><i class="fa fa-map-marker margin-r-5"></i> Location</strong> + <p class="text-muted box-profile-property"> + {{user.city}}, {{user.country}} + </p> + {% endif %} +{% endblock %} |