commit
335acb130e
114 changed files with 4856 additions and 2575 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ SESSION_SECURE_COOKIE=true
|
|||
|
||||
LOG_SLACK_WEBHOOK_URL=
|
||||
FLARE_KEY=
|
||||
IGNITION_OPEN_AI_KEY=
|
||||
|
||||
FONT_LINK=
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
|
@ -2,6 +2,8 @@ name: Deploy
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
|
|
|||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/pint.yml
vendored
2
.github/workflows/pint.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"import-notation": "string"
|
||||
}
|
||||
"extends": ["stylelint-config-standard"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ use Illuminate\Support\Facades\DB;
|
|||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class MigratePlaceDataFromPostgis extends Command
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ use Illuminate\Console\Command;
|
|||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\FileSystem\FileSystem;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ParseCachedWebMentions extends Command
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ use App\Jobs\DownloadWebMention;
|
|||
use App\Models\WebMention;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class ReDownloadWebMentions extends Command
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class TwitterContentException extends \Exception
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ use App\Models\Like;
|
|||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class LikesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* @psalm-suppress UnusedClass
|
||||
*/
|
||||
class NotesController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
|
|||
257
app/Http/Controllers/Admin/PasskeysController.php
Normal file
257
app/Http/Controllers/Admin/PasskeysController.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue