Merge pull request #1103 from jonnybarnes/develop

MTM Passkey Support
This commit is contained in:
Jonny Barnes 2023-10-27 20:17:14 +00:00 committed by GitHub
commit 335acb130e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 4696 additions and 2415 deletions

View file

@ -8,6 +8,9 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,css}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View file

@ -84,6 +84,7 @@ SESSION_SECURE_COOKIE=true
LOG_SLACK_WEBHOOK_URL=
FLARE_KEY=
IGNITION_OPEN_AI_KEY=
FONT_LINK=

View file

@ -1,5 +1,6 @@
parserOptions:
sourceType: 'module'
ecmaVersion: 'latest'
extends: 'eslint:recommended'
env:
browser: true
@ -9,7 +10,7 @@ ignorePatterns:
rules:
indent:
- error
- 4
- 2
linebreak-style:
- error
- unix
@ -24,3 +25,14 @@ rules:
- allow:
- warn
- error
no-await-in-loop:
- error
no-promise-executor-return:
- error
require-atomic-updates:
- error
max-nested-callbacks:
- error
- 3
prefer-promise-reject-errors:
- error

View file

@ -2,6 +2,8 @@ name: Deploy
on:
workflow_dispatch:
release:
types: [published]
jobs:
deploy:

View file

@ -33,7 +33,7 @@ jobs:
- name: Setup PHP with pecl extensions
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.2'
extensions: phpredis,imagick
- name: Copy .env

View file

@ -16,7 +16,7 @@ jobs:
- name: Setup PHP with pecl extensions
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.2'
- name: Get Composer Cache Directory
id: composer-cache

View file

@ -1,7 +1,3 @@
{
"extends": ["stylelint-config-standard"],
"rules": {
"indentation": 4,
"import-notation": "string"
}
"extends": ["stylelint-config-standard"]
}

View file

