Refactor user storage and update
Rename accounts to users to be consistent with the new naming scheme where account only referes to the logged in user of the session and implement live updates of users via a user store which listens for updates from the event stream.
This commit is contained in:
parent
6336ccdb96
commit
3be7f8be05
24 changed files with 331 additions and 182 deletions
|
@ -24,18 +24,18 @@
|
||||||
defineProps<{
|
defineProps<{
|
||||||
edit?: boolean
|
edit?: boolean
|
||||||
}>();
|
}>();
|
||||||
const { data: accounts } = useAccounts();
|
const usersStore = useUsersStore();
|
||||||
const accountsById = computed(() => new Map(accounts.value?.map?.(a => [a.id, a])));
|
await usersStore.fetch();
|
||||||
const assignedIds = defineModel<Set<number>>({ required: true });
|
const assignedIds = defineModel<Set<number>>({ required: true });
|
||||||
const assigned = computed(
|
const assigned = computed(
|
||||||
() => [...assignedIds.value].map(
|
() => [...assignedIds.value].map(
|
||||||
id => accountsById.value.get(id) ?? { id, name: String(id) }
|
id => usersStore.users.get(id) ?? { id, name: String(id) }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const crewByName = computed(() => new Map(
|
const crewByName = computed(() => new Map(
|
||||||
accounts.value
|
[...usersStore.users.values()]
|
||||||
?.filter?.(a => a.type === "crew" || a.type === "admin")
|
.filter(a => a.type === "crew" || a.type === "admin")
|
||||||
?.map?.(a => [a.name, a])
|
.map(a => [a.name!, a])
|
||||||
));
|
));
|
||||||
const addName = ref("");
|
const addName = ref("");
|
||||||
function addCrew() {
|
function addCrew() {
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</template>
|
</template>
|
||||||
<p v-if="slot.assigned">
|
<p v-if="slot.assigned">
|
||||||
Crew:
|
Crew:
|
||||||
{{ [...slot.assigned].map(id => idToAccount.get(id)?.name).join(", ") }}
|
{{ [...slot.assigned].map(id => usersStore.users.get(id)?.name).join(", ") }}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -48,8 +48,8 @@ defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const { data: accounts } = await useAccounts();
|
const usersStore = useUsersStore();
|
||||||
const idToAccount = computed(() => new Map(accounts.value?.map(a => [a.id, a])));
|
await usersStore.fetch();
|
||||||
|
|
||||||
function formatTime(time: DateTime) {
|
function formatTime(time: DateTime) {
|
||||||
return time.toFormat("yyyy-LL-dd HH:mm");
|
return time.toFormat("yyyy-LL-dd HH:mm");
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export const useAccounts = () => useFetch(
|
|
||||||
"/api/accounts",
|
|
||||||
{
|
|
||||||
transform: (input) => input === undefined ? false as any as null: input,
|
|
||||||
getCachedData(key, nuxtApp) {
|
|
||||||
return nuxtApp.payload.data[key];
|
|
||||||
},
|
|
||||||
dedupe: "defer",
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -11,11 +11,11 @@
|
||||||
:selected="crewFilter === undefined"
|
:selected="crewFilter === undefined"
|
||||||
><All Crew></option>
|
><All Crew></option>
|
||||||
<option
|
<option
|
||||||
v-for="account in accounts?.filter(a => a.type === 'crew' || a.type === 'admin')"
|
v-for="user in [...usersStore.users.values()].filter(a => a.type === 'crew' || a.type === 'admin')"
|
||||||
:key="account.id"
|
:key="user.id"
|
||||||
:value="String(account.id)"
|
:value="String(user.id)"
|
||||||
:selected="crewFilter === String(account.id)"
|
:selected="crewFilter === String(user.id)"
|
||||||
>{{ account.name }}</option>
|
>{{ user.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<Timetable :schedule :eventSlotFilter :shiftSlotFilter />
|
<Timetable :schedule :eventSlotFilter :shiftSlotFilter />
|
||||||
|
@ -104,7 +104,8 @@ const tabs = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const schedule = await useSchedule();
|
const schedule = await useSchedule();
|
||||||
const { data: accounts } = await useAccounts();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
|
@ -30,13 +30,13 @@
|
||||||
value="assigned"
|
value="assigned"
|
||||||
:selected='filter === "assigned"'
|
:selected='filter === "assigned"'
|
||||||
>Assigned to Me</option>
|
>Assigned to Me</option>
|
||||||
<optgroup v-if="accountStore.isCrew && accounts" label="Crew">
|
<optgroup v-if="accountStore.isCrew" label="Crew">
|
||||||
<option
|
<option
|
||||||
v-for="account in accounts.filter(a => a.type === 'crew' || a.type === 'admin')"
|
v-for="user in [...usersStore.users.values()].filter(a => a.type === 'crew' || a.type === 'admin')"
|
||||||
:key="account.id"
|
:key="user.id"
|
||||||
:value="`crew-${account.id}`"
|
:value="`crew-${user.id}`"
|
||||||
:selected="filter === `crew-${account.id}`"
|
:selected="filter === `crew-${user.id}`"
|
||||||
>{{ account.name }}</option>
|
>{{ user.name }}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
@ -55,7 +55,8 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const { data: accounts } = await useAccounts();
|
const usersStore = useUsersStore();
|
||||||
|
await usersStore.fetch();
|
||||||
const schedule = await useSchedule();
|
const schedule = await useSchedule();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { readAccounts } from "~/server/database"
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const session = await requireServerSession(event);
|
|
||||||
const accounts = await readAccounts();
|
|
||||||
const account = accounts.find(a => a.id === session.accountId);
|
|
||||||
if (!account) {
|
|
||||||
throw new Error("Account does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.type === "admin") {
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
if (account.type === "crew") {
|
|
||||||
return accounts.filter(a => a.type === "crew" || a.type === "admin");
|
|
||||||
}
|
|
||||||
throw createError({
|
|
||||||
status: 403,
|
|
||||||
statusText: "Forbidden",
|
|
||||||
message: "You do not have permission to list accounts",
|
|
||||||
});
|
|
||||||
})
|
|
|
@ -1,15 +1,8 @@
|
||||||
import { deleteDatbase, readAccounts } from "~/server/database";
|
import { deleteDatbase } from "~/server/database";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await requireServerSession(event);
|
const session = await requireServerSession(event);
|
||||||
let accounts = await readAccounts();
|
if (session.account.type !== "admin") {
|
||||||
const sessionAccount = accounts.find(
|
|
||||||
account => account.id === session.accountId
|
|
||||||
);
|
|
||||||
if (!sessionAccount) {
|
|
||||||
throw Error("Account does not exist");
|
|
||||||
}
|
|
||||||
if (sessionAccount.type !== "admin") {
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
statusMessage: "Forbidden",
|
statusMessage: "Forbidden",
|
||||||
|
|
|
@ -1,45 +1,49 @@
|
||||||
import {
|
import {
|
||||||
readAccounts, readSessions, readSubscriptions,
|
readUsers, readSessions, readSubscriptions,
|
||||||
writeAccounts, writeSessions, writeSubscriptions,
|
writeUsers, writeSessions, writeSubscriptions,
|
||||||
} from "~/server/database";
|
} from "~/server/database";
|
||||||
import { cancelAccountStreams } from "~/server/streams";
|
import { broadcastEvent, cancelAccountStreams } from "~/server/streams";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const serverSession = await requireServerSession(event);
|
const serverSession = await requireServerSession(event);
|
||||||
|
let users = await readUsers();
|
||||||
|
|
||||||
let accounts = await readAccounts();
|
// Remove sessions for this user
|
||||||
const sessionAccount = accounts.find(
|
|
||||||
account => account.id === serverSession.accountId
|
|
||||||
);
|
|
||||||
if (!sessionAccount) {
|
|
||||||
throw Error("Account does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove sessions for this account
|
|
||||||
const removedSessionIds = new Set<number>();
|
const removedSessionIds = new Set<number>();
|
||||||
let sessions = await readSessions();
|
let sessions = await readSessions();
|
||||||
sessions = sessions.filter(session => {
|
sessions = sessions.filter(session => {
|
||||||
if (session.accountId === serverSession.accountId) {
|
if (session.account.id === serverSession.account.id) {
|
||||||
removedSessionIds.add(session.id);
|
removedSessionIds.add(session.id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
cancelAccountStreams(serverSession.accountId);
|
cancelAccountStreams(serverSession.account.id);
|
||||||
await writeSessions(sessions);
|
await writeSessions(sessions);
|
||||||
await deleteCookie(event, "session");
|
await deleteCookie(event, "session");
|
||||||
|
|
||||||
// Remove subscriptions for this account
|
// Remove subscriptions for this user
|
||||||
let subscriptions = await readSubscriptions();
|
let subscriptions = await readSubscriptions();
|
||||||
subscriptions = subscriptions.filter(
|
subscriptions = subscriptions.filter(
|
||||||
subscription => !removedSessionIds.has(subscription.sessionId)
|
subscription => !removedSessionIds.has(subscription.sessionId)
|
||||||
);
|
);
|
||||||
await writeSubscriptions(subscriptions);
|
await writeSubscriptions(subscriptions);
|
||||||
|
|
||||||
// Remove the account
|
// Remove the user
|
||||||
accounts = accounts.filter(account => account.id !== serverSession.accountId);
|
const account = users.find(user => user.id === serverSession.account.id)!;
|
||||||
await writeAccounts(accounts);
|
const now = new Date().toISOString();
|
||||||
|
account.deleted = true;
|
||||||
|
account.updatedAt = now;
|
||||||
|
await writeUsers(users);
|
||||||
|
await broadcastEvent({
|
||||||
|
type: "user-update",
|
||||||
|
data: {
|
||||||
|
id: account.id,
|
||||||
|
updatedAt: now,
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update Schedule counts.
|
// Update Schedule counts.
|
||||||
await updateScheduleInterestedCounts(accounts);
|
await updateScheduleInterestedCounts(users);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { readAccounts, writeAccounts } from "~/server/database";
|
import { readUsers, writeUsers } from "~/server/database";
|
||||||
import { DateTime, Info } from "~/shared/utils/luxon";
|
import { DateTime, Info } from "~/shared/utils/luxon";
|
||||||
import { apiAccountPatchSchema } from "~/shared/types/api";
|
import { apiAccountPatchSchema } from "~/shared/types/api";
|
||||||
import { z } from "zod/v4-mini";
|
import { z } from "zod/v4-mini";
|
||||||
|
@ -34,40 +34,40 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await readAccounts();
|
const users = await readUsers();
|
||||||
const sessionAccount = accounts.find(account => account.id === session.accountId);
|
const account = users.find(user => user.id === session.account.id);
|
||||||
if (!sessionAccount) {
|
if (!account) {
|
||||||
throw Error("Account does not exist");
|
throw Error("Account does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patch.interestedEventIds !== undefined) {
|
if (patch.interestedEventIds !== undefined) {
|
||||||
if (patch.interestedEventIds.length) {
|
if (patch.interestedEventIds.length) {
|
||||||
sessionAccount.interestedEventIds = patch.interestedEventIds;
|
account.interestedEventIds = patch.interestedEventIds;
|
||||||
} else {
|
} else {
|
||||||
delete sessionAccount.interestedEventIds;
|
delete account.interestedEventIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (patch.interestedEventSlotIds !== undefined) {
|
if (patch.interestedEventSlotIds !== undefined) {
|
||||||
if (patch.interestedEventSlotIds.length) {
|
if (patch.interestedEventSlotIds.length) {
|
||||||
sessionAccount.interestedEventSlotIds = patch.interestedEventSlotIds;
|
account.interestedEventSlotIds = patch.interestedEventSlotIds;
|
||||||
} else {
|
} else {
|
||||||
delete sessionAccount.interestedEventSlotIds;
|
delete account.interestedEventSlotIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (patch.timezone !== undefined) {
|
if (patch.timezone !== undefined) {
|
||||||
if (patch.timezone)
|
if (patch.timezone)
|
||||||
sessionAccount.timezone = patch.timezone;
|
account.timezone = patch.timezone;
|
||||||
else
|
else
|
||||||
delete sessionAccount.timezone;
|
delete account.timezone;
|
||||||
}
|
}
|
||||||
if (patch.locale !== undefined) {
|
if (patch.locale !== undefined) {
|
||||||
if (patch.locale)
|
if (patch.locale)
|
||||||
sessionAccount.locale = patch.locale;
|
account.locale = patch.locale;
|
||||||
else
|
else
|
||||||
delete sessionAccount.locale;
|
delete account.locale;
|
||||||
}
|
}
|
||||||
await writeAccounts(accounts);
|
await writeUsers(users);
|
||||||
|
|
||||||
// Update Schedule counts.
|
// Update Schedule counts.
|
||||||
await updateScheduleInterestedCounts(accounts);
|
await updateScheduleInterestedCounts(users);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { readAccounts, writeAccounts, nextAccountId } from "~/server/database";
|
import { readUsers, writeUsers, nextUserId, type ServerUser } from "~/server/database";
|
||||||
import type { ApiAccount } from "~/shared/types/api";
|
import { broadcastEvent } from "~/server/streams";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
let session = await getServerSession(event);
|
let session = await getServerSession(event);
|
||||||
|
@ -13,8 +13,8 @@ export default defineEventHandler(async (event) => {
|
||||||
const formData = await readFormData(event);
|
const formData = await readFormData(event);
|
||||||
const name = formData.get("name");
|
const name = formData.get("name");
|
||||||
|
|
||||||
const accounts = await readAccounts();
|
const users = await readUsers();
|
||||||
let account: ApiAccount;
|
let user: ServerUser;
|
||||||
if (typeof name === "string") {
|
if (typeof name === "string") {
|
||||||
if (name === "") {
|
if (name === "") {
|
||||||
throw createError({
|
throw createError({
|
||||||
|
@ -22,22 +22,24 @@ export default defineEventHandler(async (event) => {
|
||||||
message: "Name cannot be blank",
|
message: "Name cannot be blank",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (accounts.some(account => account.name && account.name.toLowerCase() === name.toLowerCase())) {
|
if (users.some(user => user.name && user.name.toLowerCase() === name.toLowerCase())) {
|
||||||
throw createError({
|
throw createError({
|
||||||
status: 409,
|
status: 409,
|
||||||
message: "User already exists",
|
message: "User already exists",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
account = {
|
user = {
|
||||||
id: await nextAccountId(),
|
id: await nextUserId(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
type: "regular",
|
type: "regular",
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (name === null) {
|
} else if (name === null) {
|
||||||
account = {
|
user = {
|
||||||
id: await nextAccountId(),
|
id: await nextUserId(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
type: "anonymous",
|
type: "anonymous",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,7 +49,11 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts.push(account);
|
users.push(user);
|
||||||
await writeAccounts(accounts);
|
await writeUsers(users);
|
||||||
await setServerSession(event, account.id);
|
await broadcastEvent({
|
||||||
|
type: "user-update",
|
||||||
|
data: user,
|
||||||
|
});
|
||||||
|
await setServerSession(event, user);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { readAccounts } from "~/server/database";
|
import { readUsers } from "~/server/database";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { name } = await readBody(event);
|
const { name } = await readBody(event);
|
||||||
|
@ -7,12 +7,12 @@ export default defineEventHandler(async (event) => {
|
||||||
return new Response(undefined, { status: 400 })
|
return new Response(undefined, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await readAccounts();
|
const accounts = await readUsers();
|
||||||
const account = accounts.find(a => a.name === name);
|
const account = accounts.find(a => a.name === name);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return new Response(undefined, { status: 403 })
|
return new Response(undefined, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await setServerSession(event, account.id);
|
await setServerSession(event, account);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { readAccounts } from "~/server/database";
|
|
||||||
import { cancelSessionStreams } from "~/server/streams";
|
import { cancelSessionStreams } from "~/server/streams";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await getServerSession(event);
|
const session = await getServerSession(event);
|
||||||
if (session) {
|
if (session) {
|
||||||
const accounts = await readAccounts();
|
if (session.account.type === "anonymous") {
|
||||||
const account = accounts.find(
|
|
||||||
account => account.id === session.accountId
|
|
||||||
);
|
|
||||||
if (account && account.type === "anonymous") {
|
|
||||||
throw createError({
|
throw createError({
|
||||||
status: 409,
|
status: 409,
|
||||||
message: "Cannot log out of an anonymous account",
|
message: "Cannot log out of an anonymous account",
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { readAccounts, readSubscriptions } from "~/server/database";
|
import { readSubscriptions } from "~/server/database";
|
||||||
import type { ApiSession } from "~/shared/types/api";
|
import type { ApiSession } from "~/shared/types/api";
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<ApiSession | undefined> => {
|
export default defineEventHandler(async (event): Promise<ApiSession | undefined> => {
|
||||||
const session = await getServerSession(event);
|
const session = await getServerSession(event);
|
||||||
if (!session)
|
if (!session)
|
||||||
return;
|
return;
|
||||||
const accounts = await readAccounts();
|
|
||||||
const subscriptions = await readSubscriptions();
|
const subscriptions = await readSubscriptions();
|
||||||
const push = Boolean(
|
const push = Boolean(
|
||||||
subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id)
|
subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id)
|
||||||
|
@ -15,7 +14,7 @@ export default defineEventHandler(async (event): Promise<ApiSession | undefined>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
account: accounts.find(account => account.id === session.accountId)!,
|
account: session.account,
|
||||||
push,
|
push,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import { pipeline } from "node:stream";
|
import { pipeline } from "node:stream";
|
||||||
import { addStream, deleteStream } from "~/server/streams";
|
import { addStream, deleteStream } from "~/server/streams";
|
||||||
import { readAccounts } from "~/server/database";
|
import { readUsers } from "~/server/database";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await getServerSession(event);
|
const session = await getServerSession(event);
|
||||||
let accountId: number | undefined;
|
const accountId = session?.account.id;
|
||||||
if (session) {
|
|
||||||
const accounts = await readAccounts()
|
|
||||||
accountId = accounts.find(account => account.id === session.accountId)?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const source = event.headers.get("x-forwarded-for");
|
const source = event.headers.get("x-forwarded-for");
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import { z } from "zod/v4-mini";
|
import { z } from "zod/v4-mini";
|
||||||
import { readAccounts, readSchedule, writeSchedule } from "~/server/database";
|
import { readUsers, readSchedule, writeSchedule } from "~/server/database";
|
||||||
import { broadcastEvent } from "~/server/streams";
|
import { broadcastEvent } from "~/server/streams";
|
||||||
import { apiScheduleSchema } from "~/shared/types/api";
|
import { apiScheduleSchema } from "~/shared/types/api";
|
||||||
import { applyUpdatesToArray } from "~/shared/utils/update";
|
import { applyUpdatesToArray } from "~/shared/utils/update";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await requireServerSession(event);
|
const session = await requireServerSession(event);
|
||||||
const accounts = await readAccounts();
|
|
||||||
const account = accounts.find(a => a.id === session.accountId);
|
|
||||||
if (!account) {
|
|
||||||
throw new Error("Account does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.type !== "admin" && account.type !== "crew") {
|
if (session.account.type !== "admin" && session.account.type !== "crew") {
|
||||||
throw createError({
|
throw createError({
|
||||||
status: 403,
|
status: 403,
|
||||||
statusMessage: "Forbidden",
|
statusMessage: "Forbidden",
|
||||||
|
@ -46,7 +41,7 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate edit restrictions for crew
|
// Validate edit restrictions for crew
|
||||||
if (account.type === "crew") {
|
if (session.account.type === "crew") {
|
||||||
if (update.locations?.length) {
|
if (update.locations?.length) {
|
||||||
throw createError({
|
throw createError({
|
||||||
status: 403,
|
status: 403,
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { readAccounts, readSchedule } from "~/server/database";
|
import { readUsers, readSchedule } from "~/server/database";
|
||||||
import type { ApiAccount } from "~/shared/types/api";
|
import type { ApiAccount } from "~/shared/types/api";
|
||||||
import { canSeeCrew } from "../utils/schedule";
|
import { canSeeCrew } from "../utils/schedule";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await getServerSession(event);
|
const session = await getServerSession(event);
|
||||||
let account: ApiAccount | undefined;
|
|
||||||
if (session) {
|
|
||||||
const accounts = await readAccounts()
|
|
||||||
account = accounts.find(account => account.id === session.accountId);
|
|
||||||
}
|
|
||||||
const schedule = await readSchedule();
|
const schedule = await readSchedule();
|
||||||
return canSeeCrew(account?.type) ? schedule : filterSchedule(schedule);
|
return canSeeCrew(session?.account.type) ? schedule : filterSchedule(schedule);
|
||||||
});
|
});
|
||||||
|
|
35
server/api/users/index.get.ts
Normal file
35
server/api/users/index.get.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { readUsers, type ServerUser } from "~/server/database"
|
||||||
|
import type { ApiUser } from "~/shared/types/api";
|
||||||
|
|
||||||
|
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);
|
||||||
|
const users = await readUsers();
|
||||||
|
|
||||||
|
if (session.account.type === "admin") {
|
||||||
|
return users.map(serverUserToApi);
|
||||||
|
}
|
||||||
|
if (session.account.type === "crew") {
|
||||||
|
return users.filter(u => u.type === "crew" || u.type === "admin").map(serverUserToApi);
|
||||||
|
}
|
||||||
|
throw createError({
|
||||||
|
status: 403,
|
||||||
|
statusText: "Forbidden",
|
||||||
|
message: "You do not have permission to list users",
|
||||||
|
});
|
||||||
|
})
|
|
@ -1,19 +1,37 @@
|
||||||
import { readFile, unlink, writeFile } from "node:fs/promises";
|
import { readFile, unlink, writeFile } from "node:fs/promises";
|
||||||
import type { ApiAccount, ApiSchedule, ApiSubscription } from "~/shared/types/api";
|
import type { ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api";
|
||||||
import { generateDemoSchedule, generateDemoAccounts } from "./generate-demo-schedule";
|
import { generateDemoSchedule, generateDemoAccounts } from "./generate-demo-schedule";
|
||||||
|
import type { Id } from "~/shared/types/common";
|
||||||
|
|
||||||
export interface ServerSession {
|
export interface ServerSession {
|
||||||
id: number,
|
id: number,
|
||||||
accountId: number,
|
account: ServerUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface StoredSession {
|
||||||
|
id: number,
|
||||||
|
accountId: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerUser {
|
||||||
|
id: Id,
|
||||||
|
updatedAt: string,
|
||||||
|
deleted?: boolean,
|
||||||
|
type: ApiUserType,
|
||||||
|
name?: string,
|
||||||
|
interestedEventIds?: number[],
|
||||||
|
interestedEventSlotIds?: number[],
|
||||||
|
timezone?: string,
|
||||||
|
locale?: string,
|
||||||
|
}
|
||||||
|
|
||||||
// For this demo I'm just storing the runtime data in JSON files. When making
|
// For this demo I'm just storing the runtime data in JSON files. When making
|
||||||
// this into proper application this should be replaced with an actual database.
|
// this into proper application this should be replaced with an actual database.
|
||||||
|
|
||||||
const schedulePath = "data/schedule.json";
|
const schedulePath = "data/schedule.json";
|
||||||
const subscriptionsPath = "data/subscriptions.json";
|
const subscriptionsPath = "data/subscriptions.json";
|
||||||
const accountsPath = "data/accounts.json";
|
const usersPath = "data/users.json";
|
||||||
const nextAccountIdPath = "data/next-account-id.json";
|
const nextUserIdPath = "data/next-user-id.json";
|
||||||
const sessionsPath = "data/sessions.json";
|
const sessionsPath = "data/sessions.json";
|
||||||
const nextSessionIdPath = "data/next-session-id.json";
|
const nextSessionIdPath = "data/next-session-id.json";
|
||||||
|
|
||||||
|
@ -30,7 +48,7 @@ async function remove(path: string) {
|
||||||
export async function deleteDatbase() {
|
export async function deleteDatbase() {
|
||||||
await remove(schedulePath);
|
await remove(schedulePath);
|
||||||
await remove(subscriptionsPath);
|
await remove(subscriptionsPath);
|
||||||
await remove(accountsPath);
|
await remove(usersPath);
|
||||||
await remove(sessionsPath);
|
await remove(sessionsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,21 +85,21 @@ export async function writeSubscriptions(subscriptions: ApiSubscription[]) {
|
||||||
await writeFile(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
|
await writeFile(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nextAccountId() {
|
export async function nextUserId() {
|
||||||
let nextId = await readJson(nextAccountIdPath, 0);
|
let nextId = await readJson(nextUserIdPath, 0);
|
||||||
if (nextId === 0) {
|
if (nextId === 0) {
|
||||||
nextId = Math.max(...(await readAccounts()).map(account => account.id), -1) + 1;
|
nextId = Math.max(...(await readUsers()).map(user => user.id), -1) + 1;
|
||||||
}
|
}
|
||||||
await writeFile(nextAccountIdPath, String(nextId + 1), "utf-8");
|
await writeFile(nextUserIdPath, String(nextId + 1), "utf-8");
|
||||||
return nextId;
|
return nextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readAccounts() {
|
export async function readUsers() {
|
||||||
return await readJson(accountsPath, generateDemoAccounts);
|
return await readJson(usersPath, generateDemoAccounts as () => ServerUser[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeAccounts(accounts: ApiAccount[]) {
|
export async function writeUsers(users: ServerUser[]) {
|
||||||
await writeFile(accountsPath, JSON.stringify(accounts, undefined, "\t") + "\n", "utf-8");
|
await writeFile(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nextSessionId() {
|
export async function nextSessionId() {
|
||||||
|
@ -91,9 +109,26 @@ export async function nextSessionId() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readSessions() {
|
export async function readSessions() {
|
||||||
return await readJson<ServerSession[]>(sessionsPath, []);
|
const users = await readUsers();
|
||||||
|
const sessions: ServerSession[] = [];
|
||||||
|
for (const stored of await readJson<StoredSession[]>(sessionsPath, [])) {
|
||||||
|
const user = users.find(user => user.id === stored.accountId);
|
||||||
|
if (user) {
|
||||||
|
sessions.push({
|
||||||
|
id: stored.id,
|
||||||
|
account: user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeSessions(sessions: ServerSession[]) {
|
export async function writeSessions(sessions: ServerSession[]) {
|
||||||
await writeFile(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
|
const stored: StoredSession[] = sessions.map(
|
||||||
|
session => ({
|
||||||
|
id: session.id,
|
||||||
|
accountId: session.account.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await writeFile(sessionsPath, JSON.stringify(stored, undefined, "\t") + "\n", "utf-8");
|
||||||
}
|
}
|
||||||
|
|
|
@ -260,7 +260,7 @@ function toShift(origin: Date, shorthand: string, idToAssigned: Map<number, numb
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateDemoSchedule(): ApiSchedule {
|
function getDemoOrigin() {
|
||||||
const origin = new Date();
|
const origin = new Date();
|
||||||
const utcOffset = 1;
|
const utcOffset = 1;
|
||||||
origin.setUTCDate(origin.getUTCDate() - origin.getUTCDay() + 1); // Go to Monday
|
origin.setUTCDate(origin.getUTCDate() - origin.getUTCDay() + 1); // Go to Monday
|
||||||
|
@ -268,10 +268,15 @@ export function generateDemoSchedule(): ApiSchedule {
|
||||||
origin.setUTCMinutes(0);
|
origin.setUTCMinutes(0);
|
||||||
origin.setUTCSeconds(0);
|
origin.setUTCSeconds(0);
|
||||||
origin.setUTCMilliseconds(0);
|
origin.setUTCMilliseconds(0);
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDemoSchedule(): ApiSchedule {
|
||||||
|
const origin = getDemoOrigin();
|
||||||
|
|
||||||
const eventCounts = new Map<number, number>()
|
const eventCounts = new Map<number, number>()
|
||||||
const slotCounts = new Map<number, number>()
|
const slotCounts = new Map<number, number>()
|
||||||
const accounts = generateDemoAccounts();
|
const accounts = generateDemoAccounts(origin);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
for (const id of account.interestedEventIds ?? []) {
|
for (const id of account.interestedEventIds ?? []) {
|
||||||
eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1);
|
eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1);
|
||||||
|
@ -390,13 +395,14 @@ function random() {
|
||||||
return (seed = (a * seed + c) % m | 0) / 2 ** 31;
|
return (seed = (a * seed + c) % m | 0) / 2 ** 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateDemoAccounts(): ApiAccount[] {
|
export function generateDemoAccounts(origin = getDemoOrigin()): ApiAccount[] {
|
||||||
seed = 1;
|
seed = 1;
|
||||||
const accounts: ApiAccount[] = [];
|
const accounts: ApiAccount[] = [];
|
||||||
|
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
accounts.push({
|
accounts.push({
|
||||||
id: accounts.length,
|
id: accounts.length,
|
||||||
|
updatedAt: toIso(toDate(origin, "d-1", "10:41")),
|
||||||
name,
|
name,
|
||||||
type: (["regular", "crew", "crew", "crew", "admin"] as const)[Math.floor(random() ** 3 * 5)],
|
type: (["regular", "crew", "crew", "crew", "admin"] as const)[Math.floor(random() ** 3 * 5)],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { readAccounts } from "~/server/database";
|
import { readUsers } from "~/server/database";
|
||||||
import { canSeeCrew } from "./utils/schedule";
|
import { canSeeCrew } from "./utils/schedule";
|
||||||
import type { ApiAccount, ApiEvent } from "~/shared/types/api";
|
import type { ApiAccount, ApiEvent } from "~/shared/types/api";
|
||||||
|
|
||||||
|
@ -66,16 +66,16 @@ export function cancelSessionStreams(sessionId: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodeEventCache = new WeakMap<ApiEvent, Map<ApiAccount["type"] | undefined, string>>();
|
const encodeEventCache = new WeakMap<ApiEvent, Map<ApiAccount["type"] | undefined, string>>();
|
||||||
function encodeEvent(event: ApiEvent, accountType: ApiAccount["type"] | undefined) {
|
function encodeEvent(event: ApiEvent, userType: ApiAccount["type"] | undefined) {
|
||||||
const cache = encodeEventCache.get(event);
|
const cache = encodeEventCache.get(event);
|
||||||
const cacheEntry = cache?.get(accountType);
|
const cacheEntry = cache?.get(userType);
|
||||||
if (cacheEntry) {
|
if (cacheEntry) {
|
||||||
return cacheEntry;
|
return cacheEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: string;
|
let data: string;
|
||||||
if (event.type === "schedule-update") {
|
if (event.type === "schedule-update") {
|
||||||
if (!canSeeCrew(accountType)) {
|
if (!canSeeCrew(userType)) {
|
||||||
event = {
|
event = {
|
||||||
type: event.type,
|
type: event.type,
|
||||||
updatedFrom: event.updatedFrom,
|
updatedFrom: event.updatedFrom,
|
||||||
|
@ -83,14 +83,29 @@ function encodeEvent(event: ApiEvent, accountType: ApiAccount["type"] | undefine
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
data = JSON.stringify(event);
|
data = JSON.stringify(event);
|
||||||
|
} else if (event.type === "user-update") {
|
||||||
|
if (
|
||||||
|
!canSeeCrew(userType)
|
||||||
|
|| !event.data.deleted && event.data.type === "anonymous" && !canSeeAnonymous(userType)
|
||||||
|
) {
|
||||||
|
event = {
|
||||||
|
type: event.type,
|
||||||
|
data: {
|
||||||
|
id: event.data.id,
|
||||||
|
updatedAt: event.data.updatedAt,
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = JSON.stringify(event);
|
||||||
} else {
|
} else {
|
||||||
throw Error(`encodeEvent cannot encode ${event.type} event`);
|
throw Error(`encodeEvent cannot encode ${event.type} event`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cache) {
|
if (cache) {
|
||||||
cache.set(accountType, data);
|
cache.set(userType, data);
|
||||||
} else {
|
} else {
|
||||||
encodeEventCache.set(event, new Map([[accountType, data]]));
|
encodeEventCache.set(event, new Map([[userType, data]]));
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -101,20 +116,20 @@ export async function broadcastEvent(event: ApiEvent) {
|
||||||
if (!streams.size) {
|
if (!streams.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const accounts = await readAccounts();
|
const users = await readUsers();
|
||||||
for (const [stream, streamData] of streams) {
|
for (const [stream, streamData] of streams) {
|
||||||
// Account events are specially handled and only sent to the account they belong to.
|
// Account events are specially handled and only sent to the user they belong to.
|
||||||
if (event.type === "account-update") {
|
if (event.type === "account-update") {
|
||||||
if (streamData.accountId === event.data.id) {
|
if (streamData.accountId === event.data.id) {
|
||||||
sendMessage(stream, `id: ${id}\nevent: update\ndata: ${JSON.stringify(event)}\n\n`);
|
sendMessage(stream, `id: ${id}\nevent: update\ndata: ${JSON.stringify(event)}\n\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let accountType: ApiAccount["type"] | undefined;
|
let userType: ApiAccount["type"] | undefined;
|
||||||
if (streamData.accountId !== undefined) {
|
if (streamData.accountId !== undefined) {
|
||||||
accountType = accounts.find(a => a.id === streamData.accountId)?.type
|
userType = users.find(a => a.id === streamData.accountId)?.type
|
||||||
}
|
}
|
||||||
const data = encodeEvent(event, accountType)
|
const data = encodeEvent(event, userType)
|
||||||
sendMessage(stream, `id: ${id}\nevent: update\ndata: ${data}\n\n`);
|
sendMessage(stream, `id: ${id}\nevent: update\ndata: ${data}\n\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { readSchedule, writeSchedule } from '~/server/database';
|
import { readSchedule, ServerUser, writeSchedule } from '~/server/database';
|
||||||
import { broadcastEvent } from '~/server/streams';
|
import { broadcastEvent } from '~/server/streams';
|
||||||
import type { ApiAccount, ApiSchedule } from '~/shared/types/api';
|
import type { ApiAccount, ApiSchedule } from '~/shared/types/api';
|
||||||
|
|
||||||
export async function updateScheduleInterestedCounts(accounts: ApiAccount[]) {
|
export async function updateScheduleInterestedCounts(users: ServerUser[]) {
|
||||||
const eventCounts = new Map<number, number>();
|
const eventCounts = new Map<number, number>();
|
||||||
const eventSlotCounts = new Map<number, number>();
|
const eventSlotCounts = new Map<number, number>();
|
||||||
for (const account of accounts) {
|
for (const user of users) {
|
||||||
if (account.interestedEventIds)
|
if (user.deleted)
|
||||||
for (const id of account.interestedEventIds)
|
continue;
|
||||||
|
if (user.interestedEventIds)
|
||||||
|
for (const id of user.interestedEventIds)
|
||||||
eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1);
|
eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1);
|
||||||
if (account.interestedEventSlotIds)
|
if (user.interestedEventSlotIds)
|
||||||
for (const id of account.interestedEventSlotIds)
|
for (const id of user.interestedEventSlotIds)
|
||||||
eventSlotCounts.set(id, (eventSlotCounts.get(id) ?? 0) + 1);
|
eventSlotCounts.set(id, (eventSlotCounts.get(id) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +60,12 @@ export async function updateScheduleInterestedCounts(accounts: ApiAccount[]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canSeeCrew(accountType: string | undefined) {
|
export function canSeeAnonymous(userType: string | undefined) {
|
||||||
return accountType === "crew" || accountType === "admin";
|
return userType === "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canSeeCrew(userType: string | undefined) {
|
||||||
|
return userType === "crew" || userType === "admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filters out crew visible only parts of schedule */
|
/** Filters out crew visible only parts of schedule */
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { H3Event } from "h3";
|
import type { H3Event } from "h3";
|
||||||
import { nextSessionId, readSessions, readSubscriptions, type ServerSession, writeSessions, writeSubscriptions } from "~/server/database";
|
import { nextSessionId, readSessions, readSubscriptions, type ServerSession, type ServerUser, writeSessions, writeSubscriptions } from "~/server/database";
|
||||||
|
|
||||||
const oneYearSeconds = 365 * 24 * 60 * 60;
|
const oneYearSeconds = 365 * 24 * 60 * 60;
|
||||||
|
|
||||||
|
@ -34,12 +34,12 @@ export async function clearServerSession(event: H3Event) {
|
||||||
deleteCookie(event, "session");
|
deleteCookie(event, "session");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setServerSession(event: H3Event, accountId: number) {
|
export async function setServerSession(event: H3Event, account: ServerUser) {
|
||||||
const sessions = await readSessions();
|
const sessions = await readSessions();
|
||||||
await clearServerSessionInternal(event, sessions);
|
await clearServerSessionInternal(event, sessions);
|
||||||
|
|
||||||
const newSession: ServerSession = {
|
const newSession: ServerSession = {
|
||||||
accountId,
|
account,
|
||||||
id: await nextSessionId(),
|
id: await nextSessionId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ export type ApiUserType = z.infer<typeof apiUserTypeSchema>;
|
||||||
|
|
||||||
export interface ApiAccount {
|
export interface ApiAccount {
|
||||||
id: Id,
|
id: Id,
|
||||||
|
updatedAt: string,
|
||||||
type: ApiUserType,
|
type: ApiUserType,
|
||||||
/** Name of the account. Not present on anonymous accounts */
|
/** Name of the account. Not present on anonymous accounts */
|
||||||
name?: string,
|
name?: string,
|
||||||
|
@ -110,6 +111,13 @@ export const apiUserSchema = defineEntity({
|
||||||
});
|
});
|
||||||
export type ApiUser = z.infer<typeof apiUserSchema>;
|
export type ApiUser = z.infer<typeof apiUserSchema>;
|
||||||
|
|
||||||
|
export const apiUserPatchSchema = z.object({
|
||||||
|
id: idSchema,
|
||||||
|
type: z.optional(apiUserTypeSchema),
|
||||||
|
name: z.optional(z.string()),
|
||||||
|
});
|
||||||
|
export type ApiUserPatch = z.infer<typeof apiUserPatchSchema>;
|
||||||
|
|
||||||
export interface ApiAccountUpdate {
|
export interface ApiAccountUpdate {
|
||||||
type: "account-update",
|
type: "account-update",
|
||||||
data: ApiAccount,
|
data: ApiAccount,
|
||||||
|
@ -121,7 +129,14 @@ export interface ApiScheduleUpdate {
|
||||||
data: ApiSchedule,
|
data: ApiSchedule,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiUserUpdate {
|
||||||
|
type: "user-update",
|
||||||
|
updatedFrom?: string,
|
||||||
|
data: ApiUser,
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiEvent =
|
export type ApiEvent =
|
||||||
| ApiAccountUpdate
|
| ApiAccountUpdate
|
||||||
| ApiScheduleUpdate
|
| ApiScheduleUpdate
|
||||||
|
| ApiUserUpdate
|
||||||
;
|
;
|
||||||
|
|
84
stores/users.ts
Normal file
84
stores/users.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { Info } from "~/shared/utils/luxon";
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
interface SyncOperation {
|
||||||
|
controller: AbortController,
|
||||||
|
promise: Promise<Ref<ClientMap<ClientUser>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUsersStore = defineStore("users", () => {
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
fetched: ref<boolean>(false),
|
||||||
|
pendingSync: ref<SyncOperation>(),
|
||||||
|
users: ref<ClientMap<ClientUser>>(new ClientMap(ClientUser, new Map(), new Map())),
|
||||||
|
}
|
||||||
|
const getters = {
|
||||||
|
}
|
||||||
|
const actions = {
|
||||||
|
async fetch() {
|
||||||
|
if (state.fetched.value) {
|
||||||
|
return state.users;
|
||||||
|
}
|
||||||
|
const pending = state.pendingSync.value;
|
||||||
|
if (pending) {
|
||||||
|
return pending.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestFetch = useRequestFetch();
|
||||||
|
const controller = new AbortController();
|
||||||
|
const zone = Info.normalizeZone(accountStore.activeTimezone);
|
||||||
|
const locale = accountStore.activeLocale;
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
const apiUsers = await requestFetch("/api/users", { signal: controller.signal });
|
||||||
|
state.users.value.apiUpdate({
|
||||||
|
type: "user",
|
||||||
|
entities: apiUsers,
|
||||||
|
}, { zone, locale });
|
||||||
|
state.pendingSync.value = undefined;
|
||||||
|
state.fetched.value = true;
|
||||||
|
return state.users;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.name !== "AbortError")
|
||||||
|
state.pendingSync.value = undefined;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
state.pendingSync.value = {
|
||||||
|
controller,
|
||||||
|
promise,
|
||||||
|
};
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
async resync(id: number) {
|
||||||
|
const pending = state.pendingSync.value;
|
||||||
|
if (pending) {
|
||||||
|
pending.controller.abort();
|
||||||
|
}
|
||||||
|
state.pendingSync.value = undefined;
|
||||||
|
state.fetched.value = false;
|
||||||
|
await actions.fetch();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
appEventSource?.addEventListener("update", (event) => {
|
||||||
|
if (event.data.type !== "user-update") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("appyling", event.data)
|
||||||
|
const zone = Info.normalizeZone(accountStore.activeTimezone);
|
||||||
|
const locale = accountStore.activeLocale;
|
||||||
|
state.users.value.apiUpdate({
|
||||||
|
type: "user",
|
||||||
|
entities: [event.data.data],
|
||||||
|
}, { zone, locale });
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...getters,
|
||||||
|
...actions,
|
||||||
|
};
|
||||||
|
})
|
Loading…
Add table
Add a link
Reference in a new issue