API keys control

This commit is contained in:
Artem Kokos
2026-03-28 21:30:22 +07:00
parent 3d8939a6aa
commit b84fac66e8

View File

@@ -209,6 +209,50 @@
</div> </div>
<div v-if="devices.length === 0" class="text-center py-10 text-slate-600 text-sm">Устройства не найдены. Нажмите "Сканировать".</div> <div v-if="devices.length === 0" class="text-center py-10 text-slate-600 text-sm">Устройства не найдены. Нажмите "Сканировать".</div>
</section> </section>
<!-- Гостевые API-ключи -->
<section class="glass p-6 rounded-2xl">
<h2 class="text-lg font-black mb-6 uppercase">Гостевые ключи</h2>
<!-- Создание -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
<input v-model="newKeyName" placeholder="Имя (Вася, гости...)" class="bg-black/30 border border-slate-700/50 p-3 rounded-xl focus:border-orange-500 outline-none text-sm">
<label class="flex items-center gap-2 bg-black/30 border border-slate-700/50 p-3 rounded-xl text-sm text-slate-400 cursor-pointer">
<input type="checkbox" v-model="newKeyAdmin" class="w-4 h-4 accent-orange-500 rounded">
Админ-права
</label>
<button @click="createApiKey" :disabled="!newKeyName.trim()" class="bg-orange-600 hover:bg-orange-500 disabled:opacity-30 disabled:cursor-not-allowed rounded-xl font-bold transition-all text-sm active:scale-[0.97]">СОЗДАТЬ КЛЮЧ</button>
</div>
<!-- Только что созданный ключ -->
<div v-if="lastCreatedKey" class="bg-green-900/20 border border-green-500/30 p-4 rounded-xl mb-6">
<div class="flex justify-between items-center mb-2">
<span class="text-green-400 text-xs font-bold uppercase">Новый ключ создан -- скопируйте!</span>
<button @click="copyKey(lastCreatedKey)" class="text-green-400 hover:text-green-300 text-xs font-bold transition-colors">КОПИРОВАТЬ</button>
</div>
<div class="mono text-sm text-green-300 break-all select-all bg-black/30 p-3 rounded-lg">{{ lastCreatedKey }}</div>
</div>
<!-- Список ключей -->
<div class="space-y-2">
<div v-for="k in apiKeys" :key="k.key" class="bg-black/20 border border-slate-800/50 p-4 rounded-xl flex justify-between items-center group hover:border-slate-700 transition-all">
<div>
<div class="flex items-center gap-2">
<span class="font-bold text-sm">{{ k.name }}</span>
<span v-if="k.is_admin" class="text-[9px] text-orange-400 font-bold uppercase bg-orange-400/10 px-2 py-0.5 rounded">админ</span>
<span v-else class="text-[9px] text-slate-500 font-bold uppercase bg-slate-500/10 px-2 py-0.5 rounded">гость</span>
<span :class="k.active ? 'text-green-500' : 'text-red-500'" class="text-[9px] font-bold uppercase">{{ k.active ? 'активен' : 'отозван' }}</span>
</div>
<div class="mono text-[10px] text-slate-600 mt-1">{{ k.key.slice(0, 12) }}...{{ k.key.slice(-6) }}</div>
</div>
<div class="flex gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button v-if="k.active" @click="revokeApiKey(k.key, k.name)" class="p-2 rounded-lg bg-red-900/20 hover:bg-red-900/40 text-red-400 text-xs font-bold transition-all" title="Отозвать">ОТОЗВАТЬ</button>
<button v-else @click="activateApiKey(k.key, k.name)" class="p-2 rounded-lg bg-green-900/20 hover:bg-green-900/40 text-green-400 text-xs font-bold transition-all" title="Активировать">АКТИВИР.</button>
</div>
</div>
<div v-if="apiKeys.length === 0" class="text-center py-8 text-slate-600 text-sm">Гостевых ключей нет</div>
</div>
</section>
</div> </div>
</template> </template>
@@ -235,6 +279,7 @@
newTask: { target_id: '', state: true }, newTask: { target_id: '', state: true },
tasks: [], allScenes: {}, tasks: [], allScenes: {},
toasts: [], toastCounter: 0, toasts: [], toastCounter: 0,
apiKeys: [], newKeyName: '', newKeyAdmin: false, lastCreatedKey: '',
} }
}, },
methods: { methods: {
@@ -289,6 +334,7 @@
if (dData) this.devices = Array.isArray(dData) ? dData : Object.values(dData); if (dData) this.devices = Array.isArray(dData) ? dData : Object.values(dData);
if (sData) this.allScenes = sData; if (sData) this.allScenes = sData;
if (this.isAdmin) this.fetchTasks(); if (this.isAdmin) this.fetchTasks();
if (this.isAdmin) this.fetchApiKeys();
} finally { this.isFetching = false; } } finally { this.isFetching = false; }
}, },
@@ -340,6 +386,39 @@
const res = await this.request('/schedules/once', 'POST', { target_id: id, hours_from_now: 4, is_group: true, state: false }); const res = await this.request('/schedules/once', 'POST', { target_id: id, hours_from_now: 4, is_group: true, state: false });
if (res) { this.toast('Таймер 4ч', 'success'); this.fetchTasks(); } if (res) { this.toast('Таймер 4ч', 'success'); this.fetchTasks(); }
}, },
// ─── API-ключи ───────────────────────────────
async fetchApiKeys() {
const data = await this.request('/api-keys');
if (data) this.apiKeys = data;
},
async createApiKey() {
const name = this.newKeyName.trim();
if (!name) return;
const res = await this.request('/api-keys', 'POST', { name, is_admin: this.newKeyAdmin });
if (res) {
this.lastCreatedKey = res.key;
this.newKeyName = '';
this.newKeyAdmin = false;
this.toast(`Ключ "${name}" создан`, 'success');
this.fetchApiKeys();
}
},
async revokeApiKey(key, name) {
if (confirm(`Отозвать ключ "${name}"?`)) {
await this.request(`/api-keys/${key}`, 'DELETE');
this.toast(`Ключ "${name}" отозван`, 'success');
this.fetchApiKeys();
}
},
async activateApiKey(key, name) {
await this.request(`/api-keys/${key}/activate`, 'POST');
this.toast(`Ключ "${name}" активирован`, 'success');
this.fetchApiKeys();
},
copyKey(key) {
navigator.clipboard.writeText(key).then(() => this.toast('Скопировано', 'success'));
},
}, },
async mounted() { if (this.apiKey) await this.initApp(); } async mounted() { if (this.apiKey) await this.initApp(); }
}).mount('#app') }).mount('#app')