[MTM] Initial token re-work #117

Merged
jonny merged 5 commits from develop into main 2026-08-14 11:42:18 +02:00
5 changed files with 159 additions and 0 deletions
Showing only changes of commit 9d6cf6c815 - Show all commits

Add admin page to list and revoke Micropub tokens

Gives a way to actually use the revocation capability built up over
the last few commits from the admin side, not just self-service via
the client. Lists client_id/scope/issue time per token (never the raw
token itself, since only its hash is stored) with a revoke button for
active ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014625MfkGZ7GVdbqKme4a8L
Jonny Barnes 2026-08-13 16:18:52 +01:00
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\MicropubToken;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class TokensController extends Controller
{
/**
* Show a list of issued Micropub tokens.
*/
public function index(): View
{
$tokens = MicropubToken::latest()->get();
return view('admin.tokens.index', compact('tokens'));
}
/**
* Revoke a Micropub token.
*/
public function revoke(MicropubToken $token): RedirectResponse
{
$token->revoke();
return redirect('/admin/tokens');
}
}

View file

@ -0,0 +1,27 @@
@extends('master')
@section('title')List Tokens « Admin CP « @stop
@section('content')
<h1>Micropub Tokens</h1>
@if($tokens->isEmpty())
<p>No tokens have been issued.</p>
@else
<ul>
@foreach($tokens as $token)
<li>
{{ $token->client_id }} scope: {{ $token->scope }} issued {{ $token->created_at->diffForHumans() }}
@if($token->isRevoked)
revoked {{ $token->revoked_at->diffForHumans() }}
@else
<form action="/admin/tokens/{{ $token->id }}/revoke" method="post" style="display:inline">
{{ csrf_field() }}
{{ method_field('PUT') }}
<button type="submit" name="revoke">Revoke</button>
</form>
@endif
</li>
@endforeach
</ul>
@endif
@stop

View file

@ -47,6 +47,11 @@
or <a href="/admin/syndication">edit</a> them. or <a href="/admin/syndication">edit</a> them.
</p> </p>
<h2>Tokens</h2>
<p>
View and <a href="/admin/tokens">revoke</a> issued Micropub tokens.
</p>
<h2>Bio</h2> <h2>Bio</h2>
<p> <p>
Edit your <a href="/admin/bio">bio</a>. Edit your <a href="/admin/bio">bio</a>.

View file

@ -10,6 +10,7 @@ use App\Http\Controllers\Admin\NotesController as AdminNotesController;
use App\Http\Controllers\Admin\PasskeysController; use App\Http\Controllers\Admin\PasskeysController;
use App\Http\Controllers\Admin\PlacesController as AdminPlacesController; use App\Http\Controllers\Admin\PlacesController as AdminPlacesController;
use App\Http\Controllers\Admin\SyndicationTargetsController; use App\Http\Controllers\Admin\SyndicationTargetsController;
use App\Http\Controllers\Admin\TokensController;
use App\Http\Controllers\ArticlesController; use App\Http\Controllers\ArticlesController;
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\BookmarksController; use App\Http\Controllers\BookmarksController;
@ -147,6 +148,12 @@ Route::middleware(MyAuthMiddleware::class)->prefix('admin')->group(function () {
Route::delete('/{clientId}', [ClientsController::class, 'destroy']); Route::delete('/{clientId}', [ClientsController::class, 'destroy']);
}); });
// Micropub Tokens
Route::prefix('tokens')->group(function () {
Route::get('/', [TokensController::class, 'index']);
Route::put('/{token}/revoke', [TokensController::class, 'revoke']);
});
// Bio // Bio
Route::prefix('bio')->group(function () { Route::prefix('bio')->group(function () {
Route::get('/', [BioController::class, 'show'])->name('admin.bio.show'); Route::get('/', [BioController::class, 'show'])->name('admin.bio.show');

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Admin;
use App\Models\MicropubToken;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class TokensTest extends TestCase
{
use RefreshDatabase;
#[Test]
public function index_requires_authentication(): void
{
$response = $this->get('/admin/tokens');
$response->assertRedirect();
}
#[Test]
public function index_lists_issued_tokens(): void
{
$user = User::factory()->make();
$token = MicropubToken::create([
'token_hash' => hash('sha256', 'a-token'),
'client_id' => 'https://quill.p3k.io',
'me' => 'https://jonnybarnes.uk',
'scope' => 'create update',
]);
$response = $this->actingAs($user)->get('/admin/tokens');
$response->assertOk();
$response->assertSeeText($token->client_id);
}
#[Test]
public function revoke_requires_authentication(): void
{
$token = MicropubToken::create([
'token_hash' => hash('sha256', 'a-token'),
'client_id' => 'https://quill.p3k.io',
'me' => 'https://jonnybarnes.uk',
'scope' => 'create',
]);
$response = $this->put("/admin/tokens/{$token->id}/revoke");
$response->assertRedirect();
$this->assertFalse($token->fresh()->isRevoked);
}
#[Test]
public function revoke_marks_the_token_as_revoked(): void
{
$user = User::factory()->make();
$token = MicropubToken::create([
'token_hash' => hash('sha256', 'a-token'),
'client_id' => 'https://quill.p3k.io',
'me' => 'https://jonnybarnes.uk',
'scope' => 'create',
]);
$this->actingAs($user)->put("/admin/tokens/{$token->id}/revoke");
$this->assertTrue($token->fresh()->isRevoked);
}
#[Test]
public function revoke_redirects_to_index(): void
{
$user = User::factory()->make();
$token = MicropubToken::create([
'token_hash' => hash('sha256', 'a-token'),
'client_id' => 'https://quill.p3k.io',
'me' => 'https://jonnybarnes.uk',
'scope' => 'create',
]);
$response = $this->actingAs($user)->put("/admin/tokens/{$token->id}/revoke");
$response->assertRedirect('/admin/tokens');
}
}