Skip to content

Commit

Permalink
Self update roles
Browse files Browse the repository at this point in the history
  • Loading branch information
Witold Wiśniewski committed Sep 20, 2023
1 parent c45cd57 commit 9a28db5
Show file tree
Hide file tree
Showing 25 changed files with 302 additions and 0 deletions.
7 changes: 7 additions & 0 deletions app/Dtos/RoleCreateDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class RoleCreateDto extends Dto implements InstantiateFromRequest
private bool|Missing $is_registration_role;
private array $permissions;
private array|Missing $metadata;
private bool|Missing $is_joinable;

public static function instantiateFromRequest(FormRequest $request): self
{
Expand All @@ -26,6 +27,7 @@ public static function instantiateFromRequest(FormRequest $request): self
is_registration_role: $request->input('is_registration_role', new Missing()),
permissions: $request->input('permissions', []),
metadata: self::mapMetadata($request),
is_joinable: $request->input('is_joinable', new Missing()),
);
}

Expand All @@ -48,4 +50,9 @@ public function getPermissions(): array
{
return $this->permissions;
}

public function getIsJoinable(): bool|Missing
{
return $this->is_joinable;
}
}
11 changes: 11 additions & 0 deletions app/Dtos/RoleSearchDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function __construct(
private ?array $metadata,
private ?array $metadata_private,
private ?array $ids,
private ?bool $is_joinable,
) {}

public function toArray(): array
Expand Down Expand Up @@ -50,6 +51,10 @@ public function toArray(): array
$data['ids'] = $this->getIds();
}

if ($this->getIsJoinable() !== null) {
$data['is_joinable'] = $this->getIsJoinable();
}

return $data;
}

Expand All @@ -63,6 +68,7 @@ public static function instantiateFromRequest(FormRequest $request): self
$request->input('metadata', null),
$request->input('metadata_private', null),
$request->input('ids', null),
$request->input('is_joinable', null),
);
}

Expand Down Expand Up @@ -100,4 +106,9 @@ public function getIds(): ?array
{
return $this->ids;
}

public function getIsJoinable(): ?bool
{
return $this->is_joinable;
}
}
7 changes: 7 additions & 0 deletions app/Dtos/RoleUpdateDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class RoleUpdateDto extends Dto implements InstantiateFromRequest
private Missing|string|null $description;
private bool|Missing $is_registration_role;
private array|Missing $permissions;
private bool|Missing $is_joinable;

public static function instantiateFromRequest(FormRequest|RoleUpdateRequest $request): self
{
Expand All @@ -22,6 +23,7 @@ public static function instantiateFromRequest(FormRequest|RoleUpdateRequest $req
description: $request->input('description', new Missing()),
is_registration_role: $request->input('is_registration_role', new Missing()),
permissions: $request->input('permissions', new Missing()),
is_joinable: $request->input('is_joinable', new Missing()),
);
}

Expand All @@ -44,4 +46,9 @@ public function getPermissions(): array|Missing
{
return $this->permissions;
}

public function getIsJoinable(): bool|Missing
{
return $this->is_joinable;
}
}
25 changes: 25 additions & 0 deletions app/Dtos/SelfUpdateRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Dtos;

use App\Enums\RoleType;
use Illuminate\Validation\Rule;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;

class SelfUpdateRoles extends Data
{
public function __construct(
public array|Optional $roles = [],
) {}

public function roles(): array
{
return [
'roles.*' => [
'uuid',
Rule::exists('roles', 'id')->where(fn ($query) => $query->whereNotIn('type', [RoleType::AUTHENTICATED, RoleType::UNAUTHENTICATED])),
],
];
}
}
1 change: 1 addition & 0 deletions app/Enums/ExceptionsEnums/Exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ final class Exceptions extends Enum
public const CLIENT_PROVIDER_MERGE_TOKEN_INVALID = 'Provider merge token is invalid';
public const CLIENT_PROVIDER_MERGE_TOKEN_MISMATCH =
'Provider merge token is for an account with different email address';
public const CLIENT_JOINING_NON_JOINABLE_ROLE = 'Can\'t join to a non joinable role';

