Look up users by email and document invitations

This commit is contained in:
envoyr committed 2026-11-06 10:26:51 +00:00
1 parent 27e868720f
commit 7813e8fbaa
2 files changed
+95 -10

No files matched your search

+48 -10
View File
@@ -5,7 +5,9 @@ namespace Anikeen\Id\Concerns;
use Anikeen\Id\ApiOperations\Get; use Anikeen\Id\ApiOperations\Get;
use Anikeen\Id\ApiOperations\Post; use Anikeen\Id\ApiOperations\Post;
use Anikeen\Id\Exceptions\ApiException;
use Anikeen\Id\Result; use Anikeen\Id\Result;
use Anikeen\Id\Support\AppRequest;
use Throwable; use Throwable;
trait ManagesUsers trait ManagesUsers
@@ -24,20 +26,32 @@ trait ManagesUsers
} }
/** /**
* Creates a new user on behalf of the current user. * Finds the account of an email address or creates it (trusted apps, app token). The email
* address is matched regardless of case; a new account stores it in lower case, unverified.
* An existing account is returned unchanged (only a missing name is filled in).
* *
* @param array{ * @param array{
* first_name: null|string, * name?: null|string,
* last_name: null|string, * first_name?: null|string,
* username: null|string, * last_name?: null|string,
* username?: null|string,
* email: string, * email: string,
* password: null|string * password?: null|string,
* password_confirmation?: null|string,
* send_invitation?: bool
* } $attributes The user data * } $attributes The user data
* - first_name: The first name (optional) * - name: The display name (optional)
* - last_name: The last name (optional) * - first_name: The first name (optional, joined with last_name into name)
* - username: The username (optional) * - last_name: The last name (optional, joined with first_name into name)
* - email: The email (required) * - username: The username (optional)
* - password: The password (optional, can be reset by the user if not provided) * - email: The email (required)
* - password: The password (optional, can be reset by the user if not provided)
* - password_confirmation: Defaults to password
* - send_invitation: Send the user an invitation mail with a link to set a password
* (optional, only when this call created the account without a
* password, never for an existing account)
* @return Result `data` is the user plus `created` (this call created the account) and
* `invitation_sent` (the invitation mail was sent); 201 when created, 200 when it existed
* @throws Throwable * @throws Throwable
*/ */
public function createUser(array $attributes): Result public function createUser(array $attributes): Result
@@ -57,6 +71,30 @@ trait ManagesUsers
return $this->asApp()->post('v1/admin/users', $attributes); return $this->asApp()->post('v1/admin/users', $attributes);
} }
/**
* Looks up the account of an email address without creating or changing anything (trusted
* apps, app token). The email address is matched regardless of case.
*
* `data` is `object{id: string, name: ?string, email: string, email_verified: bool,
* has_billing_address: bool}`.
*
* @return Result|null null when no account uses the email address
* @throws ApiException when the request fails for another reason
*/
public function findUserByEmail(string $email): ?Result
{
try {
return AppRequest::send('POST', 'v1/admin/users/lookup', ['email' => $email]);
} catch (ApiException $e) {
// only Anikeen ID's own answer means "no account", not e.g. an unknown route
if ($e->isNotFound() && $e->error === 'user_not_found') {
return null;
}
throw $e;
}
}
/** /**
* Find the user of a social login or create it (trusted apps, app token). * Find the user of a social login or create it (trusted apps, app token).
* *
+47
View File
@@ -4,6 +4,7 @@ namespace Anikeen\Id\Tests;
use Anikeen\Id\AnikeenId; use Anikeen\Id\AnikeenId;
use Anikeen\Id\Billable; use Anikeen\Id\Billable;
use Anikeen\Id\Exceptions\ApiException;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/** /**
@@ -90,6 +91,52 @@ class EndpointsTest extends TestCase
$this->assertSame(['email' => 'erika@example.com', 'password' => 'secret-123', 'name' => 'Erika Mustermann', 'password_confirmation' => 'secret-123'], $this->sentJson(0)); $this->assertSame(['email' => 'erika@example.com', 'password' => 'secret-123', 'name' => 'Erika Mustermann', 'password_confirmation' => 'secret-123'], $this->sentJson(0));
} }
public function test_create_user_can_send_an_invitation(): void
{
$this->respond(201, ['data' => ['id' => '01JNEW', 'email' => 'erika@example.com', 'created' => true, 'invitation_sent' => true]]);
$result = app(AnikeenId::class)->createUser(['name' => 'Erika Mustermann', 'email' => 'erika@example.com', 'send_invitation' => true]);
$this->assertSame('POST /api/v1/admin/users Bearer app-token', $this->sentCall(0));
$this->assertSame(['name' => 'Erika Mustermann', 'email' => 'erika@example.com', 'send_invitation' => true], $this->sentJson(0));
$this->assertTrue($result->data->created);
$this->assertTrue($result->data->invitation_sent);
}
public function test_find_user_by_email_uses_the_lookup_endpoint(): void
{
$this->respond(200, ['data' => ['id' => '01JUSER', 'name' => 'Erika', 'email' => 'erika@example.com', 'email_verified' => true, 'has_billing_address' => false]]);
$result = app(AnikeenId::class)->findUserByEmail('Erika@Example.com');
$this->assertSame('POST /api/v1/admin/users/lookup Bearer app-token', $this->sentCall(0));
$this->assertSame(['email' => 'Erika@Example.com'], $this->sentJson(0));
$this->assertSame('01JUSER', $result->data->id);
$this->assertFalse($result->data->has_billing_address);
}
public function test_find_user_by_email_returns_null_without_an_account(): void
{
$this->respond(404, ['message' => 'User not found.', 'error' => 'user_not_found']);
$this->assertNull(app(AnikeenId::class)->findUserByEmail('nobody@example.com'));
}
public function test_find_user_by_email_throws_other_failures(): void
{
$this->respond(404, ['message' => 'Not found.']); // e.g. an Anikeen ID without the lookup
$this->respond(500, ['message' => 'Server Error']);
foreach ([404, 500] as $status) {
try {
app(AnikeenId::class)->findUserByEmail('erika@example.com');
$this->fail('Expected an ApiException.');
} catch (ApiException $e) {
$this->assertSame($status, $e->status);
}
}
}
public function test_email_check_and_socialite_users(): void public function test_email_check_and_socialite_users(): void
{ {
$this->respond(200, ['data' => ['exists' => true]]); $this->respond(200, ['data' => ['exists' => true]]);