Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# On non-SE installations the flag is ignored and Lychee branding remains visible.
# WHITE_LABEL_ENABLED=false

###################################################################
# Keygen License Management #
###################################################################

# API token obtained from keygen.lycheeorg.dev.
# When set, Lychee will automatically rotate an expired license key
# on admin login and check the token health on the diagnostics page.
# KEYGEN_API_KEY=

###################################################################
# LDAP Authentication (enterprise directory integration) #
###################################################################
Expand Down
2 changes: 2 additions & 0 deletions app/Actions/Diagnostics/Errors.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use App\Actions\Diagnostics\Pipes\Checks\ImageOptCheck;
use App\Actions\Diagnostics\Pipes\Checks\ImagickPdfCheck;
use App\Actions\Diagnostics\Pipes\Checks\IniSettingsCheck;
use App\Actions\Diagnostics\Pipes\Checks\KeygenApiTokenCheck;
use App\Actions\Diagnostics\Pipes\Checks\MigrationCheck;
use App\Actions\Diagnostics\Pipes\Checks\OldLicenseCheck;
use App\Actions\Diagnostics\Pipes\Checks\OpCacheCheck;
Expand Down Expand Up @@ -57,6 +58,7 @@ class Errors
BasicPermissionCheck::class,
ConfigSanityCheck::class,
OldLicenseCheck::class,
KeygenApiTokenCheck::class,
DBSupportCheck::class,
GDSupportCheck::class,
ImageOptCheck::class,
Expand Down
68 changes: 68 additions & 0 deletions app/Actions/Diagnostics/Pipes/Checks/KeygenApiTokenCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Actions\Diagnostics\Pipes\Checks;

use App\Contracts\DiagnosticPipe;
use App\DTO\DiagnosticData;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use LycheeVerify\TokenExtension;

/**
* Check the expiration status of the Keygen API token.
* Only displayed if the user is an admin and a token is configured.
*/
class KeygenApiTokenCheck implements DiagnosticPipe
{
public function __construct(
private TokenExtension $token_extension,
) {
}

/**
* {@inheritDoc}
*/
public function handle(array &$data, \Closure $next): array
{
/** @var User|null */
$user = Auth::user();

if ($user?->may_administrate !== true) {
return $next($data);
}

/** @var string $api_key */
$api_key = config('verify.keygen_api_key', '');
if ($api_key === '') {
return $next($data);
}

// @codeCoverageIgnoreStart
$result = $this->token_extension->extend();

if (!$result->success) {
$data[] = DiagnosticData::error(
'Keygen API token error: ' . ($result->message ?? 'unknown error') . ' Go to keygen.lycheeorg.dev to retrieve a new one.',
self::class,
);

return $next($data);
}

if ($result->expires_at !== null && $result->expires_at->isBefore(now()->addWeek())) {
$data[] = DiagnosticData::warn(
'Your Keygen API token expires on ' . $result->expires_at->toDateString() . '. Consider renewing it at keygen.lycheeorg.dev.',
self::class,
);
}
// @codeCoverageIgnoreEnd

return $next($data);
}
}
15 changes: 15 additions & 0 deletions app/Actions/Diagnostics/Pipes/Checks/OldLicenseCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\Repositories\ConfigManager;
use Illuminate\Support\Facades\Schema;
use LycheeVerify\Contract\Status;
use LycheeVerify\Rotation;
use LycheeVerify\Verify;

/**
Expand All @@ -22,6 +23,7 @@ class OldLicenseCheck implements DiagnosticPipe
{
public function __construct(
private Verify $verify,
private Rotation $rotation,
protected readonly ConfigManager $config_manager,
) {
}
Expand All @@ -47,6 +49,19 @@ public function handle(array &$data, \Closure $next): array
return $next($data);
}

// @codeCoverageIgnoreStart
/** @var string $api_key */
$api_key = config('verify.keygen_api_key', '');
if ($api_key !== '') {
$result = $this->rotation->rotate();
if ($result->success) {
$this->verify->reset_status();

return $next($data);
}
}
// @codeCoverageIgnoreEnd

$data[] = DiagnosticData::error('Your license has expired. Go to keygen.lycheeorg.dev to retrieve a new one or erase the value in the license field.', self::class);

return $next($data);
Expand Down
46 changes: 46 additions & 0 deletions app/Jobs/RotateLicenseKeyJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Jobs;

use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use LycheeVerify\Contract\Status;
use LycheeVerify\Rotation;
use LycheeVerify\Verify;

class RotateLicenseKeyJob
{
use Dispatchable;

public function handle(Verify $verify, Rotation $rotation): void
{
if (!Schema::hasTable('configs')) {
return;
}

if ($verify->get_status() !== Status::FREE_EDITION) {
return;
}

/** @var string $api_key */
$api_key = config('verify.keygen_api_key', '');
if ($api_key === '') {
return;
}

// Make sure we try on login
Cache::forget(Rotation::CACHE_KEY);
$result = $rotation->rotate();

if ($result->success) {
$verify->reset_status();
}
}
}
27 changes: 27 additions & 0 deletions app/Listeners/RotateLicenseKeyOnLogin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Listeners;

use App\Jobs\RotateLicenseKeyJob;
use App\Models\User;
use Illuminate\Auth\Events\Login;

class RotateLicenseKeyOnLogin
{
public function handle(Login $event): void
{
$user = $event->user;

if (!$user instanceof User || $user->may_administrate !== true) {
return;
}

RotateLicenseKeyJob::dispatch();
Comment thread
ildyria marked this conversation as resolved.
}
}
7 changes: 7 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
use Illuminate\Support\Str;
use Laravel\Octane\Events\RequestTerminated;
use Laravel\Octane\Facades\Octane;
use LycheeVerify\Contract\VerifyFactory;
use LycheeVerify\Contract\VerifyInterface;
use LycheeVerify\DefaultVerifyFactory;
use LycheeVerify\Verify;
use Opcodes\LogViewer\Facades\LogViewer;
use Safe\Exceptions\StreamException;
Expand Down Expand Up @@ -190,6 +192,11 @@ public function register()
VerifyInterface::class,
Verify::class
);

$this->app->bind(
VerifyFactory::class,
DefaultVerifyFactory::class
);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
use App\Listeners\RecomputeAlbumSizeOnPhotoMutation;
use App\Listeners\RecomputeAlbumStatsOnAlbumChange;
use App\Listeners\RecomputeAlbumStatsOnPhotoChange;
use App\Listeners\RotateLicenseKeyOnLogin;
use App\Listeners\TaggedRouteCacheCleaner;
use App\Listeners\WebhookListener;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
Expand Down Expand Up @@ -124,6 +126,8 @@ public function boot(): void
Event::listen(AlbumSaved::class, RecomputeAlbumSizeOnAlbumChange::class . '@handleAlbumSaved');
Event::listen(AlbumDeleted::class, RecomputeAlbumSizeOnAlbumChange::class . '@handleAlbumDeleted');

Event::listen(Login::class, RotateLicenseKeyOnLogin::class . '@handle');

// Webhook dispatch for photo lifecycle events
Event::listen(PhotoAdded::class, WebhookListener::class . '@handlePhotoAdded');
Event::listen(PhotoMoved::class, WebhookListener::class . '@handlePhotoMoved');
Expand Down
Loading
Loading