public static function getCode(string $value): int
{
Expand Down
6 changes: 6 additions & 0 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Dtos\RegisterDto;
use App\Dtos\SavedAddressDto;
use App\Dtos\SelfUpdateRoles;
use App\Dtos\TFAConfirmDto;
use App\Dtos\TFAPasswordDto;
use App\Dtos\TFASetupDto;
Expand Down Expand Up @@ -243,4 +244,9 @@ public function updateProfile(ProfileUpdateRequest $request): JsonResource
$this->authService->updateProfile(UpdateProfileDto::instantiateFromRequest($request)),
);
}

public function selfUpdateRoles(SelfUpdateRoles $dto): JsonResource
{
return UserResource::make($this->authService->selfUpdateRoles($dto));
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/RoleIndexRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function rules(): array
'metadata_private' => ['nullable', 'array'],
'ids' => ['array'],
'ids.*' => ['uuid'],
'is_joinable' => ['boolean'],
];
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/RoleStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public function rules(): array
'is_registration_role' => ['boolean'],
'permissions' => ['array'],
'permissions.*' => ['string'],
'is_joinable' => ['boolean'],
]
);
}
Expand Down
1 change: 1 addition & 0 deletions app/Http/Requests/RoleUpdateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public function rules(): array
'is_registration_role' => ['boolean'],
'permissions' => ['array'],
'permissions.*' => ['string'],
'is_joinable' => ['boolean'],
];
}
}
1 change: 1 addition & 0 deletions app/Http/Resources/RoleResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function base(Request $request): array
'assignable' => $this->resource->isAssignable(),
'deletable' => $this->resource->type->is(RoleType::REGULAR),
'users_count' => $this->resource->users_count,
'is_joinable' => $this->resource->is_joinable,
], $this->metadataResource('roles.show_metadata_private'));
}

Expand Down
5 changes: 5 additions & 0 deletions app/Models/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Traits\HasDiscountConditions;
use App\Traits\HasMetadata;
use App\Traits\HasUuid;
use Heseya\Searchable\Criteria\Equals;
use Heseya\Searchable\Criteria\Like;
use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Builder;
Expand All @@ -26,6 +27,7 @@
* @property RoleType $type
* @property string $name
* @property bool $is_registration_role
* @property bool $is_joinable
*
* @mixin IdeHelperRole
*/
Expand All @@ -43,11 +45,13 @@ class Role extends SpatieRole implements AuditableContract
'description',
'guard_name',
'is_registration_role',
'is_joinable',
];

protected $casts = [
'type' => RoleType::class,
'is_registration_role' => 'bool',
'is_joinable' => 'bool',
];

protected array $criteria = [
Expand All @@ -58,6 +62,7 @@ class Role extends SpatieRole implements AuditableContract
'metadata' => MetadataSearch::class,
'metadata_private' => MetadataPrivateSearch::class,
'ids' => WhereInIds::class,
'is_joinable' => Equals::class,
];

public function users(): BelongsToMany
Expand Down
13 changes: 13 additions & 0 deletions app/Services/AuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Services;

use App\Dtos\RegisterDto;
use App\Dtos\SelfUpdateRoles;
use App\Dtos\TFAConfirmDto;
use App\Dtos\TFAPasswordDto;
use App\Dtos\TFASetupDto;
Expand Down Expand Up @@ -369,6 +370,18 @@ public function selfRemove(string $password): void
$this->userService->destroy($user);
}

public function selfUpdateRoles(SelfUpdateRoles $dto): User
{
if ($this->isAppAuthenticated()) {
throw new ClientException(Exceptions::CLIENT_APPS_NO_ACCESS);
}

/** @var User $user */
$user = Auth::user();

return $this->userService->selfUpdateRoles($user, $dto);
}

