Skip to content
Open
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
4 changes: 4 additions & 0 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
'getting_started_widget_intro' => 'Start building your new Statamic site with these steps.',
'getting_started_widget_navigation' => 'Create multi-level lists of links for navigation bars, footers, side menus, and other site areas.',
'getting_started_widget_pro' => 'Statamic Pro adds unlimited user accounts, roles, permissions, git integration, revisions, multi-site, and more!',
'enable_pro_license_required' => 'A Statamic Pro license is required. Enter your site license key below.',
'enable_pro_license_key_required' => 'Please enter your Statamic Pro license key.',
'enable_pro_license_key_invalid' => 'Invalid license key.',
'enable_pro_license_key_instructions' => 'Find your site license key at [statamic.com/account](https://statamic.com/account/sites).',
'git_disabled' => 'Statamic Git integration is disabled.',
'git_nothing_to_commit' => 'Nothing to commit.',
'git_utility_description' => 'View and commit changes to git-tracked content and assets.',
Expand Down
139 changes: 136 additions & 3 deletions resources/js/pages/Dashboard.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,96 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { router } from '@inertiajs/vue3';
import Head from '@/pages/layout/Head.vue';
import DynamicHtmlRenderer from '@/components/DynamicHtmlRenderer.vue';
import { Icon, EmptyStateMenu, EmptyStateItem, DocsCallout } from '@ui';
import { Icon, EmptyStateMenu, EmptyStateItem, DocsCallout, Modal, ModalClose, Button, Field, Input, ErrorMessage } from '@ui';
import useArchitecturalBackground from '@/pages/layout/architectural-background.js';

const props = defineProps({
widgets: Array,
pro: Boolean,
canEnablePro: Boolean,
hasLicenseKey: Boolean,
enableProUrl: String,
blueprintsUrl: String,
collectionsCreateUrl: String,
navigationCreateUrl: String,
});

if (props.widgets.length === 0) useArchitecturalBackground();

const confirmingEnablePro = ref(false);
const enablingPro = ref(false);
const licenseKey = ref('');
const licenseKeyError = ref(null);

watch(confirmingEnablePro, (open) => {
if (!open) {
licenseKey.value = '';
licenseKeyError.value = null;
}
});

const trimmedLicenseKey = computed(() => licenseKey.value.trim());
const hasValidEnteredKey = computed(() => /^[a-zA-Z0-9]{16}$/.test(trimmedLicenseKey.value));

watch(trimmedLicenseKey, (key) => {
if (!key || hasValidEnteredKey.value) {
licenseKeyError.value = null;
return;
}

if (/[^a-zA-Z0-9]/.test(key) || key.length > 16) {
licenseKeyError.value = __('statamic::messages.enable_pro_license_key_invalid');
}
});

function validateLicenseKey() {
const key = trimmedLicenseKey.value;

if (!key) {
if (props.hasLicenseKey) {
licenseKeyError.value = null;
return true;
}

licenseKeyError.value = __('statamic::messages.enable_pro_license_key_required');
return false;
}

if (!hasValidEnteredKey.value) {
licenseKeyError.value = __('statamic::messages.enable_pro_license_key_invalid');
return false;
}

licenseKeyError.value = null;
return true;
}

function enablePro() {
if (!validateLicenseKey()) {
return;
}

enablingPro.value = true;

router.post(props.enableProUrl, {
license_key: trimmedLicenseKey.value || null,
}, {
onSuccess: () => {
confirmingEnablePro.value = false;
},
onError: (errors) => {
licenseKeyError.value = Array.isArray(errors.license_key)
? errors.license_key[0]
: errors.license_key;
},
onFinish: () => {
enablingPro.value = false;
},
});
}

