Merge pull request '[MTM] Add manual Micropub token generation for non-PKCE clients' (#133) from develop into main
Reviewed-on: #133
This commit is contained in:
commit
89c083aebe
12 changed files with 190 additions and 2 deletions
|
|
@ -6,7 +6,9 @@ namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\MicropubToken;
|
use App\Models\MicropubToken;
|
||||||
|
use App\Services\TokenService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class TokensController extends Controller
|
class TokensController extends Controller
|
||||||
|
|
@ -21,6 +23,36 @@ class TokensController extends Controller
|
||||||
return view('admin.tokens.index', compact('tokens'));
|
return view('admin.tokens.index', compact('tokens'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form to manually generate a new Micropub token.
|
||||||
|
*
|
||||||
|
* This is for clients (e.g. iA Writer) that don't support the IndieAuth
|
||||||
|
* PKCE flow and instead expect to be given a token directly.
|
||||||
|
*/
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('admin.tokens.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually generate a new Micropub token.
|
||||||
|
*/
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'client_id' => 'required|string',
|
||||||
|
'scope' => 'required|array|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = resolve(TokenService::class)->getNewToken([
|
||||||
|
'me' => config('app.url'),
|
||||||
|
'client_id' => $validated['client_id'],
|
||||||
|
'scope' => implode(' ', $validated['scope']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect('/admin/tokens')->with('new_token', $token);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke a Micropub token.
|
* Revoke a Micropub token.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -134,4 +134,30 @@
|
||||||
.token-list button.revoke:hover {
|
.token-list button.revoke:hover {
|
||||||
background: light-dark(oklch(80% 0.2 25deg), oklch(45% 0.18 25deg));
|
background: light-dark(oklch(80% 0.2 25deg), oklch(45% 0.18 25deg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.token-reveal {
|
||||||
|
margin-block-end: 1em;
|
||||||
|
padding: 1em 1.2em;
|
||||||
|
border: 1px solid var(--clr-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: light-dark(
|
||||||
|
oklch(96% 0.08 145deg),
|
||||||
|
oklch(28% 0.08 145deg)
|
||||||
|
);
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 0.5em 0.7em;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--clr-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-checkboxes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
resources/views/admin/tokens/create.blade.php
Normal file
52
resources/views/admin/tokens/create.blade.php
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
@extends('master')
|
||||||
|
|
||||||
|
@section('title')New Token « Admin CP « @stop
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h1>Generate a new token</h1>
|
||||||
|
<p>Use this for clients that can't complete the IndieAuth authorization flow (e.g. they don't support PKCE) and instead let you paste in a token directly.</p>
|
||||||
|
|
||||||
|
<form action="/admin/tokens" method="post" accept-charset="utf-8" class="admin-form form">
|
||||||
|
{{ csrf_field() }}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="client_id">Client</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="client_id"
|
||||||
|
id="client_id"
|
||||||
|
value="{{ old('client_id') }}"
|
||||||
|
placeholder="https://ia.net/writer"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scope-checkboxes">
|
||||||
|
<span>Scope</span>
|
||||||
|
<label for="scope_create">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="scope[]"
|
||||||
|
id="scope_create"
|
||||||
|
value="create"
|
||||||
|
@checked(in_array('create', old('scope', []), true))
|
||||||
|
>
|
||||||
|
create
|
||||||
|
</label>
|
||||||
|
<label for="scope_update">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="scope[]"
|
||||||
|
id="scope_update"
|
||||||
|
value="update"
|
||||||
|
@checked(in_array('update', old('scope', []), true))
|
||||||
|
>
|
||||||
|
update
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="save">Generate token</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@stop
|
||||||
|
|
@ -4,6 +4,15 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<h1>Micropub Tokens</h1>
|
<h1>Micropub Tokens</h1>
|
||||||
|
<p><a href="/admin/tokens/create">Generate new token</a></p>
|
||||||
|
|
||||||
|
@if(session('new_token'))
|
||||||
|
<div class="token-reveal">
|
||||||
|
<p>Here's your new token. <strong>Copy it now</strong> — it won't be shown again.</p>
|
||||||
|
<input type="text" readonly value="{{ session('new_token') }}" onclick="this.select()">
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if($tokens->isEmpty())
|
@if($tokens->isEmpty())
|
||||||
<p>No tokens have been issued.</p>
|
<p>No tokens have been issued.</p>
|
||||||
@else
|
@else
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,8 @@ Route::middleware(MyAuthMiddleware::class)->prefix('admin')->group(function () {
|
||||||
// Micropub Tokens
|
// Micropub Tokens
|
||||||
Route::prefix('tokens')->group(function () {
|
Route::prefix('tokens')->group(function () {
|
||||||
Route::get('/', [TokensController::class, 'index']);
|
Route::get('/', [TokensController::class, 'index']);
|
||||||
|
Route::get('/create', [TokensController::class, 'create']);
|
||||||
|
Route::post('/', [TokensController::class, 'store']);
|
||||||
Route::put('/{token}/revoke', [TokensController::class, 'revoke']);
|
Route::put('/{token}/revoke', [TokensController::class, 'revoke']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,73 @@ class TokensTest extends TestCase
|
||||||
$response->assertSeeText($token->client_id);
|
$response->assertSeeText($token->client_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function create_requires_authentication(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/admin/tokens/create');
|
||||||
|
$response->assertRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function create_shows_form(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get('/admin/tokens/create');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('name="client_id"', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function store_requires_authentication(): void
|
||||||
|
{
|
||||||
|
$response = $this->post('/admin/tokens', [
|
||||||
|
'client_id' => 'https://ia.net/writer',
|
||||||
|
'scope' => ['create'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
$this->assertDatabaseCount('micropub_tokens', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function store_creates_a_new_token_and_redirects_with_it_flashed(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->post('/admin/tokens', [
|
||||||
|
'client_id' => 'https://ia.net/writer',
|
||||||
|
'scope' => ['create', 'update'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect('/admin/tokens');
|
||||||
|
$response->assertSessionHas('new_token');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('micropub_tokens', [
|
||||||
|
'client_id' => 'https://ia.net/writer',
|
||||||
|
'scope' => 'create update',
|
||||||
|
'me' => config('app.url'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $response->getSession()->get('new_token');
|
||||||
|
$this->assertNotNull(MicropubToken::findActive($token));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function store_requires_at_least_one_scope(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->make();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->post('/admin/tokens', [
|
||||||
|
'client_id' => 'https://ia.net/writer',
|
||||||
|
'scope' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSessionHasErrors('scope');
|
||||||
|
$this->assertDatabaseCount('micropub_tokens', 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function revoke_requires_authentication(): void
|
public function revoke_requires_authentication(): void
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue