Add admin page that can edit users
Add admin page that's only accessible to admins with a listing of users and the ability to edit the access types of those users.
This commit is contained in:
parent
3be7f8be05
commit
87525a6ef5
5 changed files with 170 additions and 0 deletions
|
@ -11,6 +11,9 @@
|
||||||
<li v-if="accountStore.canEdit">
|
<li v-if="accountStore.canEdit">
|
||||||
<NuxtLink to="/edit">Edit</NuxtLink>
|
<NuxtLink to="/edit">Edit</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="accountStore.isAdmin">
|
||||||
|
<NuxtLink to="/admin">Admin</NuxtLink>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="account">
|
<div class="account">
|
||||||
|
|
73
components/TableUsers.vue
Normal file
73
components/TableUsers.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in usersStore.users.values()">
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>
|
||||||
|
{{ user.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
v-if='user.type !== "anonymous"'
|
||||||
|
v-model="user.type"
|
||||||
|
>
|
||||||
|
<option value="regular">Regular</option>
|
||||||
|
<option value="crew">Crew</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
<template v-else>
|
||||||
|
{{ user.type }}
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ user.updatedAt.toFormat("yyyy-LL-dd HH:mm") }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
v-if="user.isModified()"
|
||||||
|
type="button"
|
||||||
|
@click="saveUser(user);"
|
||||||
|
>Save</button>
|
||||||
|
<button
|
||||||
|
v-if="user.isModified()"
|
||||||
|
type="button"
|
||||||
|
@click="user.discard()"
|
||||||
|
>Discard</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
useEventSource();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
|
|
||||||
|
async function saveUser(user: ClientUser) {
|
||||||
|
try {
|
||||||
|
await $fetch("/api/admin/user", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: user.toApi(),
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
alert(err?.data?.message ?? err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
14
pages/admin.vue
Normal file
14
pages/admin.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<h1>Admin</h1>
|
||||||
|
<h2>Users</h2>
|
||||||
|
<TableUsers />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
79
server/api/admin/user.patch.ts
Normal file
79
server/api/admin/user.patch.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { readUsers, ServerUser, writeUsers } from "~/server/database";
|
||||||
|
import { ApiUser, apiUserPatchSchema } from "~/shared/types/api";
|
||||||
|
import { z } from "zod/v4-mini";
|
||||||
|
import { broadcastEvent } from "~/server/streams";
|
||||||
|
|
||||||
|
function serverUserToApi(user: ServerUser): ApiUser {
|
||||||
|
if (user.deleted) {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
type: user.type,
|
||||||
|
name: user.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const session = await requireServerSession(event);
|
||||||
|
if (session.account.type !== "admin") {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Forbidden",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { success, error, data: patch } = apiUserPatchSchema.safeParse(await readBody(event));
|
||||||
|
if (!success) {
|
||||||
|
throw createError({
|
||||||
|
status: 400,
|
||||||
|
statusText: "Bad Request",
|
||||||
|
message: z.prettifyError(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await readUsers();
|
||||||
|
const user = users.find(user => user.id === patch.id);
|
||||||
|
if (!user || user.deleted) {
|
||||||
|
throw createError({
|
||||||
|
status: 409,
|
||||||
|
statusText: "Conflict",
|
||||||
|
message: "User does not exist",
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.type) {
|
||||||
|
if (patch.type === "anonymous" || user.type === "anonymous") {
|
||||||
|
throw createError({
|
||||||
|
status: 409,
|
||||||
|
statusText: "Conflict",
|
||||||
|
message: "Anonymous user type cannot be changed.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.type = patch.type;
|
||||||
|
}
|
||||||
|
if (patch.name) {
|
||||||
|
if (user.type === "anonymous") {
|
||||||
|
throw createError({
|
||||||
|
status: 409,
|
||||||
|
statusText: "Conflict",
|
||||||
|
message: "Anonymous user cannot have name set.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.name = patch.name;
|
||||||
|
}
|
||||||
|
user.updatedAt = new Date().toISOString();
|
||||||
|
await writeUsers(users);
|
||||||
|
broadcastEvent({
|
||||||
|
type: "user-update",
|
||||||
|
data: serverUserToApi(user),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update Schedule counts.
|
||||||
|
await updateScheduleInterestedCounts(users);
|
||||||
|
})
|
|
@ -27,6 +27,7 @@ export const useAccountStore = defineStore("account", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
|
isAdmin: computed(() => state.type.value === "admin"),
|
||||||
isCrew: computed(() => state.type.value === "crew" || state.type.value === "admin"),
|
isCrew: computed(() => state.type.value === "crew" || state.type.value === "admin"),
|
||||||
canEdit: computed(() => state.type.value === "admin" || state.type.value === "crew" ),
|
canEdit: computed(() => state.type.value === "admin" || state.type.value === "crew" ),
|
||||||
canEditPublic: computed(() => state.type.value === "admin"),
|
canEditPublic: computed(() => state.type.value === "admin"),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue