fix(auth): fixed bug where users where not properly logged in on their first login
Build and Push Docker image / build-and-push (push) Successful in 2m2s Details

This commit is contained in:
Valentin Kolb 2024-06-06 16:17:18 +02:00
parent 54057be1f6
commit 4ea290ddf4
9 changed files with 156 additions and 48 deletions

View File

@ -9,5 +9,5 @@ export const PB_STORAGE_KEY = "stuve-it-login-record"
// general // general
export const APP_NAME = "StuVe IT" export const APP_NAME = "StuVe IT"
export const APP_VERSION = "0.8.9 (beta)" export const APP_VERSION = "0.8.11 (beta)"
export const APP_URL = "https://it.stuve.uni-ulm.de" export const APP_URL = "https://it.stuve.uni-ulm.de"

View File

@ -33,8 +33,7 @@ export default function EmailTokenVerification() {
const verifyTokenMutation = useMutation({ const verifyTokenMutation = useMutation({
mutationFn: async (token: string) => { mutationFn: async (token: string) => {
const res = await pb.collection("users").confirmVerification(token) await pb.collection("users").confirmVerification(token)
console.log({res})
}, },
onSuccess: () => { onSuccess: () => {
showSuccessNotification("E-Mail erfolgreich verifiziert") showSuccessNotification("E-Mail erfolgreich verifiziert")
@ -51,7 +50,7 @@ export default function EmailTokenVerification() {
useEffect(() => { useEffect(() => {
const token = searchParams.get(EMAIL_TOKEN_KEY) const token = searchParams.get(EMAIL_TOKEN_KEY)
if (token !== null) { if (token !== null && !user && !verifyTokenMutation.isPending) {
verifyTokenMutation.mutate(token) verifyTokenMutation.mutate(token)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -8,6 +8,7 @@ import {useMutation} from "@tanstack/react-query";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
export default function RegisterModal() { export default function RegisterModal() {
const {value, handler} = useRegister() const {value, handler} = useRegister()
@ -31,7 +32,8 @@ export default function RegisterModal() {
passwordConfirm: (val, values) => val !== values.password ? "Die Passwörter stimmen nicht überein" : null, passwordConfirm: (val, values) => val !== values.password ? "Die Passwörter stimmen nicht überein" : null,
terms: (val) => !val ? "Du musst die AGB akzeptieren" : null, terms: (val) => !val ? "Du musst die AGB akzeptieren" : null,
privacy: (val) => !val ? "Du musst die Datenschutzerklärung akzeptieren" : null privacy: (val) => !val ? "Du musst die Datenschutzerklärung akzeptieren" : null
} },
}) })
const registerMutation = useMutation({ const registerMutation = useMutation({
@ -79,10 +81,28 @@ export default function RegisterModal() {
<Collapse in={formValues.values.email.length > 0} className={"stack"}> <Collapse in={formValues.values.email.length > 0} className={"stack"}>
<Group grow>
<TextInput
label={"Vorname"}
description={"Ist für eingeloggte Personen sichtbar"}
placeholder={"Vorname"}
required
{...formValues.getInputProps("givenName")}
/>
<TextInput
label={"Nachname"}
description={"Ist für eingeloggte Personen sichtbar"}
placeholder={"Nachname"}
required
{...formValues.getInputProps("sn")}
/>
</Group>
<TextInput <TextInput
label={"Anmeldename"} label={"Anmeldename"}
description={"Dein Anmeldename ist für eingeloggte Personen sichtbar"} description={"Dein Anmeldename ist für eingeloggte Personen sichtbar"}
placeholder={"Anmeldename"} placeholder={"vorname.nachname"}
leftSection={<IconUser/>} leftSection={<IconUser/>}
required required
{...formValues.getInputProps("username")} {...formValues.getInputProps("username")}

View File

@ -94,6 +94,7 @@ const PocketData = () => {
pb.authStore.save(token, record) pb.authStore.save(token, record)
return record return record
} catch (e) { } catch (e) {
console.error("refresh token error", e)
pb.authStore.clear() pb.authStore.clear()
return null return null
} }
@ -111,12 +112,14 @@ const PocketData = () => {
const record = await pb.collection(PB_USER_COLLECTION).getOne(pb.authStore.model?.id ?? "", { const record = await pb.collection(PB_USER_COLLECTION).getOne(pb.authStore.model?.id ?? "", {
expand: "memberOf" expand: "memberOf"
}) })
if (record.verified) { if (record.verified === true) {
pb.authStore.save(pb.authStore.token, record) pb.authStore.save(pb.authStore.token, record)
return record return record
} else { } else if (record.verified === false) {
pb.authStore.clear() pb.authStore.clear()
return null return null
} else {
return null
} }
} catch (e) { } catch (e) {
if (e instanceof ClientResponseError && e.status === 401) { if (e instanceof ClientResponseError && e.status === 401) {
@ -179,7 +182,7 @@ const PocketData = () => {
ldapLogin, ldapLogin,
guestLogin, guestLogin,
logout, logout,
user: user as UserModal | null, user: pb.authStore.isValid ? user as UserModal | null : null,
pb, pb,
refreshUser: refreshUserQuery.refetch, refreshUser: refreshUserQuery.refetch,
useSubscription, useSubscription,

View File

@ -2,17 +2,17 @@ import PromptLoginModal from "@/components/users/modals/PromptLoginModal.tsx";
import {Link, useNavigate} from "react-router-dom"; import {Link, useNavigate} from "react-router-dom";
import {useState} from "react"; import {useState} from "react";
import {useDebouncedValue, useToggle} from "@mantine/hooks"; import {useDebouncedValue, useToggle} from "@mantine/hooks";
import {useQuery} from "@tanstack/react-query"; import {useInfiniteQuery} from "@tanstack/react-query";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import { import {
ActionIcon, ActionIcon,
Anchor, Anchor,
Breadcrumbs, Breadcrumbs,
Button,
Center, Center,
Group, Group,
Loader, Loader,
LoadingOverlay, LoadingOverlay,
Pagination,
Stack, Stack,
Text, Text,
TextInput, TextInput,
@ -21,7 +21,7 @@ import {
Tooltip Tooltip
} from "@mantine/core"; } from "@mantine/core";
import ShowHelp from "@/components/ShowHelp.tsx"; import ShowHelp from "@/components/ShowHelp.tsx";
import {IconConfetti, IconDatabaseOff, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; import {IconArrowDown, IconConfetti, IconDatabaseOff, IconSortAscending, IconSortDescending} from "@tabler/icons-react";
import UserEntryRow from "@/pages/events/entries/UserEntryRow.tsx"; import UserEntryRow from "@/pages/events/entries/UserEntryRow.tsx";
export default function UserEntries() { export default function UserEntries() {
@ -30,23 +30,24 @@ export default function UserEntries() {
const {pb, user} = usePB() const {pb, user} = usePB()
const [page, setPage] = useState(1)
const [sortDirection, toggleSortDirection] = useToggle(['DESC', 'ASC']); const [sortDirection, toggleSortDirection] = useToggle(['DESC', 'ASC']);
const [eventSearch, setEventSearch] = useState("") const [eventSearch, setEventSearch] = useState("")
const [debouncedSearch] = useDebouncedValue(eventSearch, 200) const [debouncedSearch] = useDebouncedValue(eventSearch, 200)
const entriesQuery = useQuery({ const entriesQuery = useInfiniteQuery({
queryKey: ["userEntries", user?.id, page, debouncedSearch, sortDirection], queryKey: ["userEntries", user?.id, debouncedSearch, sortDirection],
queryFn: async () => ( queryFn: async ({pageParam}) => (
await pb.collection("eventListSlotEntriesWithUser").getList(page, 50, { await pb.collection("eventListSlotEntriesWithUser").getList(pageParam, 50, {
filter: `user='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`, filter: `user='${user?.id}'${debouncedSearch ? `&&event.name~'${debouncedSearch}'` : ""}`,
sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate", sort: sortDirection === "ASC" ? "-event.startDate" : "event.startDate",
expand: "user" expand: "user"
}) })
), ),
getNextPageParam: (lastPage) =>
lastPage.page >= lastPage.totalPages ? undefined : lastPage.page + 1,
initialPageParam: 1,
enabled: !!user enabled: !!user
}) })
@ -54,6 +55,9 @@ export default function UserEntries() {
return <PromptLoginModal onAbort={() => navigate("/")}/> return <PromptLoginModal onAbort={() => navigate("/")}/>
} }
const entries = entriesQuery.data?.pages.flatMap(page => page.items) ?? []
const totalItems = entriesQuery.data?.pages[0].totalItems ?? 0
return <> return <>
{entriesQuery.isLoading && <LoadingOverlay/>} {entriesQuery.isLoading && <LoadingOverlay/>}
@ -104,13 +108,13 @@ export default function UserEntries() {
</Text> </Text>
<Text c={"dimmed"} size={"xs"}> <Text c={"dimmed"} size={"xs"}>
{entriesQuery.data?.totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Anmeldungen"} {totalItems ?? 0} {eventSearch ? "Ergebnisse" : "Anmeldungen"}
</Text> </Text>
</Group> </Group>
<PocketBaseErrorAlert error={entriesQuery.error}/> <PocketBaseErrorAlert error={entriesQuery.error}/>
{entriesQuery.data?.totalItems === 0 && <Stack align={"center"}> {totalItems === 0 && <Stack align={"center"}>
<ThemeIcon variant={"transparent"} color={"gray"} size={"md"}> <ThemeIcon variant={"transparent"} color={"gray"} size={"md"}>
<IconDatabaseOff/> <IconDatabaseOff/>
</ThemeIcon> </ThemeIcon>
@ -119,14 +123,25 @@ export default function UserEntries() {
</Stack> </Stack>
} }
{entriesQuery.data?.items.map(entry => ( {entries.map(entry => (
<UserEntryRow entry={entry} refetch={() => entriesQuery.refetch()} key={entry.id}/> <UserEntryRow entry={entry} refetch={() => entriesQuery.refetch()} key={entry.id}/>
))} ))}
<Center> {
<Pagination total={entriesQuery.data?.totalPages ?? 1} value={page} onChange={setPage} size={"xs"}/> entriesQuery.hasNextPage &&
</Center> <Center p={"xs"}>
<Button
disabled={entriesQuery.isFetchingNextPage || !entriesQuery.hasNextPage}
loading={entriesQuery.isFetchingNextPage}
variant={"transparent"}
size={"compact-xs"}
leftSection={<IconArrowDown/>}
onClick={() => entriesQuery.fetchNextPage()}
>
Mehr laden
</Button>
</Center>
}
</div> </div>
</> </>
} }

View File

@ -18,6 +18,7 @@ import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListCom
import { import {
EntryQuestionAndStatusData EntryQuestionAndStatusData
} from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx"; } from "@/pages/events/e/:eventId/EventLists/EventListComponents/EntryQuestionAndStatusData.tsx";
import {Link} from "react-router-dom";
export default function UserEntryRow({entry, refetch}: { export default function UserEntryRow({entry, refetch}: {
@ -80,7 +81,9 @@ export default function UserEntryRow({entry, refetch}: {
<IconList/> <IconList/>
</ThemeIcon> </ThemeIcon>
}> }>
{entry.listName} <Link to={`/events/s/${entry.event}?lists=${entry.eventList}`} >
{entry.listName}
</Link>
</TextWithIcon> </TextWithIcon>
<Group gap={"xs"} justify={"center"}> <Group gap={"xs"} justify={"center"}>

View File

@ -1,19 +1,20 @@
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts"; import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import classes from "./EventListSlotView.module.css"; import classes from "./EventListSlotView.module.css";
import {useDisclosure} from "@mantine/hooks"; import {useDisclosure} from "@mantine/hooks";
import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core"; import {Alert, Button, Collapse, Group, List, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react"; import {IconChevronDown, IconChevronRight, IconEye, IconInfoCircle} from "@tabler/icons-react";
import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx"; import {PocketBaseErrorAlert, usePB} from "@/lib/pocketbase.tsx";
import {useMutation} from "@tanstack/react-query"; import {useMutation, useQuery} from "@tanstack/react-query";
import {FieldEntries} from "@/components/formUtil/FromInput/types.ts"; import {FieldEntries} from "@/components/formUtil/FromInput/types.ts";
import {showSuccessNotification} from "@/components/util.tsx"; import {showSuccessNotification} from "@/components/util.tsx";
import InnerHtml from "@/components/InnerHtml"; import InnerHtml from "@/components/InnerHtml";
import FormInput from "@/components/formUtil/FromInput"; import FormInput from "@/components/formUtil/FromInput";
import {useNavigate} from "react-router-dom"; import {Link, useNavigate} from "react-router-dom";
import {getListSchemas} from "@/pages/events/util.ts"; import {getListSchemas} from "@/pages/events/util.ts";
import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx"; import {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx"; import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx"; import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx";
import {pprintDateRange} from "@/lib/datetime.ts";
export default function EventListSlotView({slot, list, refetch}: { export default function EventListSlotView({slot, list, refetch}: {
list: EventListModel, list: EventListModel,
@ -32,6 +33,24 @@ export default function EventListSlotView({slot, list, refetch}: {
const navigate = useNavigate() const navigate = useNavigate()
const slotOccupiedByUserQuery = useQuery({
queryKey: ["eventListSlotEntries", {eventListsSlot: slot.id, user: user?.id}],
queryFn: async () => {
return await pb.collection("eventListSlotEntriesWithUser").getList(1, 50, {
filter: `event='${list.event}'&&
user='${user?.id}'&&
(
slotStartDate>='${slot.startDate}'&&slotStartDate<='${slot.endDate}'||
slotEndDate>='${slot.startDate}'&&slotEndDate<='${slot.endDate}'||
slotStartDate<='${slot.startDate}'&&slotEndDate>='${slot.endDate}'
)
`,
requestKey: `slotOccupiedByUserQuery-${slot.id}-${user?.id}`
})
},
enabled: !!user && !list.allowOverlappingEntries
})
const createEntryMutation = useMutation({ const createEntryMutation = useMutation({
mutationFn: async (data: FieldEntries) => { mutationFn: async (data: FieldEntries) => {
await pb.collection("eventListSlotEntries").create({ await pb.collection("eventListSlotEntries").create({
@ -57,11 +76,9 @@ export default function EventListSlotView({slot, list, refetch}: {
</Tooltip> </Tooltip>
<div className={classes.slotInfo}> <div className={classes.slotInfo}>
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/> <RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
<SlotProgress slot={slot}/> <SlotProgress slot={slot}/>
</div> </div>
</UnstyledButton> </UnstyledButton>
@ -77,7 +94,35 @@ export default function EventListSlotView({slot, list, refetch}: {
{ {
!user ? <EventLoginWarning/> : !user ? <EventLoginWarning/> :
list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <> (slotOccupiedByUserQuery.data?.totalItems && !list.allowOverlappingEntries) ? <>
<Alert>
<div className={"stack"}>
Du hast dich bei diesem Event bereits in einen überschneidenden Zeitslot eingetragen
und
diese Liste erlaubt keine überschneidenden Einträge.
<List size={"sm"} c={"dimmed"}>
{slotOccupiedByUserQuery.data.items.map((entry) => (
<List.Item key={entry.id}>
{entry.listName}
{", Zeitslot "}
{pprintDateRange(entry.slotStartDate, entry.slotEndDate)}
</List.Item>
))}
</List>
<Group>
<Button
component={Link}
to={`/events/entries`}
variant={"light"}
leftSection={<IconEye/>}
size={"xs"}
>
Anmeldungen-Seite
</Button>
</Group>
</div>
</Alert>
</> : list.onlyStuVeAccounts && user?.REALM !== "LDAP" ? <>
<Alert color={"red"}> <Alert color={"red"}>
Für diese Liste sind nur StuVe-Accounts zugelassen Für diese Liste sind nur StuVe-Accounts zugelassen
</Alert> </Alert>
@ -89,8 +134,7 @@ export default function EventListSlotView({slot, list, refetch}: {
<Alert color={"red"} title={"Zeitslot voll"}> <Alert color={"red"} title={"Zeitslot voll"}>
Dieser Zeitslot ist bereits voll Dieser Zeitslot ist bereits voll
</Alert> </Alert>
</> </> :
:
<FormInput <FormInput
disabled={!user || slotIsFull} disabled={!user || slotIsFull}
schema={questionSchema} schema={questionSchema}

View File

@ -62,15 +62,6 @@ export default function EventListView({event, listId}: { event: EventModel, list
</Alert> </Alert>
} }
{
!list.allowOverlappingEntries &&
<Alert>
Wenn du bei diesem Event bereits für einen anderen Zeitslot angemeldet bist,
kannst du in dieser Liste nur einen Zeitslot wählen,
der nicht mit deinen anderen Anmeldungen kollidiert.
</Alert>
}
{ {
list.onlyStuVeAccounts && list.onlyStuVeAccounts &&
<Alert> <Alert>
@ -78,7 +69,6 @@ export default function EventListView({event, listId}: { event: EventModel, list
</Alert> </Alert>
} }
{ {
(list.open && slots.length !== 0) && <> (list.open && slots.length !== 0) && <>
{slots.map(slot => ( {slots.map(slot => (

View File

@ -14,21 +14,27 @@ import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx";
export default function SharedEvent() { export default function SharedEvent() {
const {pb} = usePB() const {pb, user} = usePB()
const {eventId} = useParams() as { eventId: string } const {eventId} = useParams() as { eventId: string }
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const listIds = searchParams.get('lists')?.split(",") ?? [] const listIds = searchParams.get('lists')?.split(",") ?? []
const eventQuery = useQuery({ const eventQuery = useQuery({
queryKey: ["event", eventId], queryKey: ["event", eventId],
queryFn: async () => (await pb.collection("events").getOne(eventId)), queryFn: async () => (await pb.collection("events").getOne(eventId)),
enabled: !!eventId enabled: !!eventId
}) })
const userEntriesQuery = useQuery({
queryKey: ["userEntries", user?.id ?? "anonym"],
queryFn: async () => (await pb.collection("eventListSlotEntriesWithUser").getList(1, 0, {
filter: `user='${user?.id ?? "anonym"}'&&event='${eventId}'`,
})),
enabled: !!user
})
const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data) const {isPrivilegedUser, canEditEvent} = useEventRights(eventQuery.data)
if (eventQuery.isLoading) { if (eventQuery.isLoading) {
@ -68,6 +74,34 @@ export default function SharedEvent() {
<EventLoginWarning/> <EventLoginWarning/>
{
(userEntriesQuery.data?.totalItems ?? 0) > 0 && <>
<div className={"section-transparent"}>
<Alert
color={"green"}
title={
`Du hast für dieses Event bereits
${userEntriesQuery.data?.totalItems === 1 ? "eine Anmeldung" :
`${userEntriesQuery.data?.totalItems} Anmeldungen`}`
}
>
Du hast dich bereits für dieses Event angemeldet. Du kannst deine Anmeldungen auf der
<Button
component={Link}
to={`/events/entries`}
variant={"light"}
leftSection={<IconEye/>}
size={"compact-xs"}
ms="xs"
me={"xs"}
>
Anmeldungen-Seite
</Button> einsehen.
</Alert>
</div>
</>
}
{eventIsArchived && <div className={"section-transparent"}> {eventIsArchived && <div className={"section-transparent"}>
<Alert color={"orange"} icon={<IconArchive/>}> <Alert color={"orange"} icon={<IconArchive/>}>
Dieses Event ist archiviert und wird nicht mehr verwaltet Dieses Event ist archiviert und wird nicht mehr verwaltet