feat: multi-user UI login with admin and monitor roles

Add sac_users table, JWT roles, settings/users API guarded for admin, Users page in UI, and sac_manage_user.py CLI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-01 09:27:14 +10:00
parent bc77f82307
commit 56c469ddbd
20 changed files with 859 additions and 85 deletions
+2 -2
View File
@@ -25,7 +25,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { apiFetch, setToken, type TokenResponse } from "../api";
import { apiFetch, setAuth, type TokenResponse } from "../api";
const router = useRouter();
const username = ref("admin");
@@ -41,7 +41,7 @@ async function submit() {
method: "POST",
body: JSON.stringify({ username: username.value, password: password.value }),
});
setToken(data.access_token);
setAuth(data.access_token, data.role);
await router.push("/events");
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка входа";
+181
View File
@@ -0,0 +1,181 @@
<template>
<div class="users-page">
<h1>Пользователи</h1>
<p class="users-intro">
Учётные записи входа в веб-панель SAC. Роль <strong>monitor</strong> просмотр без раздела «Настройки».
</p>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="success" class="success">{{ success }}</p>
<section class="card users-card">
<h2>Новый пользователь</h2>
<form class="users-form" @submit.prevent="createUser">
<label>
Логин
<input v-model="form.username" required minlength="2" autocomplete="off" />
</label>
<label>
Пароль
<input v-model="form.password" type="password" required minlength="8" autocomplete="new-password" />
</label>
<label>
Роль
<select v-model="form.role">
<option value="monitor">monitor просмотр</option>
<option value="admin">admin полный доступ</option>
</select>
</label>
<button type="submit" :disabled="creating">{{ creating ? "Создание…" : "Создать" }}</button>
</form>
</section>
<section class="card users-card">
<h2>Список</h2>
<p v-if="loading">Загрузка</p>
<table v-else class="users-table">
<thead>
<tr>
<th>Логин</th>
<th>Роль</th>
<th>Статус</th>
<th>Создан</th>
</tr>
</thead>
<tbody>
<tr v-for="u in users" :key="u.id">
<td>{{ u.username }}</td>
<td><code>{{ u.role }}</code></td>
<td>{{ u.is_active ? "активен" : "отключён" }}</td>
<td>{{ formatDate(u.created_at) }}</td>
</tr>
</tbody>
</table>
</section>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import { apiFetch } from "../api";
interface UserSummary {
id: number;
username: string;
role: string;
is_active: boolean;
created_at: string;
}
interface UserListResponse {
items: UserSummary[];
}
const users = ref<UserSummary[]>([]);
const loading = ref(true);
const creating = ref(false);
const error = ref("");
const success = ref("");
const form = reactive({
username: "",
password: "",
role: "monitor",
});
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleString();
} catch {
return iso;
}
}
async function loadUsers() {
loading.value = true;
error.value = "";
try {
const data = await apiFetch<UserListResponse>("/api/v1/users");
users.value = data.items;
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка загрузки";
} finally {
loading.value = false;
}
}
async function createUser() {
creating.value = true;
error.value = "";
success.value = "";
try {
await apiFetch<UserSummary>("/api/v1/users", {
method: "POST",
body: JSON.stringify({
username: form.username.trim(),
password: form.password,
role: form.role,
}),
});
success.value = `Пользователь ${form.username} создан`;
form.username = "";
form.password = "";
form.role = "monitor";
await loadUsers();
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка создания";
} finally {
creating.value = false;
}
}
onMounted(loadUsers);
</script>
<style scoped>
.users-page {
max-width: 52rem;
}
.users-intro {
color: #9aa4b2;
margin-bottom: 1rem;
}
.users-card {
margin-bottom: 1.25rem;
}
.users-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 24rem;
}
.users-form label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.users-table {
width: 100%;
border-collapse: collapse;
}
.users-table th,
.users-table td {
text-align: left;
padding: 0.45rem 0.5rem;
border-bottom: 1px solid #2a3441;
}
.error {
color: #ff8a8a;
}
.success {
color: #7dcea0;
}
</style>