rustpbx 0.4.9

A SIP PBX implementation in Rust
Documentation
{% extends "console/layout.html" %}
{% block title %}{{ "licenses.title" | t }} · {{ site_name | default('RustPBX') }}{% endblock %}
{% block content %}
<div class="p-4 sm:p-6" x-data="licenseForm()">
    <div class="mx-auto max-w-7xl space-y-6">

        <!-- Page header -->
        <div class="flex items-center justify-between">
            <div>
                <h1 class="text-2xl font-semibold text-slate-900">{{ "licenses.title" | t }}</h1>
                <p class="text-sm text-slate-500 mt-1">
                    {{ "licenses.subtitle" | t }}
                </p>
            </div>
            <a href="https://miuda.ai/try" target="_blank"
                class="inline-flex items-center gap-2 rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-500 transition-colors">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
                    stroke="currentColor" class="size-4">
                    <path stroke-linecap="round" stroke-linejoin="round"
                        d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
                </svg>
                {{ "licenses.get_renew" | t }}
            </a>
        </div>

        <!-- Add / Update License form (always shown) -->
        <div class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5">
            <h2 class="text-sm font-semibold text-slate-800 mb-4 flex items-center gap-2">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
                    stroke="currentColor" class="size-4 text-slate-400">
                    <path stroke-linecap="round" stroke-linejoin="round"
                        d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
                </svg>
                {{ "licenses.add_update_license" | t }}
            </h2>
            <div>
                <label class="block text-xs font-medium text-slate-500 mb-1">{{ "licenses.license_key" | t }}</label>
                <div class="flex rounded-md shadow-sm">
                    <input type="text" x-model="licenseKey"
                        class="block w-full rounded-none rounded-l-md border border-slate-200 px-3 py-2 text-sm text-slate-700 placeholder-slate-400 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
                        placeholder="LICENSE-XXXX-XXXX-XXXX" @keydown.enter="verifyLicense">
                    <button type="button" @click="verifyLicense" :disabled="loading"
                        class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
                        <svg x-show="loading" class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg"
                            fill="none" viewBox="0 0 24 24">
                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
                            </circle>
                            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
                        </svg>
                        <span x-show="!loading">{{ "licenses.verify_save" | t }}</span>
                        <span x-show="loading" x-cloak>{{ "licenses.verifying" | t }}</span>
                    </button>
                </div>
            </div>

            <!-- Inline feedback -->
            <div x-show="error" x-cloak
                class="mt-3 rounded-md bg-red-50 border border-red-200 p-3 flex items-start gap-2">
                <svg class="h-4 w-4 text-red-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
                    <path fill-rule="evenodd"
                        d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
                        clip-rule="evenodd" />
                </svg>
                <p class="text-xs text-red-700" x-text="error"></p>
            </div>

            <div x-show="verifiedPlan" x-cloak class="mt-3 rounded-md bg-green-50 border border-green-200 p-3">
                <div class="flex items-start gap-2">
                    <svg class="h-4 w-4 text-green-600 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
                        <path fill-rule="evenodd"
                            d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
                            clip-rule="evenodd" />
                    </svg>
                    <div>
                        <p class="text-xs font-semibold text-green-800">{{ "licenses.license_verified" | t }}</p>
                        <p class="text-xs text-green-700 mt-0.5">
                            Plan: <span class="font-medium" x-text="verifiedPlan"></span>
                            <template x-if="verifiedExpiry">
                                <span> &mdash; valid until <span class="font-medium"
                                        x-text="verifiedExpiry"></span></span>
                            </template>
                            <template x-if="!verifiedExpiry">
                                <span> &mdash; <span class="font-medium">lifetime</span></span>
                            </template>
                        </p>
                        <template x-if="verifiedScope && verifiedScope.length > 0">
                            <p class="text-xs text-green-700 mt-0.5">
                                Covers: <span class="font-medium" x-text="verifiedScope.join(', ')"></span>
                            </p>
                        </template>
                        <template x-if="!verifiedScope || verifiedScope.length === 0">
                            <p class="text-xs text-green-700 mt-0.5">Covers: <span class="font-medium">all addons</span>
                            </p>
                        </template>
                    </div>
                </div>
                <p class="text-xs text-green-600 mt-2">{{ "licenses.license_active" | t }}</p>
            </div>
        </div>

        {% if licenses | length == 0 %}
        <!-- Empty state -->
        <div class="rounded-xl bg-white p-10 shadow-sm ring-1 ring-black/5 text-center">
            <div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-slate-100">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
                    stroke="currentColor" class="size-7 text-slate-400">
                    <path stroke-linecap="round" stroke-linejoin="round"
                        d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
                </svg>
            </div>
            <h3 class="mt-4 text-base font-semibold text-slate-900">{{ "licenses.no_licenses" | t }}</h3>
            <p class="mt-1 text-sm text-slate-500">
                Enter an addon ID and your license key above, or get one at miuda.ai.
            </p>
            <div class="mt-6 flex items-center justify-center gap-3">
                <a href="{{ bp }}/addons"
                    class="inline-flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 transition-colors">
                    {{ "licenses.go_to_addons" | t }}
                </a>
                <a href="https://miuda.ai/try" target="_blank"
                    class="inline-flex items-center gap-2 rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white hover:bg-sky-500 transition-colors">
                    {{ "licenses.get_license" | t }}
                </a>
            </div>
        </div>

        {% else %}
        <!-- License cards -->
        <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
            {% for lic in licenses %}
            <div class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5 flex flex-col gap-3">

                <!-- Header row: key name + status badge -->
                <div class="flex items-start justify-between gap-2">
                    <div class="min-w-0">
                        <p class="text-xs font-medium text-slate-500 uppercase tracking-wide">{{ "licenses.key" | t }}
                        </p>
                        <p class="mt-0.5 text-sm font-semibold text-slate-900 truncate font-mono">{{ lic.key_name }}</p>
                    </div>
                    {% if lic.status == "Valid" %}
                    <span
                        class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-semibold text-green-700 ring-1 ring-inset ring-green-600/20">
                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
                            <path fill-rule="evenodd"
                                d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
                                clip-rule="evenodd" />
                        </svg>
                        {{ "licenses.valid" | t }}
                    </span>
                    {% elif lic.status == "Expired" or lic.status == "Trial Expired" %}
                    <span
                        class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-red-50 px-2.5 py-0.5 text-xs font-semibold text-red-700 ring-1 ring-inset ring-red-600/20">
                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
                            <path fill-rule="evenodd"
                                d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
                                clip-rule="evenodd" />
                        </svg>
                        {{ lic.status }}
                    </span>
                    {% elif lic.is_trial %}
                    <span
                        class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-sky-50 px-2.5 py-0.5 text-xs font-semibold text-sky-700 ring-1 ring-inset ring-sky-600/20">
                        {{ lic.status }}
                    </span>
                    {% else %}
                    <span
                        class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-yellow-50 px-2.5 py-0.5 text-xs font-semibold text-yellow-700 ring-1 ring-inset ring-yellow-600/20">
                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
                            <path fill-rule="evenodd"
                                d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
                                clip-rule="evenodd" />
                        </svg>
                        {{ lic.status }}
                    </span>
                    {% endif %}
                </div>

                <!-- Plan + Expiry -->
                <dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
                    {% if lic.plan %}
                    <div>
                        <dt class="font-medium text-slate-500">{{ "licenses.plan" | t }}</dt>
                        <dd class="mt-0.5 text-slate-800 font-semibold">{{ lic.plan }}</dd>
                    </div>
                    {% endif %}
                    <div>
                        <dt class="font-medium text-slate-500">{{ "licenses.expires" | t }}</dt>
                        <dd class="mt-0.5 text-slate-800">{{ lic.expiry | default('Lifetime') }}</dd>
                    </div>
                </dl>

                <!-- Scope -->
                <div>
                    <p class="text-xs font-medium text-slate-500">{{ "licenses.covers" | t }}</p>
                    {% if lic.scope %}
                    <div class="mt-1 flex flex-wrap gap-1">
                        {% for addon_id in lic.scope %}
                        <span
                            class="inline-flex rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700">{{
                            addon_id }}</span>
                        {% endfor %}
                    </div>
                    {% else %}
                    <p class="mt-0.5 text-xs text-slate-600 font-medium">{{ "licenses.all_addons" | t }}</p>
                    {% endif %}
                </div>

                <!-- Assigned addons (from config) -->
                <div class="border-t border-slate-100 pt-3">
                    <p class="text-xs font-medium text-slate-500">{{ "licenses.assigned_to" | t }}</p>
                    <div class="mt-1 flex flex-wrap gap-1">
                        {% for addon_id in lic.addon_ids %}
                        <a href="{{ bp }}/addons/{{ addon_id }}"
                            class="inline-flex rounded-md bg-sky-50 px-2 py-0.5 text-xs font-medium text-sky-700 hover:bg-sky-100 transition-colors">
                            {{ addon_id }}
                        </a>
                        {% endfor %}
                    </div>
                </div>

            </div>
            {% endfor %}
        </div>

        <!-- Help box -->
        <div class="rounded-lg bg-slate-50 border border-slate-200 p-4 flex items-start gap-3">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
                stroke="currentColor" class="size-5 text-slate-400 flex-shrink-0 mt-0.5">
                <path stroke-linecap="round" stroke-linejoin="round"
                    d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
            </svg>
            <div class="text-xs text-slate-600">
                <p class="font-medium text-slate-700">{{ "licenses.how_to_configure" | t }}</p>
                <p class="mt-1">{{ "licenses.how_to_configure_desc" | t }}</p>
            </div>
        </div>

        {% endif %}
    </div>
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('licenseForm', () => ({
            licenseKey: '',
            error: '',
            verifiedPlan: '',
            verifiedExpiry: '',
            verifiedScope: null,
            loading: false,

            async verifyLicense() {
                this.error = '';
                this.verifiedPlan = '';
                this.verifiedExpiry = '';
                this.verifiedScope = null;
                const key = this.licenseKey.trim();
                if (!key) {
                    this.error = 'Please enter a license key.';
                    return;
                }
                this.loading = true;
                try {
                    const response = await fetch('{{ api_prefix | safe }}/licenses/verify', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ license_key: key })
                    });
                    const result = await response.json();
                    if (response.ok && result.success) {
                        this.verifiedPlan = result.plan || 'unknown';
                        this.verifiedExpiry = result.expiry || '';
                        this.verifiedScope = result.scope || null;
                    } else {
                        this.error = result.message || 'Verification failed.';
                    }
                } catch (e) {
                    this.error = 'Network error — please try again.';
                } finally {
                    this.loading = false;
                }
            }
        }))
    })
</script>
{% endblock %}