diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php index 17091ff13..27763085e 100644 --- a/app/Http/Controllers/Auth/LoginCheckpointController.php +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -63,7 +63,7 @@ class LoginCheckpointController extends AbstractLoginController $decrypted = $this->encrypter->decrypt($user->totp_secret); - if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { + if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { return $this->sendLoginResponse($user, $request); } } diff --git a/app/Models/SecurityKey.php b/app/Models/SecurityKey.php index 9dc89543d..e19c9f11f 100644 --- a/app/Models/SecurityKey.php +++ b/app/Models/SecurityKey.php @@ -2,11 +2,12 @@ namespace Pterodactyl\Models; -use Ramsey\Uuid\Uuid; use Illuminate\Http\Request; -use Ramsey\Uuid\UuidInterface; +use Symfony\Component\Uid\Uuid; use Webauthn\TrustPath\TrustPath; +use Symfony\Component\Uid\NilUuid; use Nyholm\Psr7\Factory\Psr17Factory; +use Symfony\Component\Uid\AbstractUid; use Webauthn\PublicKeyCredentialSource; use Webauthn\TrustPath\TrustPathLoader; use Webauthn\PublicKeyCredentialDescriptor; @@ -14,7 +15,27 @@ use Psr\Http\Message\ServerRequestInterface; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Factories\HasFactory; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; +use Illuminate\Database\Eloquent\Casts\Attribute; +/** + * @property int $id + * @property string $uuid + * @property int $user_id + * @property string $name + * @property string $public_key_id + * @property string $public_key + * @property AbstractUid $aaguid + * @property string $type + * @property string[] $transports + * @property string $attestation_type + * @property \Webauthn\TrustPath\TrustPath $trust_path + * @property string $user_handle + * @property int $counter + * @property array|null $other_ui + * + * @property \Carbon\CarbonImmutable $created_at + * @property \Carbon\CarbonImmutable $updated_at + */ class SecurityKey extends Model { use HasFactory; @@ -33,57 +54,36 @@ class SecurityKey extends Model 'user_id', ]; - public function getPublicKeyAttribute(string $value): string + public function publicKey(): Attribute { - return base64_decode($value); + return new Attribute( + get: fn (string $value) => base64_decode($value), + set: fn (string $value) => base64_encode($value), + ); } - public function setPublicKeyAttribute(string $value): void + public function publicKeyId(): Attribute { - $this->attributes['public_key'] = base64_encode($value); + return new Attribute( + get: fn (string $value) => base64_decode($value), + set: fn (string $value) => base64_encode($value), + ); } - public function getPublicKeyIdAttribute(string $value): string + public function aaguid(): Attribute { - return base64_decode($value); + return Attribute::make( + get: fn (string|null $value): AbstractUid => is_null($value) ? new NilUuid() : Uuid::fromString($value), + set: fn (AbstractUid|null $value): string|null => (is_null($value) || $value instanceof NilUuid) ? null : $value->__toString(), + ); } - public function setPublicKeyIdAttribute(string $value): void + public function trustPath(): Attribute { - $this->attributes['public_key_id'] = base64_encode($value); - } - - public function getTrustPathAttribute(?string $value): ?TrustPath - { - if (is_null($value)) { - return null; - } - - return TrustPathLoader::loadTrustPath(json_decode($value, true)); - } - - public function setTrustPathAttribute(?TrustPath $value): void - { - $this->attributes['trust_path'] = json_encode($value); - } - - /** - * @param \Ramsey\Uuid\UuidInterface|string|null $value - */ - public function setAaguidAttribute($value): void - { - $value = $value instanceof UuidInterface ? $value->__toString() : $value; - - $this->attributes['aaguid'] = (is_null($value) || $value === Uuid::NIL) ? null : $value; - } - - public function getAaguidAttribute(?string $value): ?UuidInterface - { - if (!is_null($value) && Uuid::isValid($value)) { - return Uuid::fromString($value); - } - - return null; + return new Attribute( + get: fn (mixed $value) => is_null($value) ? null : TrustPathLoader::loadTrustPath(json_decode($value, true)), + set: fn (TrustPath|null $value) => json_encode($value), + ); } public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor @@ -99,7 +99,7 @@ class SecurityKey extends Model $this->transports, $this->attestation_type, $this->trust_path, - $this->aaguid ?? Uuid::fromString(Uuid::NIL), + $this->aaguid, $this->public_key, $this->user_handle, $this->counter diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php index ec678e40a..f10473bf1 100644 --- a/app/Services/Databases/DeployServerDatabaseService.php +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -38,7 +38,9 @@ class DeployServerDatabaseService throw new NoSuitableDatabaseHostException(); } - $databaseHostId = $hosts->random()->id; + /** @var \Pterodactyl\Models\DatabaseHost $databaseHost */ + $databaseHost = $hosts->random(); + $databaseHostId = $databaseHost->id; } return $this->managementService->create($server, [ diff --git a/app/Transformers/Api/Client/SecurityKeyTransformer.php b/app/Transformers/Api/Client/SecurityKeyTransformer.php index aaf36fca5..48ee6ffc2 100644 --- a/app/Transformers/Api/Client/SecurityKeyTransformer.php +++ b/app/Transformers/Api/Client/SecurityKeyTransformer.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\SecurityKey; +use Pterodactyl\Transformers\Api\Transformer; -class SecurityKeyTransformer extends BaseClientTransformer +class SecurityKeyTransformer extends Transformer { public function getResourceName(): string { diff --git a/resources/scripts/api/account/security-keys.ts b/resources/scripts/api/account/security-keys.ts index cfc89576e..2541729ed 100644 --- a/resources/scripts/api/account/security-keys.ts +++ b/resources/scripts/api/account/security-keys.ts @@ -1,11 +1,14 @@ -import useSWR, { ConfigInterface } from 'swr'; -import { useStoreState } from '@/state/hooks'; -import http, { FractalResponseList } from '@/api/http'; -import { SecurityKey, Transformers } from '@definitions/user'; -import { AxiosError } from 'axios'; +import type { AxiosError } from 'axios'; +import type { SWRConfiguration } from 'swr'; +import useSWR from 'swr'; + +import type { SecurityKey } from '@definitions/user'; +import { Transformers } from '@definitions/user'; +import { LoginResponse } from '@/api/auth/login'; +import type { FractalResponseList } from '@/api/http'; +import http from '@/api/http'; import { decodeBase64 } from '@/lib/base64'; import { decodeBuffer, encodeBuffer } from '@/lib/buffer'; -import { LoginResponse } from '@/api/auth/login'; import { useUserSWRKey } from '@/plugins/useSWRKey'; function decodeSecurityKeyCredentials(credentials: PublicKeyCredentialDescriptor[]) { @@ -16,8 +19,7 @@ function decodeSecurityKeyCredentials(credentials: PublicKeyCredentialDescriptor })); } -function useSecurityKeys(config?: ConfigInterface) { - const uuid = useStoreState(state => state.user.data!.uuid); +function useSecurityKeys(config?: SWRConfiguration) { const key = useUserSWRKey(['account', 'security-keys']); return useSWR( @@ -25,9 +27,9 @@ function useSecurityKeys(config?: ConfigInterface) { async (): Promise => { const { data } = await http.get('/api/client/account/security-keys'); - return (data as FractalResponseList).data.map((datum) => Transformers.toSecurityKey(datum.attributes)); + return (data as FractalResponseList).data.map(datum => Transformers.toSecurityKey(datum.attributes)); }, - { revalidateOnMount: false, ...(config || {}) }, + { revalidateOnMount: false, ...(config ?? {}) }, ); } @@ -35,7 +37,11 @@ async function deleteSecurityKey(uuid: string): Promise { await http.delete(`/api/client/account/security-keys/${uuid}`); } -async function registerCredentialForAccount(name: string, tokenId: string, credential: PublicKeyCredential): Promise { +async function registerCredentialForAccount( + name: string, + tokenId: string, + credential: PublicKeyCredential, +): Promise { const { data } = await http.post('/api/client/account/security-keys/register', { name, token_id: tokenId, @@ -44,7 +50,9 @@ async function registerCredentialForAccount(name: string, tokenId: string, crede type: credential.type, rawId: encodeBuffer(credential.rawId), response: { - attestationObject: encodeBuffer((credential.response as AuthenticatorAttestationResponse).attestationObject), + attestationObject: encodeBuffer( + (credential.response as AuthenticatorAttestationResponse).attestationObject, + ), clientDataJSON: encodeBuffer(credential.response.clientDataJSON), }, }, @@ -66,7 +74,9 @@ async function registerSecurityKey(name: string): Promise { const credentials = await navigator.credentials.create({ publicKey }); if (!credentials || credentials.type !== 'public-key') { - throw new Error(`Unexpected type returned by navigator.credentials.create(): expected "public-key", got "${credentials?.type}"`); + throw new Error( + `Unexpected type returned by navigator.credentials.create(): expected "public-key", got "${credentials?.type}"`, + ); } return await registerCredentialForAccount(name, data.data.token_id, credentials as PublicKeyCredential); diff --git a/resources/scripts/api/definitions/user/transformers.ts b/resources/scripts/api/definitions/user/transformers.ts index 4fa7fd3e3..6a04af939 100644 --- a/resources/scripts/api/definitions/user/transformers.ts +++ b/resources/scripts/api/definitions/user/transformers.ts @@ -22,7 +22,7 @@ export default class Transformers { }; }; - static toSecurityKey (data: Record): Models.SecurityKey { + static toSecurityKey(data: Record): Models.SecurityKey { return { uuid: data.uuid, name: data.name, diff --git a/resources/scripts/lib/base64.spec.ts b/resources/scripts/lib/base64.spec.ts index 84e7124b7..5f4299eda 100644 --- a/resources/scripts/lib/base64.spec.ts +++ b/resources/scripts/lib/base64.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { decodeBase64 } from '@/lib/base64'; describe('@/lib/base64.ts', function () { diff --git a/resources/scripts/lib/base64.ts b/resources/scripts/lib/base64.ts index dec9bbe35..a9da15330 100644 --- a/resources/scripts/lib/base64.ts +++ b/resources/scripts/lib/base64.ts @@ -1,4 +1,4 @@ -function decodeBase64 (input: string): string { +function decodeBase64(input: string): string { input = input.replace(/-/g, '+').replace(/_/g, '/'); const pad = input.length % 4; @@ -13,4 +13,4 @@ function decodeBase64 (input: string): string { return input; } -export { decodeBase64 } +export { decodeBase64 }; diff --git a/resources/scripts/lib/buffer.spec.ts b/resources/scripts/lib/buffer.spec.ts index 9e1297dee..9a6e6b24a 100644 --- a/resources/scripts/lib/buffer.spec.ts +++ b/resources/scripts/lib/buffer.spec.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { decodeBuffer, encodeBuffer } from '@/lib/buffer'; describe('@/lib/buffer.ts', function () {