API keys control
This commit is contained in:
@@ -209,6 +209,50 @@
|
||||
</div>
|
||||
<div v-if="devices.length === 0" class="text-center py-10 text-slate-600 text-sm">Устройства не найдены. Нажмите "Сканировать".</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -235,6 +279,7 @@
|
||||
newTask: { target_id: '', state: true },
|
||||
tasks: [], allScenes: {},
|
||||
toasts: [], toastCounter: 0,
|
||||
apiKeys: [], newKeyName: '', newKeyAdmin: false, lastCreatedKey: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -289,6 +334,7 @@
|
||||
if (dData) this.devices = Array.isArray(dData) ? dData : Object.values(dData);
|
||||
if (sData) this.allScenes = sData;
|
||||
if (this.isAdmin) this.fetchTasks();
|
||||
if (this.isAdmin) this.fetchApiKeys();
|
||||
} 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 });
|
||||
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(); }
|
||||
}).mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user