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
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"

View File

@ -33,8 +33,7 @@ export default function EmailTokenVerification() {
const verifyTokenMutation = useMutation({
mutationFn: async (token: string) => {
const res = await pb.collection("users").confirmVerification(token)
console.log({res})
await pb.collection("users").confirmVerification(token)
},
onSuccess: () => {
showSuccessNotification("E-Mail erfolgreich verifiziert")
@ -51,7 +50,7 @@ export default function EmailTokenVerification() {
useEffect(() => {
const token = searchParams.get(EMAIL_TOKEN_KEY)
if (token !== null) {
if (token !== null && !user && !verifyTokenMutation.isPending) {
verifyTokenMutation.mutate(token)
}
// 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 {Link} from "react-router-dom";
export default function RegisterModal() {
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,
terms: (val) => !val ? "Du musst die AGB akzeptieren" : null,
privacy: (val) => !val ? "Du musst die Datenschutzerklärung akzeptieren" : null
}
},
})
const registerMutation = useMutation({
@ -79,10 +81,28 @@ export default function RegisterModal() {
<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
label={"Anmeldename"}
description={"Dein Anmeldename ist für eingeloggte Personen sichtbar"}
placeholder={"Anmeldename"}
placeholder={"vorname.nachname"}
leftSection={<IconUser/>}
required
{...formValues.getInputProps("username")}

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
import {EventListModel, EventListSlotsWithEntriesCountModel} from "@/models/EventTypes.ts";
import classes from "./EventListSlotView.module.css";
import {useDisclosure} from "@mantine/hooks";
import {Alert, Collapse, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconInfoCircle} from "@tabler/icons-react";
import {Alert, Button, Collapse, Group, List, ThemeIcon, Tooltip, UnstyledButton} from "@mantine/core";
import {IconChevronDown, IconChevronRight, IconEye, IconInfoCircle} from "@tabler/icons-react";
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 {showSuccessNotification} from "@/components/util.tsx";
import InnerHtml from "@/components/InnerHtml";
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 {RenderDateRange} from "@/pages/events/e/:eventId/EventLists/EventListComponents/RenderDateRange.tsx";
import SlotProgress from "@/pages/events/e/:eventId/EventLists/EventListComponents/SlotProgress.tsx";
import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx";
import {pprintDateRange} from "@/lib/datetime.ts";
export default function EventListSlotView({slot, list, refetch}: {
list: EventListModel,
@ -32,6 +33,24 @@ export default function EventListSlotView({slot, list, refetch}: {
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({
mutationFn: async (data: FieldEntries) => {
await pb.collection("eventListSlotEntries").create({
@ -57,11 +76,9 @@ export default function EventListSlotView({slot, list, refetch}: {
</Tooltip>
<div className={classes.slotInfo}>
<RenderDateRange start={new Date(slot.startDate)} end={new Date(slot.endDate)}/>
<SlotProgress slot={slot}/>
</div>
</UnstyledButton>
@ -77,7 +94,35 @@ export default function EventListSlotView({slot, list, refetch}: {
{
!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"}>
Für diese Liste sind nur StuVe-Accounts zugelassen
</Alert>
@ -89,8 +134,7 @@ export default function EventListSlotView({slot, list, refetch}: {
<Alert color={"red"} title={"Zeitslot voll"}>
Dieser Zeitslot ist bereits voll
</Alert>
</>
:
</> :
<FormInput
disabled={!user || slotIsFull}
schema={questionSchema}

View File

@ -62,15 +62,6 @@ export default function EventListView({event, listId}: { event: EventModel, list
</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 &&
<Alert>
@ -78,7 +69,6 @@ export default function EventListView({event, listId}: { event: EventModel, list
</Alert>
}
{
(list.open && slots.length !== 0) && <>
{slots.map(slot => (

View File

@ -14,21 +14,27 @@ import EventLoginWarning from "@/pages/events/s/EventLoginWarning.tsx";
export default function SharedEvent() {
const {pb} = usePB()
const {pb, user} = usePB()
const {eventId} = useParams() as { eventId: string }
const [searchParams] = useSearchParams()
const listIds = searchParams.get('lists')?.split(",") ?? []
const eventQuery = useQuery({
queryKey: ["event", eventId],
queryFn: async () => (await pb.collection("events").getOne(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)
if (eventQuery.isLoading) {
@ -68,6 +74,34 @@ export default function SharedEvent() {
<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"}>
<Alert color={"orange"} icon={<IconArchive/>}>
Dieses Event ist archiviert und wird nicht mehr verwaltet