icloud auth done
This commit is contained in:
@@ -34,7 +34,8 @@
|
|||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-fileinfo": "*",
|
"ext-fileinfo": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"php-http/curl-client": "^2.4"
|
"php-http/curl-client": "^2.4",
|
||||||
|
"ext-sodium": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^2.1",
|
"phpstan/phpstan": "^2.1",
|
||||||
|
|||||||
@@ -9,12 +9,17 @@ return function (PDO $db): void {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
remote_name TEXT NOT NULL UNIQUE,
|
remote_name TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
apple_id TEXT NOT NULL,
|
apple_id TEXT NOT NULL,
|
||||||
trust_token TEXT,
|
password TEXT NOT NULL,
|
||||||
cookies TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending',
|
trust_token TEXT NOT NULL,
|
||||||
connected_at INTEGER,
|
cookies TEXT NOT NULL,
|
||||||
|
|
||||||
|
status TEXT NOT NULL DEFAULT 'connected',
|
||||||
|
connected_at INTEGER NOT NULL,
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
|
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
");
|
");
|
||||||
@@ -141,8 +141,7 @@ $routes = static function (RouteCollector $r): void {
|
|||||||
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
|
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
|
||||||
|
|
||||||
$r->get('/icloud/connect', [ICloudAuthController::class, 'connectForm']);
|
$r->get('/icloud/connect', [ICloudAuthController::class, 'connectForm']);
|
||||||
$r->post('/icloud/connect', [ICloudAuthController::class, 'submitCredentials']);
|
$r->post('/icloud/connect', [ICloudAuthController::class, 'submit']);
|
||||||
$r->post('/icloud/2fa', [ICloudAuthController::class, 'submit2fa']);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,114 +1,62 @@
|
|||||||
<!-- icloud_login.php -->
|
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<h1><?= htmlspecialchars($viewModel->title ?? 'iCloud Login') ?></h1>
|
<h1>Connect iCloud</h1>
|
||||||
|
|
||||||
<?php if (!empty($viewModel->error)) : ?>
|
<?php if (!empty($viewModel->error)) : ?>
|
||||||
<p class="error"><?= htmlspecialchars($viewModel->error) ?></p>
|
<p class="error"><?= htmlspecialchars($viewModel->error) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (isset($viewModel->show2fa) && $viewModel->show2fa): ?>
|
<div class="info-message">
|
||||||
<div class="info-message" style="
|
<ol>
|
||||||
background: rgba(66, 153, 225, 0.1);
|
<li>Open <b>icloud.com</b> and log in</li>
|
||||||
border-left: 4px solid #4299e1;
|
<li>Complete 2FA</li>
|
||||||
padding: 1rem;
|
<li>Open DevTools → Network</li>
|
||||||
margin-bottom: 1.5rem;
|
<li>Copy <b>Request Header → Cookie</b></li>
|
||||||
border-radius: 0 8px 8px 0;
|
<li>Copy <b>X-APPLE-WEBAUTH-HSA-TRUST</b> value</li>
|
||||||
color: #2d3748;
|
</ol>
|
||||||
">
|
</div>
|
||||||
<p style="margin: 0; font-weight: 500;">
|
|
||||||
Enter the 6-digit verification code sent to your trusted devices.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST" action="/icloud/2fa" class="login-form">
|
<form method="POST" action="/icloud/connect" class="login-form">
|
||||||
<input type="hidden" name="apple_id" value="<?= htmlspecialchars($viewModel->appleId ?? '') ?>">
|
<label for="apple_id">Apple ID</label>
|
||||||
<input type="hidden" name="password" value="<?= htmlspecialchars($viewModel->password ?? '') ?>">
|
<input
|
||||||
|
type="email"
|
||||||
|
id="apple_id"
|
||||||
|
name="apple_id"
|
||||||
|
required
|
||||||
|
placeholder="yourname@icloud.com"
|
||||||
|
>
|
||||||
|
|
||||||
<label for="code">
|
<label for="password">Password</label>
|
||||||
6-digit verification code
|
<input
|
||||||
</label>
|
type="password"
|
||||||
<input type="text"
|
id="password"
|
||||||
id="code"
|
name="password"
|
||||||
name="code"
|
required
|
||||||
pattern="[0-9]{6}"
|
placeholder="iCloud password"
|
||||||
maxlength="6"
|
>
|
||||||
required
|
|
||||||
placeholder="123456"
|
|
||||||
autocomplete="one-time-code"
|
|
||||||
inputmode="numeric"
|
|
||||||
style="letter-spacing: 2px; font-size: 1.2rem; text-align: center;">
|
|
||||||
|
|
||||||
<button type="submit" style="margin-top: 1rem;">
|
<label for="cookies">Cookies (full header)</label>
|
||||||
Verify Code
|
<textarea
|
||||||
</button>
|
id="cookies"
|
||||||
|
name="cookies"
|
||||||
|
rows="6"
|
||||||
|
required
|
||||||
|
placeholder="X-APPLE-WEBAUTH-HSA-TRUST=...; X-APPLE-WEBAUTH-USER=...;"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
<div style="margin-top: 1rem; text-align: center;">
|
<label for="trust_token">Trust token</label>
|
||||||
<a href="/icloud/connect" style="color: #667eea; text-decoration: none;">
|
<input
|
||||||
← Back to login
|
type="text"
|
||||||
</a>
|
id="trust_token"
|
||||||
</div>
|
name="trust_token"
|
||||||
|
required
|
||||||
|
placeholder="X-APPLE-WEBAUTH-HSA-TRUST value"
|
||||||
|
>
|
||||||
|
|
||||||
<input type="hidden" name="_csrf"
|
<button type="submit">
|
||||||
value="<?= htmlspecialchars($viewModel->csrf) ?>">
|
Connect iCloud
|
||||||
</form>
|
</button>
|
||||||
|
|
||||||
<?php else: ?>
|
<input type="hidden" name="_csrf"
|
||||||
<form method="POST" action="/icloud/connect" class="login-form">
|
value="<?= htmlspecialchars($viewModel->csrf) ?>">
|
||||||
<label for="apple_id">
|
</form>
|
||||||
Apple ID
|
|
||||||
</label>
|
|
||||||
<input type="email"
|
|
||||||
id="apple_id"
|
|
||||||
name="apple_id"
|
|
||||||
required
|
|
||||||
placeholder="name@example.com"
|
|
||||||
autocomplete="username">
|
|
||||||
|
|
||||||
<label for="password">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
required
|
|
||||||
placeholder="Enter your password"
|
|
||||||
autocomplete="current-password">
|
|
||||||
|
|
||||||
<button type="submit">
|
|
||||||
Connect iCloud
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<input type="hidden" name="_csrf"
|
|
||||||
value="<?= htmlspecialchars($viewModel->csrf) ?>">
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
const codeInput = document.getElementById('code');
|
|
||||||
if (codeInput) {
|
|
||||||
codeInput.addEventListener('keypress', function (e) {
|
|
||||||
const char = String.fromCharCode(e.which);
|
|
||||||
if (!/^\d$/.test(char)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
codeInput.addEventListener('input', function () {
|
|
||||||
if (this.value.length === 6) {
|
|
||||||
this.form.submit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.info-message {
|
|
||||||
background: rgba(66, 153, 225, 0.15) !important;
|
|
||||||
border-left-color: #4299e1 !important;
|
|
||||||
color: #cbd5e1 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace Din9xtrCloud\Controllers;
|
|||||||
|
|
||||||
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
||||||
use Din9xtrCloud\Rclone\RcloneClient;
|
use Din9xtrCloud\Rclone\RcloneClient;
|
||||||
use Din9xtrCloud\Rclone\RcloneICloudConfigurator;
|
|
||||||
use Din9xtrCloud\Repositories\IcloudAccountRepository;
|
use Din9xtrCloud\Repositories\IcloudAccountRepository;
|
||||||
use Din9xtrCloud\View;
|
use Din9xtrCloud\View;
|
||||||
use Din9xtrCloud\ViewModels\Icloud\ICloudLoginViewModel;
|
use Din9xtrCloud\ViewModels\Icloud\ICloudLoginViewModel;
|
||||||
@@ -18,7 +17,6 @@ use Throwable;
|
|||||||
final readonly class ICloudAuthController
|
final readonly class ICloudAuthController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private RcloneICloudConfigurator $configurator,
|
|
||||||
private ResponseFactoryInterface $responseFactory,
|
private ResponseFactoryInterface $responseFactory,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private IcloudAccountRepository $repository,
|
private IcloudAccountRepository $repository,
|
||||||
@@ -33,122 +31,107 @@ final readonly class ICloudAuthController
|
|||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $request->getQueryParams();
|
$error = $_SESSION['icloud_error'] ?? '';
|
||||||
$show2fa = (bool)($query['show2fa'] ?? false);
|
unset($_SESSION['icloud_error']);
|
||||||
$appleId = (string)($query['apple_id'] ?? '');
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
if (isset($_SESSION['icloud_error'])) {
|
|
||||||
$error = (string)$_SESSION['icloud_error'];
|
|
||||||
unset($_SESSION['icloud_error']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = $show2fa ? 'iCloud 2FA Verification' : 'iCloud Login';
|
|
||||||
|
|
||||||
return View::display(
|
return View::display(
|
||||||
new ICloudLoginViewModel(
|
new ICloudLoginViewModel(
|
||||||
title: $title,
|
title: 'Connect iCloud',
|
||||||
csrf: CsrfMiddleware::generateToken($request),
|
csrf: CsrfMiddleware::generateToken($request),
|
||||||
show2fa: $show2fa,
|
error: $error
|
||||||
error: $error,
|
|
||||||
appleId: $appleId
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function submitCredentials(ServerRequestInterface $request): ResponseInterface
|
public function submit(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
$data = (array)$request->getParsedBody();
|
$data = (array)$request->getParsedBody();
|
||||||
$user = $request->getAttribute('user');
|
$user = $request->getAttribute('user');
|
||||||
|
|
||||||
$appleId = (string)($data['apple_id'] ?? '');
|
$appleId = trim((string)($data['apple_id'] ?? ''));
|
||||||
$password = (string)($data['password'] ?? '');
|
$password = trim((string)($data['password'] ?? ''));
|
||||||
$remote = 'icloud_' . $user->id;
|
$cookies = trim((string)($data['cookies'] ?? ''));
|
||||||
|
$trustToken = trim((string)($data['trust_token'] ?? ''));
|
||||||
|
|
||||||
try {
|
if ($appleId === '' || $password === '' || $cookies === '' || $trustToken === '') {
|
||||||
$this->configurator->createRemote($remote, $appleId);
|
return $this->fail('All fields are required');
|
||||||
$this->configurator->setPassword($remote, $password);
|
|
||||||
|
|
||||||
$config = $this->configurator->getConfig($remote);
|
|
||||||
$trustToken = $config['trust_token'] ?? null;
|
|
||||||
$cookies = $config['cookies'] ?? null;
|
|
||||||
|
|
||||||
$this->repository->create(
|
|
||||||
$user->id,
|
|
||||||
$remote,
|
|
||||||
$appleId,
|
|
||||||
$trustToken,
|
|
||||||
$cookies
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->responseFactory
|
|
||||||
->createResponse(302)
|
|
||||||
->withHeader('Location', '/icloud/connect?show2fa=1&apple_id=' . urlencode($appleId));
|
|
||||||
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
|
||||||
$_SESSION['icloud_error'] = $e->getMessage();
|
|
||||||
|
|
||||||
return $this->responseFactory
|
|
||||||
->createResponse(302)
|
|
||||||
->withHeader('Location', '/icloud/connect');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function submit2fa(ServerRequestInterface $request): ResponseInterface
|
|
||||||
{
|
|
||||||
$data = (array)$request->getParsedBody();
|
|
||||||
$user = $request->getAttribute('user');
|
|
||||||
|
|
||||||
$code = (string)($data['code'] ?? '');
|
|
||||||
$appleId = (string)($data['apple_id'] ?? '');
|
|
||||||
$remote = 'icloud_' . $user->id;
|
$remote = 'icloud_' . $user->id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$res = $this->configurator->submit2fa($remote, $code);
|
$encryptedAppleId = encryptString($appleId);
|
||||||
|
$encryptedPassword = encryptString($password);
|
||||||
|
|
||||||
$account = $this->repository->findByUserId($user->id);
|
try {
|
||||||
if ($account) {
|
$this->rclone->call('config/create', [
|
||||||
$this->repository->update($account, [
|
'name' => $remote,
|
||||||
'status' => 'connected',
|
'type' => 'iclouddrive',
|
||||||
'connected_at' => time(),
|
'parameters' => [
|
||||||
'trust_token' => $res['trust_token'],
|
'apple_id' => $appleId,
|
||||||
'cookies' => $res['cookies'],
|
'password' => $password,
|
||||||
|
'cookies' => $cookies,
|
||||||
|
'trust_token' => $trustToken,
|
||||||
|
],
|
||||||
|
'nonInteractive' => true,
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->warning('iCloud remote exists, updating', [
|
||||||
|
'remote' => $remote,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->rclone->call('config/update', [
|
||||||
|
'name' => $remote,
|
||||||
|
'parameters' => [
|
||||||
|
'apple_id' => $appleId,
|
||||||
|
'password' => $password,
|
||||||
|
'cookies' => $cookies,
|
||||||
|
'trust_token' => $trustToken,
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$contents = $this->rclone->call('operations/list', [
|
// health-check
|
||||||
|
$list = $this->rclone->call('operations/list', [
|
||||||
'fs' => $remote . ':',
|
'fs' => $remote . ':',
|
||||||
'remote' => '',
|
'remote' => '',
|
||||||
'opt' => [],
|
'maxDepth' => 1,
|
||||||
'recurse' => true,
|
|
||||||
'dirsOnly' => false,
|
|
||||||
'filesOnly' => false,
|
|
||||||
'metadata' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->logger->info('iCloud contents', ['user' => $user->id, '$contents' => $contents]);
|
$this->logger->info('icloud list', $list);
|
||||||
|
|
||||||
|
$this->repository->createOrUpdate(
|
||||||
|
userId: $user->id,
|
||||||
|
remoteName: $remote,
|
||||||
|
appleId: $encryptedAppleId,
|
||||||
|
password: $encryptedPassword,
|
||||||
|
trustToken: $trustToken,
|
||||||
|
cookies: $cookies,
|
||||||
|
);
|
||||||
|
|
||||||
return $this->responseFactory
|
return $this->responseFactory
|
||||||
->createResponse(302)
|
->createResponse(302)
|
||||||
->withHeader('Location', '/storage');
|
->withHeader('Location', '/storage');
|
||||||
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
$this->logger->error('iCloud connect failed', [
|
||||||
|
'remote' => $remote,
|
||||||
|
'exception' => $e,
|
||||||
|
]);
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
return $this->fail($e->getMessage());
|
||||||
$_SESSION['icloud_error'] = $e->getMessage();
|
|
||||||
|
|
||||||
return $this->responseFactory
|
|
||||||
->createResponse(302)
|
|
||||||
->withHeader('Location', '/icloud/connect?' . http_build_query([
|
|
||||||
'show2fa' => 1,
|
|
||||||
'apple_id' => $appleId
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
private function fail(string $message): ResponseInterface
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
$_SESSION['icloud_error'] = $message;
|
||||||
|
|
||||||
|
return $this->responseFactory
|
||||||
|
->createResponse(302)
|
||||||
|
->withHeader('Location', '/icloud/connect');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Random\RandomException;
|
||||||
|
|
||||||
if (!function_exists('formatBytes')) {
|
if (!function_exists('formatBytes')) {
|
||||||
|
|
||||||
function formatBytes(int $bytes): string
|
function formatBytes(int $bytes): string
|
||||||
@@ -88,9 +90,54 @@ if (!function_exists('validateIp')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
|
$flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
|
||||||
|
|
||||||
return filter_var($ip, FILTER_VALIDATE_IP, $flags)
|
return filter_var($ip, FILTER_VALIDATE_IP, $flags)
|
||||||
? $ip
|
? $ip
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('encryptString')) {
|
||||||
|
/**
|
||||||
|
* XSalsa20 + Poly1305
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
function encryptString(string $plaintext): string
|
||||||
|
{
|
||||||
|
$key = hex2bin($_ENV['APP_KEY']); // 32 байта
|
||||||
|
if (strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
|
||||||
|
throw new RuntimeException('APP_KEY must be 32 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||||
|
|
||||||
|
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
|
||||||
|
|
||||||
|
return base64_encode($nonce . $ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('decryptString')) {
|
||||||
|
/**
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
function decryptString(string $encrypted): string
|
||||||
|
{
|
||||||
|
$key = hex2bin($_ENV['APP_KEY']);
|
||||||
|
if (strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
|
||||||
|
throw new RuntimeException('APP_KEY must be 32 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = base64_decode($encrypted, true);
|
||||||
|
$nonce = substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||||
|
$ciphertext = substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||||
|
|
||||||
|
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
|
||||||
|
|
||||||
|
if ($plaintext === false) {
|
||||||
|
throw new RuntimeException('Failed to decrypt data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plaintext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,15 +6,19 @@ namespace Din9xtrCloud\Models;
|
|||||||
final readonly class IcloudAccount
|
final readonly class IcloudAccount
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public int $id,
|
public int $id,
|
||||||
public int $userId,
|
public int $userId,
|
||||||
public string $remoteName,
|
public string $remoteName,
|
||||||
public string $appleId,
|
|
||||||
public ?string $trustToken,
|
public string $appleId,
|
||||||
public ?string $cookies,
|
public string $password,
|
||||||
public string $status,
|
|
||||||
public ?int $connectedAt,
|
public string $trustToken,
|
||||||
public int $createdAt,
|
public string $cookies,
|
||||||
|
|
||||||
|
public string $status,
|
||||||
|
public int $connectedAt,
|
||||||
|
public int $createdAt,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Din9xtrCloud\Rclone;
|
|
||||||
|
|
||||||
use JsonException;
|
|
||||||
use Psr\Http\Client\ClientExceptionInterface;
|
|
||||||
|
|
||||||
final readonly class RcloneICloudConfigurator
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private RcloneClient $rclone,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ClientExceptionInterface
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
public function createRemote(string $remote, string $appleId): void
|
|
||||||
{
|
|
||||||
$this->rclone->call('config/create', [
|
|
||||||
'name' => $remote,
|
|
||||||
'type' => 'iclouddrive',
|
|
||||||
'parameters' => [
|
|
||||||
'apple_id' => $appleId,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ClientExceptionInterface
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
public function setPassword(string $remote, string $password): void
|
|
||||||
{
|
|
||||||
$this->rclone->call('config/password', [
|
|
||||||
'name' => $remote,
|
|
||||||
'parameters' => [
|
|
||||||
'password' => $password,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ClientExceptionInterface
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
public function submit2fa(string $remote, string $code): array
|
|
||||||
{
|
|
||||||
$this->rclone->call('config/update', [
|
|
||||||
'name' => $remote,
|
|
||||||
'parameters' => [
|
|
||||||
'config_2fa' => $code,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$config = $this->getConfig($remote);
|
|
||||||
return [
|
|
||||||
'trust_token' => $config['trust_token'] ?? null,
|
|
||||||
'cookies' => $config['cookies'] ?? null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws ClientExceptionInterface
|
|
||||||
* @throws JsonException
|
|
||||||
*/
|
|
||||||
public function getConfig(string $remote): array
|
|
||||||
{
|
|
||||||
return $this->rclone->call('config/show', [
|
|
||||||
'name' => $remote
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,60 +12,92 @@ final readonly class IcloudAccountRepository
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(
|
public function createOrUpdate(
|
||||||
int $userId,
|
int $userId,
|
||||||
string $remoteName,
|
string $remoteName,
|
||||||
string $appleId,
|
string $appleId,
|
||||||
?string $trustToken = null,
|
string $password,
|
||||||
?string $cookies = null
|
string $trustToken,
|
||||||
|
string $cookies
|
||||||
): IcloudAccount
|
): IcloudAccount
|
||||||
{
|
{
|
||||||
|
$existing = $this->findByUserId($userId);
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
UPDATE icloud_accounts
|
||||||
|
SET apple_id = :apple_id,
|
||||||
|
password = :password,
|
||||||
|
trust_token = :trust_token,
|
||||||
|
cookies = :cookies,
|
||||||
|
status = 'connected',
|
||||||
|
connected_at = :connected_at
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':apple_id' => $appleId,
|
||||||
|
':password' => $password,
|
||||||
|
':trust_token' => $trustToken,
|
||||||
|
':cookies' => $cookies,
|
||||||
|
':connected_at' => $now,
|
||||||
|
':id' => $existing->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new IcloudAccount(
|
||||||
|
id: $existing->id,
|
||||||
|
userId: $userId,
|
||||||
|
remoteName: $remoteName,
|
||||||
|
appleId: $appleId,
|
||||||
|
password: $password,
|
||||||
|
trustToken: $trustToken,
|
||||||
|
cookies: $cookies,
|
||||||
|
status: 'connected',
|
||||||
|
connectedAt: $now,
|
||||||
|
createdAt: $existing->createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare("
|
||||||
INSERT INTO icloud_accounts (user_id, remote_name, apple_id, trust_token, cookies)
|
INSERT INTO icloud_accounts
|
||||||
VALUES (:user_id, :remote_name, :apple_id, :trust_token, :cookies)
|
(user_id, remote_name, apple_id, password, trust_token, cookies, status, connected_at, created_at)
|
||||||
|
VALUES
|
||||||
|
(:user_id, :remote_name, :apple_id, :password, :trust_token, :cookies, 'connected', :connected_at, :created_at)
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':user_id' => $userId,
|
':user_id' => $userId,
|
||||||
':remote_name' => $remoteName,
|
':remote_name' => $remoteName,
|
||||||
':apple_id' => $appleId,
|
':apple_id' => $appleId,
|
||||||
|
':password' => $password,
|
||||||
':trust_token' => $trustToken,
|
':trust_token' => $trustToken,
|
||||||
':cookies' => $cookies,
|
':cookies' => $cookies,
|
||||||
|
':connected_at' => $now,
|
||||||
|
':created_at' => $now,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$id = (int)$this->db->lastInsertId();
|
|
||||||
|
|
||||||
return new IcloudAccount(
|
return new IcloudAccount(
|
||||||
id: $id,
|
id: (int)$this->db->lastInsertId(),
|
||||||
userId: $userId,
|
userId: $userId,
|
||||||
remoteName: $remoteName,
|
remoteName: $remoteName,
|
||||||
appleId: $appleId,
|
appleId: $appleId,
|
||||||
|
password: $password,
|
||||||
trustToken: $trustToken,
|
trustToken: $trustToken,
|
||||||
cookies: $cookies,
|
cookies: $cookies,
|
||||||
status: 'pending',
|
status: 'connected',
|
||||||
connectedAt: null,
|
connectedAt: $now,
|
||||||
createdAt: time(),
|
createdAt: $now,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(
|
|
||||||
IcloudAccount $account,
|
|
||||||
array $fields
|
|
||||||
): void
|
|
||||||
{
|
|
||||||
$set = [];
|
|
||||||
$params = [':id' => $account->id];
|
|
||||||
foreach ($fields as $key => $value) {
|
|
||||||
$set[] = "$key = :$key";
|
|
||||||
$params[":$key"] = $value;
|
|
||||||
}
|
|
||||||
$stmt = $this->db->prepare("UPDATE icloud_accounts SET " . implode(', ', $set) . " WHERE id = :id");
|
|
||||||
$stmt->execute($params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findByUserId(int $userId): ?IcloudAccount
|
public function findByUserId(int $userId): ?IcloudAccount
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("SELECT * FROM icloud_accounts WHERE user_id = :user_id");
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT * FROM icloud_accounts WHERE user_id = :user_id
|
||||||
|
");
|
||||||
$stmt->execute([':user_id' => $userId]);
|
$stmt->execute([':user_id' => $userId]);
|
||||||
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
return $row ? new IcloudAccount(
|
return $row ? new IcloudAccount(
|
||||||
@@ -73,10 +105,11 @@ final readonly class IcloudAccountRepository
|
|||||||
userId: (int)$row['user_id'],
|
userId: (int)$row['user_id'],
|
||||||
remoteName: $row['remote_name'],
|
remoteName: $row['remote_name'],
|
||||||
appleId: $row['apple_id'],
|
appleId: $row['apple_id'],
|
||||||
|
password: $row['password'],
|
||||||
trustToken: $row['trust_token'],
|
trustToken: $row['trust_token'],
|
||||||
cookies: $row['cookies'],
|
cookies: $row['cookies'],
|
||||||
status: $row['status'],
|
status: $row['status'],
|
||||||
connectedAt: $row['connected_at'] ? (int)$row['connected_at'] : null,
|
connectedAt: (int)$row['connected_at'],
|
||||||
createdAt: (int)$row['created_at'],
|
createdAt: (int)$row['created_at'],
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user