@ -8,6 +8,8 @@ use Illuminate\Support\Facades\DB;
/**
* @codeCoverageIgnore
*
* @psalm-suppress UnusedClass
*/
class MigratePlaceDataFromPostgis extends Command
{

View file

@ -9,6 +9,9 @@ use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\FileSystem\FileSystem;
/**
* @psalm-suppress UnusedClass
*/
class ParseCachedWebMentions extends Command
{
/**

View file

@ -8,6 +8,9 @@ use App\Jobs\DownloadWebMention;
use App\Models\WebMention;
use Illuminate\Console\Command;
/**
* @psalm-suppress UnusedClass
*/
class ReDownloadWebMentions extends Command
{
/**

View file

@ -7,23 +7,12 @@ use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
$this->reportable(function (Throwable $_e) {
//
});
}

View file

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
class TwitterContentException extends \Exception
{
}

View file

@ -9,6 +9,9 @@ use App\Models\Article;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ArticlesController extends Controller
{
public function index(): View

View file

@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class BioController extends Controller
{
public function show(): View

View file

@ -7,9 +7,11 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\MicropubClient;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ClientsController extends Controller
{
/**

View file

@ -9,10 +9,12 @@ use App\Models\Contact;
use GuzzleHttp\Client;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ContactsController extends Controller
{
/**

View file

@ -7,6 +7,9 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class HomeController extends Controller
{
/**

View file

@ -10,6 +10,9 @@ use App\Models\Like;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class LikesController extends Controller
{
/**

View file

@ -11,6 +11,9 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class NotesController extends Controller
{
/**

View file

@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Passkey;
use App\Models\User;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\EdDSA\Ed25519;
use Cose\Algorithm\Signature\RSA\RS256;
use Cose\Algorithms;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Throwable;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\Exception\WebauthnException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialUserEntity;
/**
* @psalm-suppress UnusedClass
*/
class PasskeysController extends Controller
{
public function index(): View
{
/** @var User $user */
$user = auth()->user();
$passkeys = $user->passkey;
return view('admin.passkeys.index', compact('passkeys'));
}
public function getCreateOptions(): JsonResponse
{
/** @var User $user */
$user = auth()->user();
// RP Entity i.e. the application
$rpEntity = PublicKeyCredentialRpEntity::create(
config('app.name'),
config('url.longurl'),
);
// User Entity
$userEntity = PublicKeyCredentialUserEntity::create(
$user->name,
(string) $user->id,
$user->name,
);
// Challenge
$challenge = random_bytes(16);
// List of supported public key parameters
$pubKeyCredParams = collect([
Algorithms::COSE_ALGORITHM_EDDSA,
Algorithms::COSE_ALGORITHM_ES256,
Algorithms::COSE_ALGORITHM_RS256,
])->map(
fn ($algorithm) => PublicKeyCredentialParameters::create('public-key', $algorithm)
)->toArray();
$authenticatorSelectionCriteria = AuthenticatorSelectionCriteria::create(
userVerification: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED,
residentKey: AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED,
requireResidentKey: true,
);
$options = PublicKeyCredentialCreationOptions::create(
$rpEntity,
$userEntity,
$challenge,
$pubKeyCredParams,
authenticatorSelection: $authenticatorSelectionCriteria,
attestation: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
);
$options = json_encode($options, JSON_THROW_ON_ERROR);
session(['create_options' => $options]);
return JsonResponse::fromJsonString($options);
}
public function create(Request $request): JsonResponse
{
/** @var User $user */
$user = auth()->user();
$publicKeyCredentialCreationOptionsData = session('create_options');
if (empty($publicKeyCredentialCreationOptionsData)) {
throw new WebAuthnException('No public key credential request options found');
}
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromString($publicKeyCredentialCreationOptionsData);
// Unset session data to mitigate replay attacks
session()->forget('create_options');
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
throw new WebAuthnException('Invalid response type');
}
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
attestationStatementSupportManager: $attestationStatementSupportManager,
publicKeyCredentialSourceRepository: null,
tokenBindingHandler: null,
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
);
$securedRelyingPartyId = [];
if (App::environment('local', 'development')) {
$securedRelyingPartyId = [config('url.longurl')];
}
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
authenticatorAttestationResponse: $publicKeyCredential->response,
publicKeyCredentialCreationOptions: $publicKeyCredentialCreationOptions,
request: config('url.longurl'),
securedRelyingPartyId: $securedRelyingPartyId,
);
$user->passkey()->create([
'passkey_id' => Base64UrlSafe::encodeUnpadded($publicKeyCredentialSource->publicKeyCredentialId),
'passkey' => json_encode($publicKeyCredentialSource, JSON_THROW_ON_ERROR),
]);
return response()->json([
'success' => true,
'message' => 'Passkey created successfully',
]);
}
public function getRequestOptions(): JsonResponse
{
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::create(
challenge: random_bytes(16),
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED
);
$publicKeyCredentialRequestOptions = json_encode($publicKeyCredentialRequestOptions, JSON_THROW_ON_ERROR);
session(['request_options' => $publicKeyCredentialRequestOptions]);
return JsonResponse::fromJsonString($publicKeyCredentialRequestOptions);
}
public function login(Request $request): JsonResponse
{
$requestOptions = session('request_options');
session()->forget('request_options');
if (empty($requestOptions)) {
return response()->json([
'success' => false,
'message' => 'No request options found',
], 400);
}
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($requestOptions);
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
return response()->json([
'success' => false,
'message' => 'Invalid response type',
], 400);
}
$passkey = Passkey::firstWhere('passkey_id', $publicKeyCredential->id);
if (! $passkey) {
return response()->json([
'success' => false,
'message' => 'Passkey not found',
], 404);
}
$credential = PublicKeyCredentialSource::createFromArray(json_decode($passkey->passkey, true, 512, JSON_THROW_ON_ERROR));
$algorithmManager = Manager::create();
$algorithmManager->add(new Ed25519());
$algorithmManager->add(new ES256());
$algorithmManager->add(new RS256());
$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
publicKeyCredentialSourceRepository: null,
tokenBindingHandler: null,
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
algorithmManager: $algorithmManager,
);
$securedRelyingPartyId = [];
if (App::environment('local', 'development')) {
$securedRelyingPartyId = [config('url.longurl')];
}
try {
$authenticatorAssertionResponseValidator->check(
credentialId: $credential,
authenticatorAssertionResponse: $publicKeyCredential->response,
publicKeyCredentialRequestOptions: $publicKeyCredentialRequestOptions,
request: config('url.longurl'),
userHandle: null,
securedRelyingPartyId: $securedRelyingPartyId,
);
} catch (Throwable) {
return response()->json([
'success' => false,
'message' => 'Passkey could not be verified',
], 500);
}
$user = User::find($passkey->user_id);
Auth::login($user);
return response()->json([
'success' => true,
'message' => 'Passkey verified successfully',
]);
}
}

