API keys control
This commit is contained in:
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user