private function verifyTFA(?string $code): void
{
if (!Auth::user()?->is_tfa_active && $code !== null) {
Expand Down
3 changes: 3 additions & 0 deletions app/Services/Contracts/AuthServiceContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Services\Contracts;

use App\Dtos\RegisterDto;
use App\Dtos\SelfUpdateRoles;
use App\Dtos\TFAConfirmDto;
use App\Dtos\TFAPasswordDto;
use App\Dtos\TFASetupDto;
Expand Down Expand Up @@ -51,4 +52,6 @@ public function register(RegisterDto $dto): User;
public function updateProfile(UpdateProfileDto $dto): User;

public function selfRemove(string $password): void;

public function selfUpdateRoles(SelfUpdateRoles $dto): User;
}
3 changes: 3 additions & 0 deletions app/Services/Contracts/UserServiceContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Services\Contracts;

use App\Dtos\SelfUpdateRoles;
use App\Dtos\UserCreateDto;
use App\Dtos\UserDto;
use App\Models\User;
Expand All @@ -16,4 +17,6 @@ public function create(UserCreateDto $dto): User;
public function update(User $user, UserDto $dto): User;

public function destroy(User $user): void;

public function selfUpdateRoles(User $user, SelfUpdateRoles $dto): User;
}
25 changes: 25 additions & 0 deletions app/Services/UserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Services;

use App\Dtos\SelfUpdateRoles;
use App\Dtos\UserCreateDto;
use App\Dtos\UserDto;
use App\Enums\ExceptionsEnums\Exceptions;
Expand All @@ -26,6 +27,7 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Spatie\LaravelData\Optional;

readonly class UserService implements UserServiceContract
{
Expand Down Expand Up @@ -267,4 +269,27 @@ public function destroy(User $user): void
}
});
}

public function selfUpdateRoles(User $user, SelfUpdateRoles $dto): User
{
$roleModels = collect();
if (!($dto->roles instanceof Optional)) {
/** @var Collection<int, Role> $roleModels */
$roleModels = Role::query()
->whereIn('id', $dto->roles)
->where('is_joinable', '=', true)
->get();

if ($roleModels->count() < count($dto->roles)) {
throw new ClientException(Exceptions::CLIENT_JOINING_NON_JOINABLE_ROLE);
}
}

// @phpstan-ignore-next-line
$roleModels = $roleModels->merge($user->roles->filter(fn (Role $role): bool => !$role->is_joinable));

$user->syncRoles($roleModels);

return $user;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
public function up(): void
{
Schema::table('roles', function (Blueprint $table): void {
$table->boolean('is_joinable')->default(false);
});
}

public function down(): void
{
Schema::table('roles', function (Blueprint $table): void {
$table->dropColumn('is_joinable');
});
}
};
20 changes: 20 additions & 0 deletions docs/paths/Auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -585,3 +585,23 @@ BillingAddressesParams:
type: object
security:
- BearerAuth: [ ]

SelfUpdateRoles:
patch:
tags:
- Auth
summary: 'edit your own roles'
requestBody:
$ref: './../requests/Auth.yml#/SelfUpdateRoles'
responses:
200:
description: Success
content:
application/json:
schema:
properties:
data:
$ref: './../schemas/Users.yml#/UserView'
type: object
security:
- BearerAuth: [ ]
5 changes: 5 additions & 0 deletions docs/paths/Roles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ Roles:
items:
type: string
format: uuid
- name: is_joinable
in: query
description: 'Is the user able to self-assign the role'
schema:
type: boolean
responses:
200:
description: Success
Expand Down
12 changes: 12 additions & 0 deletions docs/requests/Auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,15 @@ UpdateProfile:
preferences:
$ref: '././../schemas/Auth.yml#/Preferences'
type: object

SelfUpdateRoles:
content:
application/json:
schema:
properties:
roles:
type: array
items:
type: string
example: '119c0a63-1ea1-4769-8d5f-169f68de5598'
type: object
Loading

0 comments on commit 9a28db5

Please sign in to comment.