View file

@ -10,6 +10,9 @@ use App\Services\PlaceService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class PlacesController extends Controller
{
protected PlaceService $placeService;

View file

@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class SyndicationTargetsController extends Controller
{
/**

View file

@ -10,6 +10,9 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use Jonnybarnes\IndieWeb\Numbers;
/**
* @psalm-suppress UnusedClass
*/
class ArticlesController extends Controller
{
/**

View file

@ -9,6 +9,9 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class AuthController extends Controller
{
/**
@ -31,7 +34,7 @@ class AuthController extends Controller
$credentials = $request->only('name', 'password');
if (Auth::attempt($credentials, true)) {
return redirect()->intended('/');
return redirect()->intended('/admin');
}
return redirect()->route('login');

View file

@ -7,6 +7,9 @@ namespace App\Http\Controllers;
use App\Models\Bookmark;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class BookmarksController extends Controller
{
/**

View file

@ -8,6 +8,9 @@ use App\Models\Contact;
use Illuminate\Filesystem\Filesystem;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class ContactsController extends Controller
{
/**

View file

@ -9,6 +9,9 @@ use App\Models\Note;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
/**
* @psalm-suppress UnusedClass
*/
class FeedsController extends Controller
{
/**

View file

@ -7,16 +7,18 @@ use App\Models\Bio;
use App\Models\Bookmark;
use App\Models\Like;
use App\Models\Note;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class FrontPageController extends Controller
{
/**
* Show all the recent activity.
*/
public function index(Request $request): Response|View
public function index(): Response|View
{
$notes = Note::latest()->with(['media', 'client', 'place'])->get();
$articles = Article::latest()->get();

View file

@ -7,6 +7,9 @@ namespace App\Http\Controllers;
use App\Models\Like;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class LikesController extends Controller
{
/**

View file

@ -19,6 +19,9 @@ use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
/**
* @psalm-suppress UnusedClass
*/
class MicropubController extends Controller
{
protected TokenService $tokenService;

View file

@ -24,6 +24,9 @@ use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Ramsey\Uuid\Uuid;
/**
* @psalm-suppress UnusedClass
*/
class MicropubMediaController extends Controller
{
protected TokenService $tokenService;

View file

@ -8,19 +8,21 @@ use App\Models\Note;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
use Jonnybarnes\IndieWeb\Numbers;
// Need to sort out Twitter and webmentions!
/**
* @todo Need to sort out Twitter and webmentions!
*
* @psalm-suppress UnusedClass
*/
class NotesController extends Controller
{
/**
* Show all the notes. This is also the homepage.
*/
public function index(Request $request): View|Response
public function index(): View|Response
{
$notes = Note::latest()
->with('place', 'media', 'client')

View file

@ -7,6 +7,9 @@ namespace App\Http\Controllers;
use App\Models\Place;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class PlacesController extends Controller
{
/**

View file

@ -6,6 +6,9 @@ use App\Models\Note;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* @psalm-suppress UnusedClass
*/
class SearchController extends Controller
{
public function search(Request $request): View

View file

@ -6,6 +6,9 @@ namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
/**
* @psalm-suppress UnusedClass
*/
class ShortURLsController extends Controller
{
/*

View file

@ -12,6 +12,9 @@ use Illuminate\Http\Request;
use IndieAuth\Client;
use JsonException;
/**
* @psalm-suppress UnusedClass
*/
class TokenEndpointController extends Controller
{
/**

View file

@ -12,6 +12,9 @@ use Illuminate\Http\Response;
use Illuminate\View\View;
use Jonnybarnes\IndieWeb\Numbers;
/**
* @psalm-suppress UnusedClass
*/
class WebMentionsController extends Controller
{
/**

View file

@ -11,6 +11,8 @@ class CSPHeader
{
/**
* Handle an incoming request.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function handle(Request $request, Closure $next): Response
{

View file

@ -10,6 +10,8 @@ class CorsHeaders
{
/**
* Handle an incoming request.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function handle(Request $request, Closure $next): Response
{

View file

@ -10,6 +10,8 @@ class LinkHeadersMiddleware
{
/**
* Handle an incoming request.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function handle(Request $request, Closure $next): Response
{

View file

@ -14,6 +14,8 @@ class LocalhostSessionMiddleware
* Whilst we are developing locally, automatically log in as
* `['me' => config('app.url')]` as I cant manually log in as
* a .localhost domain.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function handle(Request $request, Closure $next): Response
{

View file

@ -13,6 +13,8 @@ class MyAuthMiddleware
{
/**
* Check the user is logged in.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function handle(Request $request, Closure $next): Response
{

View file

@ -10,6 +10,8 @@ class ValidateSignature extends Middleware
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*
* @psalm-suppress PossiblyUnusedProperty
*/
protected $except = [
// 'fbclid',

View file

@ -12,6 +12,8 @@ class VerifyMicropubToken
{
/**
* Handle an incoming request.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function handle(Request $request, Closure $next): Response
{

View file

@ -71,7 +71,6 @@ class SendWebMentions implements ShouldQueue
$endpoint = null;
/** @var Client $guzzle */
$guzzle = resolve(Client::class);
$response = $guzzle->get($url);
//check HTTP Headers for webmention endpoint

25
app/Models/Passkey.php Normal file
View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Passkey extends Model
{
use HasFactory;
/** @inerhitDoc */
protected $fillable = [
'passkey_id',
'passkey',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -24,4 +25,9 @@ class User extends Authenticatable
'password',
'remember_token',
];
public function passkey(): HasMany
{
return $this->hasMany(Passkey::class);
}
}

View file

@ -9,10 +9,15 @@ use App\Models\Tag;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
/**
* @todo Do we need psalm-suppress for these observer methods?
*/
class NoteObserver
{
/**
* Listen to the Note created event.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function created(Note $note): void
{
@ -35,6 +40,8 @@ class NoteObserver
/**
* Listen to the Note updated event.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function updated(Note $note): void
{
@ -59,6 +66,8 @@ class NoteObserver
/**
* Listen to the Note deleting event.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function deleting(Note $note): void
{

View file

@ -5,7 +5,6 @@ namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{

View file

@ -6,6 +6,9 @@ use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;
/**
* @psalm-suppress UnusedClass
*/
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
/**

View file

@ -8,7 +8,7 @@ use App\Models\Article;
class ArticleService extends Service
{
public function create(array $request, ?string $client = null): Article
public function create(array $request, string $client = null): Article
{
return Article::create([
'title' => $this->getDataByKey($request, 'name'),

View file

@ -18,7 +18,7 @@ class BookmarkService extends Service
/**
* Create a new Bookmark.
*/
public function create(array $request, ?string $client = null): Bookmark
public function create(array $request, string $client = null): Bookmark
{
if (Arr::get($request, 'properties.bookmark-of.0')) {
//micropub request

View file

@ -13,7 +13,7 @@ class LikeService extends Service
/**
* Create a new Like.
*/
public function create(array $request, ?string $client = null): Like
public function create(array $request, string $client = null): Like
{
if (Arr::get($request, 'properties.like-of.0')) {
//micropub request

View file

@ -15,7 +15,7 @@ class HEntryService
/**
* Create the relevant model from some h-entry data.
*/
public function process(array $request, ?string $client = null): ?string
public function process(array $request, string $client = null): ?string
{
if (Arr::get($request, 'properties.like-of') || Arr::get($request, 'like-of')) {
return resolve(LikeService::class)->create($request)->longurl;

View file

@ -18,7 +18,7 @@ class NoteService extends Service
/**
* Create a new note.
*/
public function create(array $request, ?string $client = null): Note
public function create(array $request, string $client = null): Note
{
$note = Note::create(
[

View file

@ -9,7 +9,7 @@ use Illuminate\Support\Arr;
abstract class Service
{
abstract public function create(array $request, ?string $client = null): Model;
abstract public function create(array $request, string $client = null): Model;
protected function getDataByKey(array $request, string $key): ?string
{

View file

@ -5,10 +5,11 @@
"keywords": ["laravel", "framework", "indieweb"],
"license": "CC0-1.0",
"require": {
"php": "^8.1",
"php": "^8.2",
"ext-dom": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-pgsql": "*",
"cviebrock/eloquent-sluggable": "^10.0",
"guzzlehttp/guzzle": "^7.2",
"indieauth/client": "^1.1",
@ -27,7 +28,8 @@
"mf2/mf2": "~0.3",
"spatie/commonmark-highlighter": "^3.0",
"spatie/laravel-ignition": "^2.1",
"symfony/html-sanitizer": "^6.1"
"symfony/html-sanitizer": "^6.1",
"web-auth/webauthn-lib": "^4.7"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.0",
@ -39,8 +41,10 @@
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"openai-php/client": "^0.7.1",
"phpunit/php-code-coverage": "^10.0",
"phpunit/phpunit": "^10.1",
"psalm/plugin-laravel": "^2.8",
"spatie/laravel-ray": "^1.12",
"vimeo/psalm": "^5.0"
},

2889
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
*/
class ArticleFactory extends Factory

View file

@ -5,6 +5,8 @@ namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bio>
*/
class BioFactory extends Factory

View file

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Bookmark>
*/
class BookmarkFactory extends Factory

View file

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Contact>
*/
class ContactFactory extends Factory

View file

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Like>
*/
class LikeFactory extends Factory

View file

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Media>
*/
class MediaFactory extends Factory

View file

@ -6,6 +6,8 @@ use App\Models\MicropubClient;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\MicropubClient>
*/
class MicropubClientFactory extends Factory

View file

@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Note>
*/
class NoteFactory extends Factory

View file

@ -0,0 +1,34 @@
<?php
namespace Database\Factories;
use App\Models\Passkey;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Passkey>
*/
class PasskeyFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Passkey::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => $this->faker->numberBetween(1, 1000),
'passkey_id' => $this->faker->uuid,
'passkey' => $this->faker->sha256,
'transports' => ['internal'],
];
}
}

View file

@ -6,6 +6,8 @@ use App\Models\Place;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Place>
*/
class PlaceFactory extends Factory

View file

@ -5,6 +5,8 @@ namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SyndicationTarget>
*/
class SyndicationTargetFactory extends Factory

View file

@ -6,6 +6,8 @@ use App\Models\Tag;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Tag>
*/
class TagFactory extends Factory

View file

@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory

View file

@ -6,6 +6,8 @@ use App\Models\WebMention;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @psalm-suppress UnusedClass
*
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebMention>
*/
class WebMentionFactory extends Factory

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('passkeys', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('passkey_id')->unique();
$table->text('passkey');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('passkeys');
}
};

View file

@ -11,6 +11,8 @@ class ArticlesTableSeeder extends Seeder
{
/**
* Seed the articles table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -5,6 +5,9 @@ namespace Database\Seeders;
use App\Models\Bio;
use Illuminate\Database\Seeder;
/**
* @psalm-suppress UnusedClass
*/
class BioSeeder extends Seeder
{
/**

View file

@ -10,6 +10,8 @@ class BookmarksTableSeeder extends Seeder
{
/**
* Seed the bookmarks table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -11,6 +11,8 @@ class ClientsTableSeeder extends Seeder
{
/**
* Seed the clients table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -10,6 +10,8 @@ class ContactsTableSeeder extends Seeder
{
/**
* Seed the contacts table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -4,6 +4,9 @@ namespace Database\Seeders;
use Illuminate\Database\Seeder;
/**
* @psalm-suppress UnusedClass
*/
class DatabaseSeeder extends Seeder
{
/**

View file

@ -12,6 +12,8 @@ class LikesTableSeeder extends Seeder
{
/**
* Seed the likes table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -14,6 +14,8 @@ class NotesTableSeeder extends Seeder
{
/**
* Seed the notes table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -9,6 +9,8 @@ class PlacesTableSeeder extends Seeder
{
/**
* Seed the places table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -9,6 +9,8 @@ class UsersTableSeeder extends Seeder
{
/**
* Seed the users table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

View file

@ -9,6 +9,8 @@ class WebMentionsTableSeeder extends Seeder
{
/**
* Seed the webmentions table.
*
* @psalm-suppress PossiblyUnusedMethod
*/
public function run(): void
{

2872
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,29 +5,29 @@
"repository": "https://github.com/jonnybarnes/jonnybarnes.uk",
"license": "CC0-1.0",
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@csstools/postcss-oklab-function": "^2.2.3",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@csstools/postcss-oklab-function": "^3.0.7",
"autoprefixer": "^10.4.16",
"babel-loader": "^9.1.3",
"browserlist": "^1.0.1",
"compression-webpack-plugin": "^10.0.0",
"css-loader": "^6.8.1",
"cssnano": "^6.0.1",
"eslint": "^8.42.0",
"eslint": "^8.52.0",
"eslint-webpack-plugin": "^4.0.1",
"mini-css-extract-plugin": "^2.7.6",
"postcss": "^8.4.24",
"postcss": "^8.4.31",
"postcss-combine-duplicated-selectors": "^10.0.2",
"postcss-combine-media-query": "^1.0.1",
"postcss-import": "^15.1.0",
"postcss-loader": "^7.3.3",
"postcss-nesting": "^11.3.0",
"postcss-nesting": "^12.0.1",
"style-loader": "^3.3.3",
"stylelint": "^15.7.0",
"stylelint-config-standard": "^33.0.0",
"stylelint": "^15.11.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-webpack-plugin": "^4.1.1",
"webpack": "^5.87.0",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
},
"scripts": {

View file

@ -1,23 +1,18 @@
<?xml version="1.0"?>
<psalm
errorLevel="7"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="true"
>
<projectFiles>
<directory name="app" />
<directory name="app"/>
<directory name="database/factories"/>
<directory name="database/seeders"/>
<ignoreFiles>
<directory name="vendor" />
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<InvalidStaticInvocation>
<errorLevel type="suppress">
<file name="app/Providers/RouteServiceProvider.php" />
</errorLevel>
</InvalidStaticInvocation>
</issueHandlers>
</psalm>
<plugins><pluginClass class="Psalm\LaravelPlugin\Plugin"/></plugins></psalm>

View file

@ -1 +1 @@
:root{--font-family-headings:"Archer SSm A","Archer SSm B",serif;--font-family-body:"Verlag A","Verlag B",sans-serif;--font-family-monospace:"Operator Mono SSm A","Operator Mono SSm B",monospace;--font-size-sm:0.75rem;--font-size-base:1rem;--font-size-md:1.25rem;--font-size-lg:1.5rem;--font-size-xl:1.75rem;--font-size-xxl:2rem;--font-size-xxxl:2.25rem;--color-primary:#334700;--color-secondary:#e3ffb7;--color-link:#00649e;--color-link-visited:#bc7aff;--color-primary-shadow:rgba(16,25,0,.4)}@supports (color:color(display-p3 0 0 0)){:root{--color-primary:color(display-p3 0.21567 0.27838 0.03615);--color-secondary:color(display-p3 0.91016 0.99842 0.74082);--color-link:color(display-p3 0.01045 0.38351 0.63618);--color-link-visited:color(display-p3 0.70467 0.47549 0.99958);--color-primary-shadow:color(display-p3 0.06762 0.09646 0.00441/0.4)}}@supports (color:oklch(0% 0 0)){:root{--color-primary:oklch(36.8% 0.1 125.505);--color-secondary:oklch(96.3% 0.1 125.505);--color-link:oklch(48.09% 0.146 241.41);--color-link-visited:oklch(70.44% 0.21 304.41);--color-primary-shadow:oklch(19.56% 0.054 125.505/40%)}}body{background-color:var(--color-secondary);color:var(--color-primary);font-family:var(--font-family-body);font-size:var(--font-size-md)}code{font-family:var(--font-family-monospace)}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-headings)}.grid{display:grid;grid-template-columns:5vw 1fr 5vw;grid-template-rows:-webkit-min-content 1fr -webkit-min-content;grid-template-rows:min-content 1fr min-content;row-gap:1rem}#site-header{grid-column:2/3;grid-row:1/2}main{grid-row:2/3}footer,main{grid-column:2/3}footer{grid-row:3/4}footer .iwc-logo{max-width:85vw}a{color:var(--color-link)}a:visited{color:var(--color-link-visited)}#site-header a:visited{color:var(--color-link)}.hljs{border-radius:.5rem}.p-bridgy-twitter-content{display:none}.h-card .hovercard{-webkit-box-orient:vertical;-webkit-box-direction:normal;background-color:var(--color-secondary);border-radius:1rem;-webkit-box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);display:none;-ms-flex-direction:column;flex-direction:column;gap:.5rem;opacity:0;padding:1rem;position:absolute;-webkit-transition:opacity .5s ease-in-out;transition:opacity .5s ease-in-out;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;z-index:100}.h-card .hovercard .u-photo{max-width:6rem}.h-card .hovercard .social-icon{height:1rem;width:1rem}.h-card:hover .hovercard{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.h-entry{-webkit-border-start:1px solid var(--color-primary);-webkit-padding-start:.5rem;border-inline-start:1px solid var(--color-primary);padding-inline-start:.5rem}.h-entry .reply-to{font-style:italic}.h-entry .post-info a{text-decoration:none}.h-entry .note-metadata{-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;gap:1rem}.h-entry .note-metadata .syndication-links{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.h-entry .note-metadata .syndication-links a{text-decoration:none}.h-entry .note-metadata .syndication-links a svg{height:1rem;width:1rem}
:root{--font-family-headings:"Archer SSm A","Archer SSm B",serif;--font-family-body:"Verlag A","Verlag B",sans-serif;--font-family-monospace:"Operator Mono SSm A","Operator Mono SSm B",monospace;--font-size-sm:0.75rem;--font-size-base:1rem;--font-size-md:1.25rem;--font-size-lg:1.5rem;--font-size-xl:1.75rem;--font-size-xxl:2rem;--font-size-xxxl:2.25rem;--color-primary:#334700;--color-secondary:#e3ffb7;--color-link:#00649e;--color-link-visited:#bc7aff;--color-primary-shadow:rgba(16,25,0,.4)}@supports (color:color(display-p3 0 0 0)){:root{--color-primary:color(display-p3 0.21567 0.27838 0.03615);--color-secondary:color(display-p3 0.91016 0.99842 0.74082);--color-link:color(display-p3 0.01045 0.38351 0.63618);--color-link-visited:color(display-p3 0.70467 0.47549 0.99958);--color-primary-shadow:color(display-p3 0.06762 0.09646 0.00441/0.4)}}@supports (color:oklch(0% 0 0)){:root{--color-primary:oklch(36.8% 0.1 125.505deg);--color-secondary:oklch(96.3% 0.1 125.505deg);--color-link:oklch(48.09% 0.146 241.41deg);--color-link-visited:oklch(70.44% 0.21 304.41deg);--color-primary-shadow:oklch(19.56% 0.054 125.505deg/40%)}}body{background-color:var(--color-secondary);color:var(--color-primary);font-family:var(--font-family-body);font-size:var(--font-size-md)}code{font-family:var(--font-family-monospace)}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-headings)}.grid{display:grid;grid-template-columns:5vw 1fr 5vw;grid-template-rows:-webkit-min-content 1fr -webkit-min-content;grid-template-rows:min-content 1fr min-content;row-gap:1rem}#site-header{grid-column:2/3;grid-row:1/2}main{grid-row:2/3}footer,main{grid-column:2/3}footer{grid-row:3/4}footer .iwc-logo{max-width:85vw}footer .footer-actions{-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;gap:1rem}a{color:var(--color-link)}a:visited{color:var(--color-link-visited)}#site-header a:visited,a.auth:visited{color:var(--color-link)}.hljs{border-radius:.5rem}.h-card .hovercard{-webkit-box-orient:vertical;-webkit-box-direction:normal;background-color:var(--color-secondary);border-radius:1rem;-webkit-box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);display:none;-ms-flex-direction:column;flex-direction:column;gap:.5rem;opacity:0;padding:1rem;position:absolute;-webkit-transition:opacity .5s ease-in-out;transition:opacity .5s ease-in-out;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;z-index:100}.h-card .hovercard .u-photo{max-width:6rem}.h-card .hovercard .social-icon{height:1rem;width:1rem}.h-card:hover .hovercard{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.h-entry{-webkit-border-start:1px solid var(--color-primary);-webkit-padding-start:.5rem;border-inline-start:1px solid var(--color-primary);padding-inline-start:.5rem}.h-entry .reply-to{font-style:italic}.h-entry .post-info a{text-decoration:none}.h-entry .note-metadata{-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;gap:1rem}.h-entry .note-metadata .syndication-links{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.h-entry .note-metadata .syndication-links a{text-decoration:none}.h-entry .note-metadata .syndication-links a svg{height:1rem;width:1rem}

Binary file not shown.

View file

@ -0,0 +1 @@
!function(){"use strict";let e=new class{constructor(){}async register(){const e=await this.getCreateOptions(),t={challenge:this.base64URLStringToBuffer(e.challenge),rp:{id:e.rp.id,name:e.rp.name},user:{id:(new TextEncoder).encode(window.atob(e.user.id)),name:e.user.name,displayName:e.user.displayName},pubKeyCredParams:e.pubKeyCredParams,excludeCredentials:[],authenticatorSelection:e.authenticatorSelection,timeout:6e4},a=await navigator.credentials.create({publicKey:t});if(!a)throw new Error("Error generating a passkey");const n={id:a.id?a.id:null,type:a.type?a.type:null,rawId:a.rawId?this.bufferToBase64URLString(a.rawId):null,response:{attestationObject:a.response.attestationObject?this.bufferToBase64URLString(a.response.attestationObject):null,clientDataJSON:a.response.clientDataJSON?this.bufferToBase64URLString(a.response.clientDataJSON):null}};if(!(await window.fetch("/admin/passkeys/register",{method:"POST",body:JSON.stringify(n),cache:"no-cache",headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Error saving the passkey");window.location.reload()}async getCreateOptions(){const e=await fetch("/admin/passkeys/register",{method:"GET"});return await e.json()}async login(){const e=await this.getLoginData(),t=await navigator.credentials.get({publicKey:{challenge:this.base64URLStringToBuffer(e.challenge),userVerification:e.userVerification,timeout:6e4}});if(!t)throw new Error("Authentication failed");const a={id:t.id?t.id:"",type:t.type?t.type:"",rawId:t.rawId?this.bufferToBase64URLString(t.rawId):"",response:{authenticatorData:t.response.authenticatorData?this.bufferToBase64URLString(t.response.authenticatorData):"",clientDataJSON:t.response.clientDataJSON?this.bufferToBase64URLString(t.response.clientDataJSON):"",signature:t.response.signature?this.bufferToBase64URLString(t.response.signature):"",userHandle:t.response.userHandle?this.bufferToBase64URLString(t.response.userHandle):""}};if(!(await window.fetch("/login/passkey",{method:"POST",body:JSON.stringify(a),headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Login failed");window.location.assign("/admin")}async getLoginData(){const e=await fetch("/login/passkey",{method:"GET"});return await e.json()}base64URLStringToBuffer(e){const t=e.replace(/-/g,"+").replace(/_/g,"/"),a=(4-t.length%4)%4,n=t.padEnd(t.length+a,"="),r=window.atob(n),i=new ArrayBuffer(r.length),s=new Uint8Array(i);for(let e=0;e<r.length;e++)s[e]=r.charCodeAt(e);return i}bufferToBase64URLString(e){const t=new Uint8Array(e);let a="";for(const e of t)a+=String.fromCharCode(e);return btoa(a).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}};document.querySelectorAll(".add-passkey").forEach((t=>{t.addEventListener("click",(()=>{e.register()}))})),document.querySelectorAll(".login-passkey").forEach((t=>{t.addEventListener("click",(()=>{e.login()}))}))}();

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,8 @@
{
"/app.js": "/app.js?id=4f93645700a6c5485654",
"/app.css": "/app.css?id=99048465add47d086ac7",
"/app-dark.css": "/app-dark.css?id=f42b555818ed19478827"
"/app.js": "/app.js?id=ff1533ec4a7afad65c5bd7bcc2cc7d7b",
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",
"/img/horizon.svg": "/img/horizon.svg?id=904d5b5185fefb09035384e15bfca765",
"/img/sprite.svg": "/img/sprite.svg?id=afc4952b74895bdef3ab4ebe9adb746f"
}

View file

@ -1,6 +1,6 @@
@import "variables.css";
@import "fonts.css";
@import "layout.css";
@import "colours.css";
@import "code.css";
@import "content.css";
@import url('variables.css');
@import url('fonts.css');
@import url('layout.css');
@import url('colours.css');
@import url('code.css');
@import url('content.css');

View file

@ -9,6 +9,10 @@ a {
&:visited {
color: var(--color-link-visited);
}
&.auth:visited {
color: var(--color-link);
}
}
#site-header {

View file

@ -1,5 +1,4 @@
@import "posse.css";
@import "h-card.css";
@import url('h-card.css');
.h-entry {
border-inline-start: 1px solid var(--color-primary);

View file

@ -22,4 +22,10 @@ footer {
& .iwc-logo {
max-width: 85vw;
}
& .footer-actions {
display: flex;
flex-direction: row;
gap: 1rem;
}
}

View file

@ -1,3 +0,0 @@
.p-bridgy-twitter-content {
display: none;
}

Some files were not shown because too many files have changed in this diff Show more