mirror of
https://github.com/anikeen-com/id.git
synced 2026-11-08 16:48:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4c41779b3 | ||
|
|
ee05bf8aa5 | ||
|
|
642e518259 |
No files matched your search
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## 3.5.0 (2026-11-08)
|
||||
|
||||
The user's role for trusted apps; it needs an Anikeen ID release that sends it.
|
||||
|
||||
### Added
|
||||
|
||||
- `Enums\Role` (`ADMIN`, `USER`): the user's Anikeen ID role, seen by trusted apps only.
|
||||
- The Socialite user carries it as `$user->role` (`null` for other apps).
|
||||
- `Resources\User::role()` and `isAdmin()`, for a user read by ID (`findUserById()`) and the
|
||||
`data.object` of `user.updated`, which for trusted apps carries `role` (and `role` in `changed`).
|
||||
|
||||
## 3.4.0 (2026-11-07)
|
||||
|
||||
Preferences, charges, the billing limit, user profiles and the user and preference webhooks; they
|
||||
|
||||
@@ -148,6 +148,22 @@ Route::get('auth/callback', function () {
|
||||
});
|
||||
```
|
||||
|
||||
### The user's role (trusted apps)
|
||||
|
||||
[Trusted apps](#trusted-apps) also get the user's Anikeen ID role, `admin` (runs Anikeen) or
|
||||
`user` (constants on `Anikeen\Id\Enums\Role`); for other apps it is `null`. An app can make
|
||||
Anikeen ID admins its own admins this way instead of keeping a list of its own. Read it at sign-in
|
||||
and keep it current with the `user.updated` webhook (`role` in `changed`, see
|
||||
[User profiles](#user-profiles)):
|
||||
|
||||
```php
|
||||
use Anikeen\Id\Enums\Role;
|
||||
|
||||
$account = Socialite::driver('anikeen')->user();
|
||||
|
||||
$user->update(['role' => $account->role ?? Role::USER]);
|
||||
```
|
||||
|
||||
Access tokens expire. Get a new one with the refresh token (or send the user through the login
|
||||
again):
|
||||
|
||||
@@ -1207,8 +1223,14 @@ $profile?->emailVerified();
|
||||
$profile?->avatar();
|
||||
$profile?->version(); // compare with the version of user.updated
|
||||
$profile?->locale; // also username, created_at
|
||||
$profile?->role(); // Role::ADMIN or Role::USER (trusted apps only)
|
||||
$profile?->isAdmin();
|
||||
```
|
||||
|
||||
The `data.object` of `user.updated` is the same profile (`new \Anikeen\Id\Resources\User($event['data']['object'])`);
|
||||
for trusted apps it carries `role`, and a change of the role alone (`changed` is `["role"]`) is
|
||||
sent to them only.
|
||||
|
||||
It throws `ApiException` for 403 `client_not_allowed` (not a trusted app), 429
|
||||
`too_many_requests` and other failures. The profile carries no billing or account data; read those
|
||||
through their own endpoints.
|
||||
|
||||
+22
@@ -148,6 +148,22 @@ Route::get('auth/callback', function () {
|
||||
});
|
||||
```
|
||||
|
||||
### The user's role (trusted apps)
|
||||
|
||||
[Trusted apps](#trusted-apps) also get the user's Anikeen ID role, `admin` (runs Anikeen) or
|
||||
`user` (constants on `Anikeen\Id\Enums\Role`); for other apps it is `null`. An app can make
|
||||
Anikeen ID admins its own admins this way instead of keeping a list of its own. Read it at sign-in
|
||||
and keep it current with the `user.updated` webhook (`role` in `changed`, see
|
||||
[User profiles](#user-profiles)):
|
||||
|
||||
```php
|
||||
use Anikeen\Id\Enums\Role;
|
||||
|
||||
$account = Socialite::driver('anikeen')->user();
|
||||
|
||||
$user->update(['role' => $account->role ?? Role::USER]);
|
||||
```
|
||||
|
||||
Access tokens expire. Get a new one with the refresh token (or send the user through the login
|
||||
again):
|
||||
|
||||
@@ -1207,8 +1223,14 @@ $profile?->emailVerified();
|
||||
$profile?->avatar();
|
||||
$profile?->version(); // compare with the version of user.updated
|
||||
$profile?->locale; // also username, created_at
|
||||
$profile?->role(); // Role::ADMIN or Role::USER (trusted apps only)
|
||||
$profile?->isAdmin();
|
||||
```
|
||||
|
||||
The `data.object` of `user.updated` is the same profile (`new \Anikeen\Id\Resources\User($event['data']['object'])`);
|
||||
for trusted apps it carries `role`, and a change of the role alone (`changed` is `["role"]`) is
|
||||
sent to them only.
|
||||
|
||||
It throws `ApiException` for 403 `client_not_allowed` (not a trusted app), 429
|
||||
`too_many_requests` and other failures. The profile carries no billing or account data; read those
|
||||
through their own endpoints.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Enums;
|
||||
|
||||
/**
|
||||
* A user's role on Anikeen ID. Only trusted apps see it: `role` of GET v1/user (the Socialite
|
||||
* user), of a user read by ID and of user.updated; other apps get none.
|
||||
*/
|
||||
class Role
|
||||
{
|
||||
/** Runs Anikeen (the Anikeen ID admin panel). */
|
||||
const ADMIN = 'admin';
|
||||
const USER = 'user';
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [self::ADMIN, self::USER];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Anikeen\Id\Resources;
|
||||
|
||||
use Anikeen\Id\Enums\Role;
|
||||
use DateTimeImmutable;
|
||||
use JsonSerializable;
|
||||
|
||||
@@ -27,6 +28,7 @@ use JsonSerializable;
|
||||
* @property ?string $created_at REST shapes
|
||||
* @property ?string $pending_email profile updates only: a new address waiting for confirmation
|
||||
* @property ?string $pending_email_expires_at profile updates only
|
||||
* @property ?string $role trusted apps only: Role::ADMIN or Role::USER
|
||||
*/
|
||||
class User implements JsonSerializable
|
||||
{
|
||||
@@ -97,6 +99,22 @@ class User implements JsonSerializable
|
||||
return (int) ($this->data->profile_version ?? $this->data->version ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Role::ADMIN or Role::USER; null for apps that aren't trusted (they never see it).
|
||||
*/
|
||||
public function role(): ?string
|
||||
{
|
||||
return $this->data->role ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An Anikeen ID admin (trusted apps only; false when the role isn't known).
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role() === Role::ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* A new email address waiting for the user's confirmation (profile updates only); `email`
|
||||
* stays the current address until then.
|
||||
|
||||
@@ -83,6 +83,8 @@ class Provider extends AbstractProvider implements ProviderInterface
|
||||
'name' => $user['name'],
|
||||
'email' => Arr::get($user, 'email'),
|
||||
'avatar' => $user['avatar'],
|
||||
// trusted apps only (Enums\Role), null for others: $socialiteUser->role
|
||||
'role' => Arr::get($user, 'role'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@ final class EventType
|
||||
* the changed fields (never their old values). An email address appears only once verified
|
||||
* (`email` is null while it isn't); `avatar` in `changed` means the picture behind the stable URL
|
||||
* changed. Keep the highest `version` and ignore older events. Opt-in (not through `*`); also
|
||||
* sent to the app that made the change.
|
||||
* sent to the app that made the change. Trusted apps also get `role` (Enums\Role) and `role` in
|
||||
* `changed`; a change of the role alone reaches only them.
|
||||
*/
|
||||
public const USER_UPDATED = 'user.updated';
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Anikeen\Id\Tests;
|
||||
|
||||
use Anikeen\Id\AnikeenId;
|
||||
use Anikeen\Id\Enums\Role;
|
||||
use Anikeen\Id\Resources\User;
|
||||
use Anikeen\Id\Socialite\Provider;
|
||||
use Illuminate\Http\Request;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* The user's role, seen by trusted apps only: at sign-in, read by ID and in user.updated.
|
||||
*/
|
||||
class UserRoleTest extends TestCase
|
||||
{
|
||||
private function socialiteUser(array $raw): \SocialiteProviders\Manager\OAuth2\User
|
||||
{
|
||||
$provider = new Provider(Request::create('/'), 'client', 'secret', 'https://app.test/callback');
|
||||
$map = new ReflectionMethod($provider, 'mapUserToObject');
|
||||
|
||||
return $map->invoke($provider, $raw);
|
||||
}
|
||||
|
||||
public function test_the_socialite_user_carries_the_role(): void
|
||||
{
|
||||
$raw = ['id' => '01JUSER', 'username' => 'ada', 'name' => 'Ada', 'email' => 'ada@example.com', 'avatar' => null];
|
||||
|
||||
$this->assertSame(Role::ADMIN, $this->socialiteUser([...$raw, 'role' => 'admin'])->role);
|
||||
// apps that aren't trusted never get it
|
||||
$this->assertNull($this->socialiteUser($raw)->role);
|
||||
}
|
||||
|
||||
public function test_a_user_read_by_id_has_the_role(): void
|
||||
{
|
||||
$this->respond(200, ['data' => ['id' => '01JUSER', 'name' => 'Ada', 'role' => 'admin']]);
|
||||
|
||||
$user = app(AnikeenId::class)->findUserById('01JUSER');
|
||||
|
||||
$this->assertSame(Role::ADMIN, $user->role());
|
||||
$this->assertTrue($user->isAdmin());
|
||||
}
|
||||
|
||||
public function test_the_webhook_object_has_it_for_trusted_apps_only(): void
|
||||
{
|
||||
$trusted = new User(['object' => 'user', 'id' => '01JUSER', 'version' => 3, 'role' => 'user']);
|
||||
$other = new User(['object' => 'user', 'id' => '01JUSER', 'version' => 3]);
|
||||
|
||||
$this->assertSame(Role::USER, $trusted->role());
|
||||
$this->assertFalse($trusted->isAdmin());
|
||||
$this->assertNull($other->role());
|
||||
$this->assertFalse($other->isAdmin());
|
||||
$this->assertSame([Role::ADMIN, Role::USER], Role::all());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user