diff --git a/components/Header.vue b/components/Header.vue
index e4ff675..9096da8 100644
--- a/components/Header.vue
+++ b/components/Header.vue
@@ -11,6 +11,9 @@
diff --git a/components/TableUsers.vue b/components/TableUsers.vue
new file mode 100644
index 0000000..78c33d4
--- /dev/null
+++ b/components/TableUsers.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ Id |
+ Name |
+ Type |
+ Last Update |
+ |
+
+
+
+
+ {{ user.id }} |
+
+ {{ user.name }}
+ |
+
+
+
+ {{ user.type }}
+
+ |
+
+ {{ user.updatedAt.toFormat("yyyy-LL-dd HH:mm") }}
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
diff --git a/pages/admin.vue b/pages/admin.vue
new file mode 100644
index 0000000..44531fd
--- /dev/null
+++ b/pages/admin.vue
@@ -0,0 +1,14 @@
+
+
+ Admin
+ Users
+
+
+
+
+
+
+
diff --git a/server/api/admin/user.patch.ts b/server/api/admin/user.patch.ts
new file mode 100644
index 0000000..dd3691d
--- /dev/null
+++ b/server/api/admin/user.patch.ts
@@ -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);
+})
diff --git a/stores/account.ts b/stores/account.ts
index bf92f77..d1a80e1 100644
--- a/stores/account.ts
+++ b/stores/account.ts
@@ -27,6 +27,7 @@ export const useAccountStore = defineStore("account", () => {
});
const getters = {
+ isAdmin: computed(() => state.type.value === "admin"),
isCrew: computed(() => state.type.value === "crew" || state.type.value === "admin"),
canEdit: computed(() => state.type.value === "admin" || state.type.value === "crew" ),
canEditPublic: computed(() => state.type.value === "admin"),