Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions app/Commands/LoginCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,26 @@ private function loginWithPersonalAccessToken(CredentialStore $credentials, Flar
}

try {
$response = Http::withToken($token)->get("{$urlResolver->getApiBaseUrl()}/me");
// Without acceptJson, an invalid token gets redirected to the HTML
// login page and reads as a successful response.
$response = Http::withToken($token)->acceptJson()->get("{$urlResolver->getApiBaseUrl()}/me");
} catch (ConnectionException) {
$this->error('Could not connect to Flare. Please check your internet connection.');

return self::FAILURE;
}

if (! $response->successful()) {
$email = $response->json('email');

if (! $response->successful() || ! is_string($email) || $email === '') {
$this->error('Invalid API token.');

return self::FAILURE;
}

$credentials->setToken($token);

return $this->reportSuccess($response->json('email', 'unknown'), $urlResolver);
return $this->reportSuccess($email, $urlResolver);
}

private function loginWithBrowser(
Expand Down Expand Up @@ -137,7 +141,7 @@ private function loginWithBrowser(
private function fetchEmail(TokenRecord $record, FlareUrlResolver $urlResolver): ?string
{
try {
$response = Http::withToken($record->accessToken)->get("{$urlResolver->getApiBaseUrl()}/me");
$response = Http::withToken($record->accessToken)->acceptJson()->get("{$urlResolver->getApiBaseUrl()}/me");
} catch (ConnectionException) {
return null;
}
Expand Down
32 changes: 32 additions & 0 deletions tests/Feature/LoginCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ function loginRecord(array $overrides = []): TokenRecord
expect($this->store->getToken())->toBe('valid-token-123');
});

it('rejects a --token when /me responds with a non-JSON page', function () {
// An invalid token used to get redirected to the HTML login page, which the
// HTTP client follows to a 200 — and login reported success as "unknown".
Http::fake([
'flareapp.io/api/me' => Http::response('<!DOCTYPE html><html><body>Log in</body></html>', 200, [
'Content-Type' => 'text/html',
]),
]);

$this->artisan('login --token')
->expectsQuestion('Enter your Flare API token', 'invalid-token')
->expectsOutput('Invalid API token.')
->assertExitCode(1);

expect($this->store->getToken())->toBeNull();
});

it('sends an Accept json header when validating a --token', function () {
Http::fake([
'flareapp.io/api/me' => Http::response(['email' => 'alex@spatie.be']),
]);

$this->artisan('login --token')
->expectsQuestion('Enter your Flare API token', 'valid-token-123')
->assertExitCode(0);

Http::assertSent(function ($request) {
return $request->url() === 'https://flareapp.io/api/me'
&& $request->hasHeader('Accept', 'application/json');
});
});

it('shows error and does not store token on invalid --token input', function () {
Http::fake([
'flareapp.io/api/me' => Http::response(['error' => 'Unauthorized'], 401),
Expand Down