function classes(widget) {
return `${widget.classes} ${tailwindWidthClass(widget.width)}`;
}
Expand Down Expand Up @@ -79,11 +156,11 @@ function tailwindWidthClass(width) {
:description="__('statamic::messages.getting_started_widget_docs')"
/>
<EmptyStateItem
v-if="!pro"
href="https://statamic.dev/licensing"
v-if="!pro && canEnablePro"
icon="pro-ribbon"
:heading="__('Enable Pro Mode')"
:description="__('statamic::messages.getting_started_widget_pro')"
@click="confirmingEnablePro = true"
/>
<EmptyStateItem
:href="blueprintsUrl"
Expand All @@ -104,6 +181,62 @@ function tailwindWidthClass(width) {
:description="__('statamic::messages.getting_started_widget_navigation')"
/>
</EmptyStateMenu>

<Modal
v-model:open="confirmingEnablePro"
:title="__('Enable Statamic Pro')"
:dismissible="!enablingPro"
blur
>
<div class="space-y-4">
<p class="text-gray-700 dark:text-gray-200 antialiased">
{{ __('statamic::messages.enable_pro_license_required') }}
</p>

<div v-if="!hasLicenseKey" class="space-y-2" @keydown.enter="enablePro">
<Field
:instructions="__('statamic::messages.enable_pro_license_key_instructions')"
instructions-below
>
<Input
v-model="licenseKey"
name="license_key"
autocomplete="off"
spellcheck="false"
:placeholder="__('Enter License Key')"
:disabled="enablingPro"
/>
<ErrorMessage v-if="licenseKeyError" :text="licenseKeyError" class="mt-2" />
</Field>
</div>

<p
v-else
class="text-sm text-gray-600 dark:text-gray-400 antialiased"
>
{{ __('A license key is already configured for this site.') }}
</p>
</div>

<template #footer>
<div class="flex items-center justify-end gap-3 pt-3 pb-1">
<ModalClose asChild>
<Button
variant="ghost"
:disabled="enablingPro"
:text="__('Cancel')"
/>
</ModalClose>
<Button
variant="primary"
:disabled="enablingPro"
:loading="enablingPro"
:text="__('Enable Pro Mode')"
@click="enablePro"
/>
</div>
</template>
</Modal>
</template>

<DocsCallout :topic="__('Widgets')" url="widgets" />
Expand Down
1 change: 1 addition & 0 deletions routes/cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@

Route::get('/', StartPageController::class)->name('index');
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::post('dashboard/enable-pro', [DashboardController::class, 'enablePro'])->name('dashboard.enable-pro');

Route::get('select-site/{handle}', [SelectSiteController::class, 'select']);

Expand Down
48 changes: 45 additions & 3 deletions src/Console/Commands/ProEnable.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class ProEnable extends Command
*/
protected $signature = 'statamic:pro:enable
{ --force : Force the operation to run when in production }
{ --update-config : Also update editions config to reference .env var }';
{ --update-config : Also update editions config to reference .env var }
{ --license-key= : Set the Statamic license key in .env }';

/**
* The console command description.
Expand All @@ -40,7 +41,7 @@ public function handle()
}

$this->checkInfo('Statamic Pro successfully enabled in .env file!');
$this->promptToSetLicenseKey();
$this->setOrPromptLicenseKey();

if ($this->option('update-config') && $this->updateConfig()) {
$this->checkInfo('Statamic editions config successfully updated to reference .env var!');
Expand Down Expand Up @@ -112,14 +113,36 @@ protected function appendProToEnv()
file_put_contents($this->envPath(), $this->envContents()."\nSTATAMIC_PRO_ENABLED=true");
}

/**
* Set license key from option, or prompt interactively.
*
* @return void
*/
protected function setOrPromptLicenseKey()
{
if ($licenseKey = trim((string) $this->option('license-key'))) {
if (! preg_match('/^[a-zA-Z0-9]{16}$/', $licenseKey)) {
$this->crossLine('Invalid license key. Site keys are 16-character alphanumeric strings.');

return;
}

$this->setLicenseKey($licenseKey);

return;
}

$this->promptToSetLicenseKey();
}

/**
* Prompt to set the license key in the environment file.
*
* @return void
*/
protected function promptToSetLicenseKey()
{
if (! $this->input->isInteractive()) {
if (! $this->input->isInteractive() || ! defined('STDIN')) {
return;
}

Expand All @@ -135,12 +158,31 @@ protected function promptToSetLicenseKey()
return;
}

if (! preg_match('/^[a-zA-Z0-9]{16}$/', $licenseKey)) {
$this->crossLine('Invalid license key. Site keys are 16-character alphanumeric strings.');

return;
}

$this->setLicenseKey($licenseKey);
}

/**
* Set the license key in the environment file.
*
* @param string $licenseKey
* @return void
*/
protected function setLicenseKey($licenseKey)
{
if ($this->licenseKeyEnvVarExists()) {
$this->replaceLicenseKeyInEnv($licenseKey);
} else {
$this->appendLicenseKeyToEnv($licenseKey);
}

config()->set('statamic.system.license_key', $licenseKey);

$this->checkInfo('Statamic license key saved in .env file.');
}

Expand Down
64 changes: 64 additions & 0 deletions src/Http/Controllers/CP/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

namespace Statamic\Http\Controllers\CP;

use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Statamic\Exceptions\AuthorizationException;
use Statamic\Facades\Preference;
use Statamic\Facades\Site;
use Statamic\Facades\User;
use Statamic\Licensing\LicenseManager;
use Statamic\Statamic;
use Statamic\Support\Arr;
use Statamic\Widgets\Loader;

use function Statamic\trans as __;

class DashboardController extends CpController
{
/**
Expand All @@ -20,16 +26,74 @@ class DashboardController extends CpController
public function index(Loader $loader)
{
$widgets = $this->getDisplayableWidgets($loader);
$hasLicenseKey = filled(config('statamic.system.license_key'));

return Inertia::render('Dashboard', [
'widgets' => $widgets,
'pro' => Statamic::pro(),
'canEnablePro' => User::current()->isSuper(),
'hasLicenseKey' => $hasLicenseKey,
'enableProUrl' => cp_route('dashboard.enable-pro'),
'blueprintsUrl' => cp_route('blueprints.index'),
'collectionsCreateUrl' => cp_route('collections.create'),
'navigationCreateUrl' => cp_route('navigation.create'),
]);
}

public function enablePro(Request $request, LicenseManager $licenses)
{
if (! User::current()->isSuper()) {
throw new AuthorizationException;
}

$hasLicenseKey = filled(config('statamic.system.license_key'));
$licenseKey = trim((string) $request->input('license_key', ''));

$request->merge([
'license_key' => $licenseKey !== '' ? $licenseKey : null,
]);

$request->validate([
'license_key' => [
$hasLicenseKey ? 'nullable' : 'required',
'string',
'regex:/^[a-zA-Z0-9]{16}$/',
],
], [
'license_key.required' => __('statamic::messages.enable_pro_license_key_required'),
'license_key.regex' => __('statamic::messages.enable_pro_license_key_invalid'),
]);

if ($licenseKey !== '') {
$this->validateLicenseKeyWithOutpost($licenses, $licenseKey);
}

Statamic::enablePro($licenseKey !== '' ? $licenseKey : null);

return back()->withSuccess(__('Statamic Pro enabled'));
}

private function validateLicenseKeyWithOutpost(LicenseManager $licenses, string $licenseKey): void
{
config(['statamic.system.license_key' => $licenseKey]);

$licenses->refresh();

if ($licenses->outpostIsOffline() || $licenses->requestFailed()) {
throw ValidationException::withMessages([
'license_key' => $licenses->requestFailureMessage(),
]);
}

$reason = Arr::get($licenses->site()->response() ?? [], 'reason');

if ($reason === 'unknown_site' || $licenses->site()->response() === null) {
throw ValidationException::withMessages([
'license_key' => __('statamic::messages.enable_pro_license_key_invalid'),
]);
}
}

/**
* Get displayable widgets.
*
Expand Down
2 changes: 2 additions & 0 deletions src/Licensing/Outpost.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ private function getCachedResponse()

public function clearCachedResponse()
{
$this->response = null;

return $this->cache()->forget(self::CACHE_KEY);
}

Expand Down
Loading
Loading