Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions apps/dashboard/src/app/(internal)/brukere/[id]/edit-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { useDebouncedValue } from "@mantine/hooks"
import { IconCheck, IconLink, IconUsersGroup, IconX, IconArrowUpRight } from "@tabler/icons-react"
import { type FC, useEffect, useState } from "react"
import { useLinkOwUserToWorkspaceUserMutation, useUpdateUserMutation } from "../mutations"
import { useFindWorkspaceUserQuery, useGroupAllByMemberQuery, useIsAdminQuery } from "../queries"
import { useAuthorization } from "@/auth/authorization-context"
import { useFindWorkspaceUserQuery, useGroupAllByMemberQuery } from "../queries"
import { useUserProfileEditForm } from "./edit-form"
import { useUserDetailsContext } from "./provider"
import { getStudyGrade } from "@dotkomonline/utils"
Expand All @@ -17,7 +18,7 @@ export const UserEditCard: FC = () => {

const [customKey, setCustomKey] = useState<string | undefined>(undefined)

const { isAdmin } = useIsAdminQuery()
const { isAdministrator } = useAuthorization()
const { groups } = useGroupAllByMemberQuery(user.id)

const update = useUpdateUserMutation()
Expand All @@ -27,7 +28,7 @@ export const UserEditCard: FC = () => {

const isWorkspaceLinked = Boolean(user.workspaceUserId)
const showWorkspaceLink = isWorkspaceLinked || groups.length > 0
const isWorkspaceFetchEnabled = (isAdmin || isUser) && showWorkspaceLink
const isWorkspaceFetchEnabled = (isAdministrator || isUser) && showWorkspaceLink
const { workspaceUser, isLoading: isLoadingWorkspaceUser } = useFindWorkspaceUserQuery(
user.id,
customKey,
Expand Down Expand Up @@ -78,7 +79,7 @@ export const UserEditCard: FC = () => {

<LinkUser
showWorkspaceLink={showWorkspaceLink}
isAdmin={isAdmin ?? false}
isAdministrator={isAdministrator}
isWorkspaceLinked={isWorkspaceLinked}
isWorkspaceFetchEnabled={isWorkspaceFetchEnabled}
isLoadingWorkspaceUser={isLoadingWorkspaceUser}
Expand All @@ -94,7 +95,7 @@ export const UserEditCard: FC = () => {

interface LinkUserProps {
showWorkspaceLink: boolean
isAdmin: boolean
isAdministrator: boolean
isWorkspaceLinked: boolean
isWorkspaceFetchEnabled: boolean
isLoadingWorkspaceUser: boolean
Expand All @@ -105,7 +106,7 @@ interface LinkUserProps {

const LinkUser: FC<LinkUserProps> = ({
showWorkspaceLink,
isAdmin,
isAdministrator,
isWorkspaceLinked,
isWorkspaceFetchEnabled,
isLoadingWorkspaceUser,
Expand Down Expand Up @@ -171,14 +172,14 @@ const LinkUser: FC<LinkUserProps> = ({
)}
</Stack>

{!isWorkspaceLinked && !isAdmin && (
{!isWorkspaceLinked && !isAdministrator && (
<Text size="xs">
Kontakt HS for å tilknytte brukeren til en Google-bruker. Brukeren må tilknyttes for å kunne bli lagt til i
e-postlister.
</Text>
)}

{!isWorkspaceLinked && isAdmin && (
{!isWorkspaceLinked && isAdministrator && (
<TextInput
description="Egendefinert nøkkel. Bruk denne om den ikke finner automatisk. Kan være komplett e-postadresse eller fullt navn."
placeholder="navn.navnesen@online.ntnu.no eller Navn Navnesen"
Expand Down Expand Up @@ -234,7 +235,7 @@ const LinkUser: FC<LinkUserProps> = ({
<Button
color="green"
w="fit-content"
disabled={!isAdmin || !workspaceUser}
disabled={!isAdministrator || !workspaceUser}
leftSection={<IconLink size={16} />}
onClick={onClick}
>
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/app/(internal)/brukere/[id]/edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
UserWriteSchema,
} from "@dotkomonline/types"
import { createTextareaInput } from "@/components/forms/TextareaInput"
import { useIsAdminQuery } from "../queries"
import { useAuthorization } from "@/auth/authorization-context"

interface UseUserProfileWriteFormProps {
onSubmit(data: UserWrite): void
Expand All @@ -20,7 +20,7 @@ interface UseUserProfileWriteFormProps {
}

export const useUserProfileEditForm = ({ defaultValues, onSubmit, label = "Bruker" }: UseUserProfileWriteFormProps) => {
const { isAdmin } = useIsAdminQuery()
const { isAdministrator } = useAuthorization()
const fileUpload = useUserFileUploadMutation()

return useFormBuilder({
Expand All @@ -36,12 +36,12 @@ export const useUserProfileEditForm = ({ defaultValues, onSubmit, label = "Bruke
name: createTextInput({
label: "Navn",
placeholder: "Ola Nordmann",
disabled: isAdmin !== true,
disabled: !isAdministrator,
}),
email: createTextInput({
label: "E-post",
placeholder: "ola.nordmann@gmail.com",
disabled: isAdmin !== true,
disabled: !isAdministrator,
}),
phone: createTextInput({
label: "Telefon",
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/app/(internal)/brukere/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "@tabler/icons-react"
import { useRouter, useSearchParams } from "next/navigation"
import type { FC } from "react"
import { useIsAdminQuery } from "../queries"
import { useAuthorization } from "@/auth/authorization-context"
import { UserEditCard } from "./edit-card"
import { MembershipPage } from "./membership-page"
import { useUserDetailsContext } from "./provider"
Expand Down Expand Up @@ -68,7 +68,7 @@ const SIDEBAR_LINKS = [

export default function UserDetailsPage() {
const { user } = useUserDetailsContext()
const { isAdmin } = useIsAdminQuery()
const { isAdministrator } = useAuthorization()
const router = useRouter()

const searchParams = useSearchParams()
Expand All @@ -89,13 +89,13 @@ export default function UserDetailsPage() {

<Tabs defaultValue={currentTab} onChange={handleTabChange}>
<Tabs.List>
{SIDEBAR_LINKS.filter((link) => !link.isAdmin || isAdmin).map(({ label, icon: Icon, slug }) => (
{SIDEBAR_LINKS.filter((link) => !link.isAdmin || isAdministrator).map(({ label, icon: Icon, slug }) => (
<Tabs.Tab key={slug} value={slug} leftSection={<Icon width={14} height={14} />}>
{label}
</Tabs.Tab>
))}
</Tabs.List>
{SIDEBAR_LINKS.filter((link) => !link.isAdmin || isAdmin).map(({ slug, component: Component }) => (
{SIDEBAR_LINKS.filter((link) => !link.isAdmin || isAdministrator).map(({ slug, component: Component }) => (
<Tabs.Panel mt="md" key={slug} value={slug}>
<Component />
</Tabs.Panel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IconEdit, IconTrash } from "@tabler/icons-react"
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { formatDate } from "date-fns"
import { useMemo } from "react"
import { useIsAdminQuery } from "../queries"
import { useAuthorization } from "@/auth/authorization-context"
import { useConfirmDeleteMembershipModal } from "./confirm-delete-membership-modal"
import { useEditMembershipModal } from "./edit-membership-modal"
import { getStudyGrade, isSpringSemester } from "@dotkomonline/utils"
Expand All @@ -17,7 +17,7 @@ interface Props {
}

export const useMembershipTable = ({ data }: Props) => {
const { isAdmin } = useIsAdminQuery()
const { isAdministrator } = useAuthorization()
const columnHelper = createColumnHelper<Membership>()
const openEditMembershipModal = useEditMembershipModal()
const openDeleteMembershipModal = useConfirmDeleteMembershipModal()
Expand Down Expand Up @@ -80,7 +80,7 @@ export const useMembershipTable = ({ data }: Props) => {
</Button>
),
}),
...(isAdmin
...(isAdministrator
? [
columnHelper.accessor((role) => role, {
id: "delete",
Expand All @@ -100,7 +100,7 @@ export const useMembershipTable = ({ data }: Props) => {
]
: []),
],
[columnHelper, openEditMembershipModal, openDeleteMembershipModal, isAdmin]
[columnHelper, openEditMembershipModal, openDeleteMembershipModal, isAdministrator]
)

return useReactTable({
Expand Down
6 changes: 0 additions & 6 deletions apps/dashboard/src/app/(internal)/brukere/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ export const useGroupAllByMemberQuery = (userId: UserId) => {
return { groups, isLoading }
}

export const useIsAdminQuery = () => {
const trpc = useTRPC()
const { data: isAdmin, isLoading } = useQuery(trpc.user.isAdmin.queryOptions())
return { isAdmin, isLoading }
}

export const useFindWorkspaceUserQuery = (userId: UserId, customKey?: string, enabled = true) => {
const trpc = useTRPC()
const {
Expand Down
18 changes: 9 additions & 9 deletions apps/dashboard/src/app/(internal)/grupper/[id]/edit-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useDebouncedValue } from "@mantine/hooks"
import { IconCheck, IconLink, IconTrash, IconUsersGroup, IconX } from "@tabler/icons-react"
import { useRouter } from "next/navigation"
import { type FC, useEffect, useState } from "react"
import { useIsAdminQuery } from "@/app/(internal)/brukere/queries"
import { useAuthorization } from "@/auth/authorization-context"
import { useDeleteGroupMutation, useLinkGroupMutation, useUpdateGroupMutation } from "../mutations"
import { useFindWorkspaceGroupQuery } from "../queries"
import { useGroupWriteForm } from "../write-form"
Expand All @@ -30,12 +30,12 @@ export const GroupEditCard: FC = () => {
},
})

const { isAdmin } = useIsAdminQuery()
const { isAdministrator } = useAuthorization()

const isWorkspaceLinked = Boolean(group.workspaceGroupId)
const showWorkspaceLink =
isWorkspaceLinked || group.type === "COMMITTEE" || group.type === "NODE_COMMITTEE" || group.type === "EMAIL_ONLY"
const isWorkspaceFetchEnabled = (isAdmin ?? false) && showWorkspaceLink
const isWorkspaceFetchEnabled = isAdministrator && showWorkspaceLink
const { workspaceGroup, isLoading: isLoadingWorkspaceGroup } = useFindWorkspaceGroupQuery(
group.slug,
customKey || undefined,
Expand All @@ -57,7 +57,7 @@ export const GroupEditCard: FC = () => {
<Stack>
<LinkGroup
showWorkspaceLink={showWorkspaceLink}
isAdmin={isAdmin ?? false}
isAdministrator={isAdministrator}
isWorkspaceLinked={isWorkspaceLinked}
isWorkspaceFetchEnabled={isWorkspaceFetchEnabled}
isLoadingWorkspaceGroup={isLoadingWorkspaceGroup}
Expand All @@ -77,7 +77,7 @@ export const GroupEditCard: FC = () => {

interface LinkGroupProps {
showWorkspaceLink: boolean
isAdmin: boolean
isAdministrator: boolean
isWorkspaceLinked: boolean
isWorkspaceFetchEnabled: boolean
isLoadingWorkspaceGroup: boolean
Expand All @@ -88,7 +88,7 @@ interface LinkGroupProps {

const LinkGroup: FC<LinkGroupProps> = ({
showWorkspaceLink,
isAdmin,
isAdministrator,
isWorkspaceLinked,
isWorkspaceFetchEnabled,
isLoadingWorkspaceGroup,
Expand Down Expand Up @@ -152,14 +152,14 @@ const LinkGroup: FC<LinkGroupProps> = ({
)}
</Stack>

{!isWorkspaceLinked && !isAdmin && (
{!isWorkspaceLinked && !isAdministrator && (
<Text size="xs">
Kontakt HS for å tilknytte gruppen til en e-postliste. Gruppen må tilknyttes for å kunne legge medlemmer til i
e-postlisten.
</Text>
)}

{!isWorkspaceLinked && isAdmin && (
{!isWorkspaceLinked && isAdministrator && (
<TextInput
description="Egendefinert nøkkel. Bruk denne om den ikke finner automatisk. Må være en komplett e-postadresse eller lokaldelen til e-postadressen (det før @)."
placeholder="dotkom@online.ntnu.no eller dotkom"
Expand Down Expand Up @@ -215,7 +215,7 @@ const LinkGroup: FC<LinkGroupProps> = ({
<Button
color="green"
w="fit-content"
disabled={!isAdmin || !workspaceGroup}
disabled={!isAdministrator || !workspaceGroup}
leftSection={<IconLink size={16} />}
onClick={onClick}
>
Expand Down
7 changes: 4 additions & 3 deletions apps/dashboard/src/app/ApplicationShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
IconUsersGroup,
IconWheelchair,
} from "@tabler/icons-react"
import { useAuthorization } from "@/auth/authorization-context"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { type FC, useEffect } from "react"
Expand Down Expand Up @@ -119,11 +120,11 @@ const navigations = [
isAdmin?: boolean
}[]
interface ApplicationShellProps {
isAdmin: boolean
children: React.ReactNode
}

export const ApplicationShell: FC<ApplicationShellProps> = ({ isAdmin, children }) => {
export const ApplicationShell: FC<ApplicationShellProps> = ({ children }) => {
const { isAdministrator } = useAuthorization()
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
const pathname = usePathname()
Expand Down Expand Up @@ -190,7 +191,7 @@ export const ApplicationShell: FC<ApplicationShellProps> = ({ isAdmin, children
</AppShellHeader>
<AppShellNavbar p="md">
{navigations
.filter((navigation) => isAdmin || !navigation.isAdmin)
.filter((navigation) => isAdministrator || !navigation.isAdmin)
.map((navigation) => (
<NavLink
component={Link}
Expand Down
21 changes: 19 additions & 2 deletions apps/dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { nb } from "date-fns/locale"
import type { Metadata } from "next"
import PlausibleProvider from "next-plausible"
import type { PropsWithChildren } from "react"
import { AuthorizationProvider } from "@/auth/authorization-context"
import { ApplicationShell } from "./ApplicationShell"
import { ModalProvider } from "./ModalProvider"
import { QueryProvider } from "./QueryProvider"
import type { GroupId, GroupRoleType } from "@dotkomonline/types"

setDateFnsDefaultOptions({ locale: nb })

Expand Down Expand Up @@ -51,7 +53,16 @@ export default async function RootLayout({ children }: PropsWithChildren) {
const accessToken = await getServerAccessToken()
// Hide the Auth0 user from the client when no usable token exists, so a stale cookie is not treated as logged-in.
const auth0User = accessToken !== null && session?.user !== undefined ? session.user : undefined
const isAdmin = accessToken !== null ? await server.user.isAdmin.query() : false

const { isAdministrator, isCommitteeMember, affiliations } =
accessToken !== null
? await server.user.getAuthorization.query()
: { isAdministrator: false, isCommitteeMember: false, affiliations: {} }

const affiliationsMap = new Map<GroupId, Set<GroupRoleType>>()
for (const [groupId, roles] of Object.entries(affiliations)) {
affiliationsMap.set(groupId, new Set(roles))
}

return (
<html lang="no" {...mantineHtmlProps}>
Expand All @@ -65,7 +76,13 @@ export default async function RootLayout({ children }: PropsWithChildren) {
<MantineProvider defaultColorScheme="auto" theme={theme}>
<Notifications />
<ModalProvider>
<ApplicationShell isAdmin={isAdmin}>{children}</ApplicationShell>
<AuthorizationProvider
isAdministrator={isAdministrator}
isCommitteeMember={isCommitteeMember}
affiliations={affiliationsMap}
>
<ApplicationShell>{children}</ApplicationShell>
</AuthorizationProvider>
</ModalProvider>
</MantineProvider>
</QueryProvider>
Expand Down
37 changes: 37 additions & 0 deletions apps/dashboard/src/auth/authorization-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client"

import type { GroupId, GroupRoleType } from "@dotkomonline/types"
import { createContext, useContext, useMemo } from "react"
import type { PropsWithChildren } from "react"

interface AuthorizationContextValue {
isAdministrator: boolean
isCommitteeMember: boolean
affiliations: Map<GroupId, Set<GroupRoleType>>
}

const AuthorizationContext = createContext<AuthorizationContextValue | null>(null)

export function AuthorizationProvider({ children, ...value }: PropsWithChildren<AuthorizationContextValue>) {
return <AuthorizationContext.Provider value={value}>{children}</AuthorizationContext.Provider>
}

export function useAuthorization() {
const ctx = useContext(AuthorizationContext)

if (ctx === null) {
throw new Error("useAuthorization must be used within AuthorizationProvider")
}

return useMemo(
() => ({
isAdministrator: ctx.isAdministrator,
isCommitteeMember: ctx.isCommitteeMember,
affiliations: ctx.affiliations,
isGroupMember: (groupId: GroupId) => ctx.isAdministrator || ctx.affiliations.has(groupId),
hasGroupRole: (groupId: GroupId, role: GroupRoleType) =>
ctx.isAdministrator || (ctx.affiliations.get(groupId)?.has(role) ?? false),
}),
[ctx]
)
}
Loading
Loading