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:
@@ -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 : "Ошибка входа";
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user