diff --git a/src/app/_components/VisiblityAdmin/VisibilityAdmin.module.scss b/src/app/_components/VisiblityAdmin/VisibilityAdmin.module.scss index 46143ec5c..f5daa86a7 100644 --- a/src/app/_components/VisiblityAdmin/VisibilityAdmin.module.scss +++ b/src/app/_components/VisiblityAdmin/VisibilityAdmin.module.scss @@ -1,15 +1,5 @@ @use '@/styles/ohma'; .VisibilityAdmin { - @include ohma.card; - background-color: ohma.$colors-secondary; - width: 100%; - .info { - margin-bottom: .5em; - } - .borderBottom { - border-bottom: 5px solid ohma.$colors-gray-900; - margin-bottom: .5em; - width: 100%; - } + } \ No newline at end of file diff --git a/src/app/_components/VisiblityAdmin/VisibilityAdmin.tsx b/src/app/_components/VisiblityAdmin/VisibilityAdmin.tsx index bd940e11f..2ef4479c7 100644 --- a/src/app/_components/VisiblityAdmin/VisibilityAdmin.tsx +++ b/src/app/_components/VisiblityAdmin/VisibilityAdmin.tsx @@ -1,40 +1,17 @@ 'use client' import styles from './VisibilityAdmin.module.scss' -import VisibilityLevelAdmin from './VisibilityLevelAdmin' -import useActionCall from '@/hooks/useActionCall' -import { readVisibilityForAdminAction } from '@/services/visibility/actions' -import { useCallback } from 'react' +import type { VisibilityMatrix } from '@/services/visibility/types' + type PropTypes = { - visibilityId: number + visibility: VisibilityMatrix } - -export default function VisibilityAdmin({ visibilityId }: PropTypes) { - const action = useCallback(() => readVisibilityForAdminAction(visibilityId), [visibilityId]) - const { data } = useActionCall(action) - console.log(data) - if (!data) return null +export default function VisibilityAdmin({ visibility }: PropTypes) { + console.log(visibility) return (
-
-

Administrer synelighet

- Synelighet for: {data.purpose} -
- { - data.type === 'REGULAR' ? (<> -
- -
-
- -
- ) : (<> -

{data.message}

-

{data.regular}

-

{data.admin}

- ) - } +

Synelighet!

) } diff --git a/src/app/_components/VisiblityAdmin/VisibilityLevelAdmin.module.scss b/src/app/_components/VisiblityAdmin/VisibilityLevelAdmin.module.scss deleted file mode 100644 index 5a65d71d6..000000000 --- a/src/app/_components/VisiblityAdmin/VisibilityLevelAdmin.module.scss +++ /dev/null @@ -1,9 +0,0 @@ -@use '@/styles/ohma'; - -.VisibilityLevelAdmin { - .requierments { - display: flex; - width: 100%; - justify-content: space-between; - } -} \ No newline at end of file diff --git a/src/app/_components/VisiblityAdmin/VisibilityLevelAdmin.tsx b/src/app/_components/VisiblityAdmin/VisibilityLevelAdmin.tsx deleted file mode 100644 index 6c7cd03ea..000000000 --- a/src/app/_components/VisiblityAdmin/VisibilityLevelAdmin.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import styles from './VisibilityLevelAdmin.module.scss' -import Form from '@/components/Form/Form' -import { updateVisibilityAction } from '@/services/visibility/actions' -import type { VisibilityLevelType, VisibilityRequiermentForAdmin } from '@/services/visibility/types' - -type PropTypes = { - data: VisibilityRequiermentForAdmin[] - level: VisibilityLevelType - levelName: string -} -export default function VisibilityLevelAdmin({ level, data, levelName }: PropTypes) { - return ( -
-

{levelName}

-
- { - data.map(requiement => -
-

{requiement.name}

- { - requiement.groups.map(group => -

{group.name}

- ) - } -
- ) - } -
-
- ) -} diff --git a/src/app/articles/[category]/EditCategory.tsx b/src/app/articles/[category]/EditCategory.tsx index bee6c69f4..770dcba2a 100644 --- a/src/app/articles/[category]/EditCategory.tsx +++ b/src/app/articles/[category]/EditCategory.tsx @@ -20,7 +20,7 @@ type PropTypes = { export default function EditCategory({ category }: PropTypes) { const { refresh, push } = useRouter() - // Make a visibility check for edit + // Make a visibility check for edit - no just call the apropriate auther. const canEditCategory = true const handleSuccessDestroy = () => { diff --git a/src/app/articles/[category]/SideBar.tsx b/src/app/articles/[category]/SideBar.tsx index 791a3a2fe..a662fbe13 100644 --- a/src/app/articles/[category]/SideBar.tsx +++ b/src/app/articles/[category]/SideBar.tsx @@ -74,7 +74,7 @@ export default function SideBar({ category, children }: PropTypes) { } function MainListContent({ category }: { category: ExpandedArticleCategory }) { - // Make a visibility check for edit + // Make a visibility check for edit - no just call the apropriate auther. const canEditCategory = true const { push, refresh } = useRouter() diff --git a/src/app/images/collections/[id]/CollectionAdmin.tsx b/src/app/images/collections/[id]/CollectionAdmin.tsx index 702b53fa1..5aa408c33 100644 --- a/src/app/images/collections/[id]/CollectionAdmin.tsx +++ b/src/app/images/collections/[id]/CollectionAdmin.tsx @@ -7,28 +7,27 @@ import TextInput from '@/components/UI/TextInput' import { ImagePagingContext } from '@/contexts/paging/ImagePaging' import ImageUploader from '@/components/Image/ImageUploader' import useEditing from '@/hooks/useEditing' -import VisibilityAdmin from '@/components/VisiblityAdmin/VisibilityAdmin' import PopUp from '@/components/PopUp/PopUp' import Button from '@/components/UI/Button' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCog, faEye, faUpload } from '@fortawesome/free-solid-svg-icons' import { useRouter } from 'next/navigation' import { useContext, useState } from 'react' -import type { VisibilityCollapsed } from '@/services/visibility/types' +import type { VisibilityMatrix } from '@/services/visibility/types' import type { ExpandedImageCollection } from '@/services/images/collections/types' type PropTypes = { collection: ExpandedImageCollection - visibility: VisibilityCollapsed + visibilityAdmin: VisibilityMatrix + visibilityRead: VisibilityMatrix } -export default function CollectionAdmin({ collection, visibility }: PropTypes) { +export default function CollectionAdmin({ collection, visibilityAdmin, visibilityRead }: PropTypes) { + console.log(visibilityAdmin, visibilityRead) const { id: collectionId } = collection const router = useRouter() const pagingContext = useContext(ImagePagingContext) - const canEdit = useEditing({ - requiredVisibility: visibility, - }) + const canEdit = useEditing({}) //TODO: pass in auther const [uploadOption, setUploadOption] = useState<'MANY' | 'ONE'>('MANY') if (!canEdit) return null @@ -112,7 +111,7 @@ export default function CollectionAdmin({ collection, visibility }: PropTypes) { }>
- + {/* VisibilityAdmin... */}
diff --git a/src/app/images/collections/[id]/page.tsx b/src/app/images/collections/[id]/page.tsx index c11c4c491..de75a2d0f 100644 --- a/src/app/images/collections/[id]/page.tsx +++ b/src/app/images/collections/[id]/page.tsx @@ -8,6 +8,7 @@ import { readImageCollectionAction } from '@/services/images/collections/actions import { readImagesPageAction } from '@/services/images/actions' import { notFound } from 'next/navigation' import type { PageSizeImage } from '@/contexts/paging/ImagePaging' +import type { VisibilityMatrix } from '@/services/visibility/types' type PropTypes = { params: Promise<{ @@ -51,7 +52,11 @@ export default async function Collection({ params }: PropTypes) { images.map(image => ) } /> - + diff --git a/src/auth/auther/RequireVisibility.ts b/src/auth/auther/RequireVisibility.ts index 86e778925..8d5f06476 100644 --- a/src/auth/auther/RequireVisibility.ts +++ b/src/auth/auther/RequireVisibility.ts @@ -1,14 +1,14 @@ import { AutherFactory } from './Auther' -import { checkVisibility } from '@/auth/checkVisibility' -import type { VisibilityCollapsed } from '@/services/visibility/types' +import { checkVisibility } from '@/auth/visibility/checkVisibility' +import type { VisibilityMatrix } from '@/services/visibility/types' import type { Permission } from '@prisma/client' export const RequireVisibility = AutherFactory< { bypassPermission: Permission }, - { visibility: VisibilityCollapsed }, + { visibility: VisibilityMatrix }, 'USER_NOT_REQUIERED_FOR_AUTHORIZED' > (({ session, dynamicFields, staticFields }) => ({ - success: checkVisibility(session, dynamicFields.visibility, 'REGULAR') || + success: checkVisibility(session.memberships, dynamicFields.visibility) || session.permissions.includes(staticFields.bypassPermission), session, })) diff --git a/src/auth/checkVisibility.ts b/src/auth/checkVisibility.ts deleted file mode 100644 index f0529543d..000000000 --- a/src/auth/checkVisibility.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { BypassPermissions } from '@/services/visibility/ConfigVars' -import type { MembershipFiltered } from '@/services/groups/memberships/types' -import type { Permission } from '@prisma/client' -import type { GroupMatrix, VisibilityCollapsed, VisibilityLevelType } from '@/services/visibility/types' - -type MembershipAndPermission = { - memberships: MembershipFiltered[], - permissions: Permission[] -} - -/** - * Check if a user meets the visibility requirements of a visibility - * @param memberships - the memberships of the user. Remember if using getUser and there is no user - * getUser and useUser will return an empty array - * @param permissions - the permissions of the user. Remember if using getUser and there is no usee - * getUser and useUser will return an empty array. Used if type of visibility is SPECIAL - * @param visibility - the visibility to require - * @param level - the level of visibility to require - * @returns - true if the user meets the visibility requirements, false otherwise - */ -export function checkVisibility({ - memberships, - permissions -}: MembershipAndPermission, -visibility: VisibilityCollapsed, -level: VisibilityLevelType, -) { - const bypassPermission = BypassPermissions[visibility.purpose] - if (bypassPermission && permissions.includes(bypassPermission)) return true - if (visibility.type === 'REGULAR' && !visibility.published) return false - if (visibility.type === 'SPECIAL') { - if (level === 'REGULAR') { - //null permission means no permission required - return visibility.regular ? permissions.includes(visibility.regular) : true - } - //null permission means no permission required - return visibility.admin ? permissions.includes(visibility.admin) : true - } - return checkVisibilityATLevel(memberships, visibility[level === 'REGULAR' ? 'regular' : 'admin']) -} - -function checkVisibilityATLevel(memberships: MembershipFiltered[], visibilityLevel: GroupMatrix) { - if (!visibilityLevel.length) return true - return visibilityLevel.every(requirement => - requirement.some( - groupId => memberships.some( - membership => membership.groupId === groupId - ) - ) - ) -} diff --git a/src/auth/getVisibilityFilter.ts b/src/auth/getVisibilityFilter.ts deleted file mode 100644 index 8eec8d04c..000000000 --- a/src/auth/getVisibilityFilter.ts +++ /dev/null @@ -1,118 +0,0 @@ -import '@pn-server-only' -import { BypassPermissions } from '@/services/visibility/ConfigVars' -import type { MembershipFiltered } from '@/services/groups/memberships/types' -import type { Permission, Prisma, VisibilityPurpose } from '@prisma/client' - -function userMayBypassVisibilityBasedOnPermission( - permissions: Permission[], - purpose: VisibilityPurpose, -) { - const bypassPermissionForPurpose = BypassPermissions[purpose] - if (!bypassPermissionForPurpose) return false - return permissions.includes(bypassPermissionForPurpose) -} - -function isVisibilityPurpose(purpose: string): purpose is VisibilityPurpose { - return Object.keys(BypassPermissions).includes(purpose) -} - -/** - * Creates a where-filter that can be used in db queries to only return - * items that mach the users groups (or permission if type is special). - * This should be used for regularVisibility level. - * @param groups - The groups the user is a member of - * @param permissions - The permissions the user has - * @returns - A where-filter that can be used in db queries. Used n query as - * ```where: getVisibilityFilter(user.memberships, user.permissions)``` - */ -export function getVisibilityFilter( - groups: MembershipFiltered[] | undefined, - permissions: Permission[], -) { - const groupIds = groups ? groups.map(group => group.groupId) : [] - - const bypassers = Object.keys(BypassPermissions).reduce((acc, purpose) => { - if (!isVisibilityPurpose(purpose)) return acc - if (!userMayBypassVisibilityBasedOnPermission(permissions, purpose)) return acc - acc.push({ - visibility: { - published: true, - purpose, - } - }) - return acc - }, [] as { - visibility: { - published: true, - purpose: VisibilityPurpose - } - }[] - ) - console.log(permissions) - return { - OR: [ - // A user has access if it has the bypass permission for spesific purpose - ...bypassers, - - // A user has access if the visibility is special and the user has the permission or the - // permission is null - { - visibility: { - published: true, - specialPurpose: { - not: null - }, - regularLevel: { - permission: { - in: permissions - } - } - } - }, - { - visibility: { - published: true, - specialPurpose: { - not: null - }, - regularLevel: { - permission: null - } - } - }, - - // If the visibility is not special, the user has access if it - // meets the requirements of the visibility. Or if there are no requirements - { - visibility: { - published: true, - specialPurpose: null, - regularLevel: { - OR: [ - { - requirements: { - some: { - visibilityRequirmenetGroups: { - some: { - groupId: { - in: groupIds - } - } - } - } - } - }, - { - requirements: { - none: {} - } - } - ] - } - } - } - ] - } satisfies Prisma.ImageCollectionWhereInput -} - -export type VisibilityFilter = ReturnType diff --git a/src/auth/visibility/checkVisibility.ts b/src/auth/visibility/checkVisibility.ts new file mode 100644 index 000000000..7cbe43a97 --- /dev/null +++ b/src/auth/visibility/checkVisibility.ts @@ -0,0 +1,23 @@ +import type { MembershipFiltered } from '@/services/groups/memberships/types' +import type { VisibilityMatrix } from '@/services/visibility/types' + + +export function checkVisibility(memberships: MembershipFiltered[], visibility: VisibilityMatrix): boolean { + return visibility.requirements.every( + requirement => requirement.conditions.some( + condition => { + if (condition.type === 'ACTIVE') { + return memberships.some( + membership => membership.groupId === condition.groupId && membership.active + ) + } + if (condition.type === 'ORDER') { + return memberships.some( + membership => membership.groupId === condition.groupId && membership.order === condition.order + ) + } + return false + } + ) + ) +} diff --git a/src/auth/visibility/isSubVisibility.ts b/src/auth/visibility/isSubVisibility.ts new file mode 100644 index 000000000..caafa78b6 --- /dev/null +++ b/src/auth/visibility/isSubVisibility.ts @@ -0,0 +1,36 @@ +import type { VisibilityMatrix, VisibilityRequirement } from '@/services/visibility/types' + +/** + * This function checks if any session (user) satisfying `visibilitySub` is also necessarily + * satisfies `visibilitySuper` + * session element in visibilitySub => session element in visibilitySuper + * If the return is true the implication holds else it does not (necessarily) hold + * + * For this to be true every requirement in the `visibilitySuper` must be a super-requirement of + * some requirement in the `visibilitySub` + * @param visibilitySub + * @param visibilitySuper + */ +export function isSubVisibility(visibilitySub: VisibilityMatrix, visibilitySuper: VisibilityMatrix): boolean { + return visibilitySuper.requirements.every( + superRequirement => visibilitySub.requirements.some( + subRequirement => isSubRequirement(subRequirement, superRequirement) + ) + ) +} + +function isSubRequirement(requirementSub: VisibilityRequirement, requirementSuper: VisibilityRequirement): boolean { + return requirementSub.conditions.every( + conditionSub => requirementSuper.conditions.some( + conditionSuper => { + if (conditionSub.groupId !== conditionSuper.groupId) return false + if (conditionSub.type === 'ACTIVE' && conditionSuper.type === 'ACTIVE') return true + if (conditionSub.type === 'ORDER' && conditionSuper.type === 'ORDER') { + return conditionSub.order === conditionSuper.order + } + // Condition types do not match.... + return false + } + ) + ) +} diff --git a/src/auth/visibility/visibilityFilter.ts b/src/auth/visibility/visibilityFilter.ts new file mode 100644 index 000000000..28457812c --- /dev/null +++ b/src/auth/visibility/visibilityFilter.ts @@ -0,0 +1,33 @@ +import type { MembershipFiltered } from '@/services/groups/memberships/types' +import type { Prisma } from '@prisma/client' + +export function visibilityFilter(memberships: MembershipFiltered[]) { + return { + requirements: { + every: { + conditions: { + some: { + OR: [ + { + type: 'ACTIVE', + groupId: { + in: memberships + .filter(membership => membership.active) + .map(membership => membership.groupId) + } + }, + { + type: 'ORDER', + OR: memberships + .map(membership => ({ + groupId: membership.groupId, + order: membership.order + })) + }, + ] + } + } + } + } + } as const satisfies Prisma.VisibilityWhereInput +} diff --git a/src/hooks/useEditing.ts b/src/hooks/useEditing.ts index b18e0478c..8fc1f5553 100644 --- a/src/hooks/useEditing.ts +++ b/src/hooks/useEditing.ts @@ -1,13 +1,15 @@ 'use client' import { useUser } from '@/auth/session/useUser' import { EditModeContext } from '@/contexts/EditMode' -import { checkVisibility } from '@/auth/checkVisibility' import { useContext, useEffect, useRef, useState } from 'react' import { v4 as uuid } from 'uuid' import type { Permission } from '@prisma/client' import type { Matrix } from '@/lib/checkMatrix' -import type { VisibilityCollapsed, VisibilityLevelType } from '@/services/visibility/types' +//TODO: Change this to take in session and a auther +//useUser should return a session. +// This function does way to many things... +// just use the auther system... /** * A hook that uses useUser to determine if the user is allowed to edit the content. * If the user is allowed to edit the content, the hook will add the content to the list of editable content @@ -24,14 +26,8 @@ import type { VisibilityCollapsed, VisibilityLevelType } from '@/services/visibi */ export default function useEditing({ requiredPermissions, - requiredVisibility, - operation = 'OR', - level = 'ADMIN' }: { requiredPermissions?: Matrix, - requiredVisibility?: VisibilityCollapsed - operation?: 'AND' | 'OR', - level?: VisibilityLevelType }): boolean { const editModeCtx = useContext(EditModeContext) const { authorized: permissionAuthorized, permissions, memberships } = useUser({ @@ -44,14 +40,7 @@ export default function useEditing({ const uniqueKey = useRef(uuid()).current useEffect(() => { - const visibilityAuthorized = requiredVisibility ? checkVisibility({ - permissions: permissions ?? [], - memberships: memberships ?? [], - }, requiredVisibility, level) : undefined - - const authorized_ = (operation === 'OR' ? - permissionAuthorized || visibilityAuthorized : - permissionAuthorized && visibilityAuthorized) ?? false + const authorized_ = permissionAuthorized === true if (editModeCtx) { if (authorized_) editModeCtx.addEditableContent(uniqueKey) if (!authorized_) editModeCtx.removeEditableContent(uniqueKey) @@ -60,7 +49,7 @@ export default function useEditing({ return () => { if (editModeCtx) editModeCtx.removeEditableContent(uniqueKey) } - }, [permissionAuthorized, requiredVisibility, permissions, memberships]) + }, [permissionAuthorized, permissions, memberships]) useEffect(() => { setEditable(editModeCtx ? Boolean(authorized) && editModeCtx.editMode : false) diff --git a/src/prisma/schema/cms.prisma b/src/prisma/schema/cms.prisma index 3e0024b69..cf03ead68 100644 --- a/src/prisma/schema/cms.prisma +++ b/src/prisma/schema/cms.prisma @@ -64,8 +64,8 @@ model CmsParagraph { Course Course[] School School[] Event Event[] - Committee Committee? @relation(name: "committeeParagraph") - CommitteApplication Committee? @relation(name: "applicationParagraph") + Committee Committee? @relation(name: "committeeParagraph") + CommitteApplication Committee? @relation(name: "applicationParagraph") } enum SpecialCmsLink { @@ -103,48 +103,47 @@ model ArticleSection { //The order "position" of the article section in the article //This field is only used when articleSection is part of an article - order Int @default(autoincrement()) + order Int @default(autoincrement()) - - cmsImage CmsImage? - cmsParagraph CmsParagraph? - cmsLink CmsLink? - InterestGroup InterestGroup? + cmsImage CmsImage? + cmsParagraph CmsParagraph? + cmsLink CmsLink? + InterestGroup InterestGroup? @@unique([articleId, order]) //There can only be one article section with a given order in an article } -enum SpecialCmsArticle{ +enum SpecialCmsArticle { REPORT_PAGE NEW_STUDENT_PAGE } model Article { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt articleSections ArticleSection[] newsArticle NewsArticle? - articleCategory ArticleCategory? @relation(fields: [articleCategoryId, articleCategoryName], references: [id, name], onDelete: Cascade) + articleCategory ArticleCategory? @relation(fields: [articleCategoryId, articleCategoryName], references: [id, name], onDelete: Cascade) articleCategoryId Int? articleCategoryName String? - coverImage CmsImage @relation(fields: [coverImageId], references: [id], onDelete: Restrict) - coverImageId Int @unique + coverImage CmsImage @relation(fields: [coverImageId], references: [id], onDelete: Restrict) + coverImageId Int @unique JobAd JobAd? Committee Committee? - special SpecialCmsArticle? @unique + special SpecialCmsArticle? @unique @@unique([name, id]) } model NewsArticle { - id Int @id @default(autoincrement()) - description String? - endDateTime DateTime //when the article is no longer considered current - article Article @relation(fields: [articleId, articleName], references: [id, name], onDelete: Restrict) - articleId Int @unique - articleName String + id Int @id @default(autoincrement()) + description String? + endDateTime DateTime //when the article is no longer considered current + article Article @relation(fields: [articleId, articleName], references: [id, name], onDelete: Restrict) + articleId Int @unique + articleName String @@unique([articleId, articleName]) } diff --git a/src/prisma/schema/group.prisma b/src/prisma/schema/group.prisma index 53e8d9c8d..e9008d7ac 100644 --- a/src/prisma/schema/group.prisma +++ b/src/prisma/schema/group.prisma @@ -6,7 +6,7 @@ model Membership { admin Boolean active Boolean title String @default("Medlem") - omegaOrder OmegaOrder @relation(fields: [order], references: [order]) + omegaOrder OmegaOrder @relation(fields: [order], references: [order], onDelete: Restrict) order Int @@unique([userId, groupId, order]) @@ -34,7 +34,7 @@ model Group { id Int @id @default(autoincrement()) groupType GroupType memberships Membership[] - omegaOrder OmegaOrder @relation(fields: [order], references: [order]) + omegaOrder OmegaOrder @relation(fields: [order], references: [order], onDelete: Restrict) order Int //The order the group is in currently. class Class? @@ -46,7 +46,7 @@ model Group { LockerReservation LockerReservation[] mailingLists MailingListGroup[] - VisibilityRequirmenetGroup VisibilityRequirmenetGroup[] + VisibilityRequirementGroup VisibilityRequirementGroup[] permissions GroupPermission[] } diff --git a/src/prisma/schema/image.prisma b/src/prisma/schema/image.prisma index 3a2d0aaca..f4a61333e 100644 --- a/src/prisma/schema/image.prisma +++ b/src/prisma/schema/image.prisma @@ -64,8 +64,11 @@ model ImageCollection { special SpecialCollection? @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - visibility Visibility @relation(fields: [visibilityId], references: [id]) - visibilityId Int @unique + + visibilityAdmin Visibility @relation(name: "ImageCollectionAdminVisibility", fields: [visibilityAdminId], references: [id], onDelete: Restrict) + visibilityAdminId Int @unique + visibilityRead Visibility @relation(name: "ImageCollectionReadVisibility", fields: [visibilityReadId], references: [id], onDelete: Restrict) + visibilityReadId Int @unique } enum ImageSize { diff --git a/src/prisma/schema/omegaorder.prisma b/src/prisma/schema/omegaorder.prisma index aa30f0d91..b543ff79c 100644 --- a/src/prisma/schema/omegaorder.prisma +++ b/src/prisma/schema/omegaorder.prisma @@ -3,6 +3,7 @@ model OmegaOrder { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - memberships Membership[] - groups Group[] + memberships Membership[] + groups Group[] + VisibilityRequirementGroup VisibilityRequirementGroup[] } diff --git a/src/prisma/schema/permission.prisma b/src/prisma/schema/permission.prisma index 918d9021a..5ab31fbe3 100644 --- a/src/prisma/schema/permission.prisma +++ b/src/prisma/schema/permission.prisma @@ -193,4 +193,3 @@ model GroupPermission { model DefaultPermission { permission Permission @unique } - diff --git a/src/prisma/schema/visibility.prisma b/src/prisma/schema/visibility.prisma index 1b74d7a64..af41a3539 100644 --- a/src/prisma/schema/visibility.prisma +++ b/src/prisma/schema/visibility.prisma @@ -1,54 +1,34 @@ -enum VisibilityPurpose { - IMAGE - EVENT - ARTICLE_CATEGORY - NEWS_ARTICLE - SPECIAL -} - -enum SpecialVisibilityPurpose { - OMBUL - COMMITTEE - USER - PUBLIC -} - model Visibility { - id Int @id @default(autoincrement()) - imageCollection ImageCollection? - published Boolean @default(false) - purpose VisibilityPurpose - specialPurpose SpecialVisibilityPurpose? @unique + id Int @id @default(autoincrement()) + requirements VisibilityRequirement[] // You must fullfill all requirements + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - regularLevel VisibilityLevel @relation(fields: [regularLevelId], references: [id], name: "regularLevel") - regularLevelId Int @unique - adminLevel VisibilityLevel @relation(fields: [adminLevelId], references: [id], name: "adminLevel") - adminLevelId Int @unique + imageCollectionAdmin ImageCollection? @relation(name: "ImageCollectionAdminVisibility") + imageCollectionRead ImageCollection? @relation(name: "ImageCollectionReadVisibility") } -model VisibilityLevel { - id Int @id @default(autoincrement()) - permission Permission? //Used if type of the Visibility is SPECIAL. (If null everybody will have access at this level) - requirements VisibilityRequirement[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - regularVisibility Visibility? @relation(name: "regularLevel") - adminVisibility Visibility? @relation(name: "adminLevel") +model VisibilityRequirement { + id Int @id @default(autoincrement()) + visibility Visibility @relation(fields: [visibilityId], references: [id], onDelete: Cascade) + visibilityId Int + conditions VisibilityRequirementGroup[] //must be in one of these to fullfill this requirement } -model VisibilityRequirement { - id Int @id @default(autoincrement()) - visibility VisibilityLevel @relation(fields: [visibilityId], references: [id], onDelete: Cascade) - visibilityId Int - visibilityRequirmenetGroups VisibilityRequirmenetGroup[] +enum VisibilityRequirementGroupType { + ACTIVE + ORDER } -model VisibilityRequirmenetGroup { - id Int @id @default(autoincrement()) - visibilityRequirmenet VisibilityRequirement @relation(fields: [visibilityRequirmenetId], references: [id], onDelete: Cascade) - visibilityRequirmenetId Int - group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) +model VisibilityRequirementGroup { + id Int @id @default(autoincrement()) + visibilityRequirement VisibilityRequirement @relation(fields: [visibilityRequirementId], references: [id], onDelete: Cascade) + visibilityRequirementId Int + group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) groupId Int + type VisibilityRequirementGroupType + omegaOrder OmegaOrder @relation(fields: [order], references: [order], onDelete: Restrict) + order Int - @@unique([visibilityRequirmenetId, groupId]) + @@unique([visibilityRequirementId, groupId, order]) } diff --git a/src/prisma/seeder/src/SeedSpecialImageCollections.ts b/src/prisma/seeder/src/SeedSpecialImageCollections.ts index 443fa1f76..a37dc51e4 100644 --- a/src/prisma/seeder/src/SeedSpecialImageCollections.ts +++ b/src/prisma/seeder/src/SeedSpecialImageCollections.ts @@ -1,27 +1,12 @@ import { SpecialCollection } from '@prisma/client' -import type { PrismaClient, SpecialVisibilityPurpose } from '@prisma/client' - -export const specialCollectionsVisibility = { - OMBULCOVERS: { - specialVisibility: 'OMBUL' - }, - PROFILEIMAGES: { - specialVisibility: 'USER' - }, - STANDARDIMAGES: { - specialVisibility: 'PUBLIC' - }, - COMMITTEELOGOS: { - specialVisibility: 'COMMITTEE' - } -} satisfies {[CollectionType in SpecialCollection]: { - specialVisibility: SpecialVisibilityPurpose -}} +import type { PrismaClient } from '@prisma/client' export default async function SeedSpecialImageCollections(prisma: PrismaClient) { const keys = Object.keys(SpecialCollection) as SpecialCollection[] - await Promise.all(keys.map((special) => - prisma.imageCollection.upsert({ + await Promise.all(keys.map(async (special) => { + const visibilityAdmin = await prisma.visibility.create({ data: {} }) + const visibilityRead = await prisma.visibility.create({ data: {} }) + return await prisma.imageCollection.upsert({ where: { name: special }, @@ -31,12 +16,9 @@ export default async function SeedSpecialImageCollections(prisma: PrismaClient) create: { name: special, special, - visibility: { - connect: { - specialPurpose: specialCollectionsVisibility[special].specialVisibility - } - } + visibilityRead: { connect: { id: visibilityRead.id } }, + visibilityAdmin: { connect: { id: visibilityAdmin.id } } } }) - )) + })) } diff --git a/src/prisma/seeder/src/development/seedDevImages.ts b/src/prisma/seeder/src/development/seedDevImages.ts index 798a84b77..85280cadd 100644 --- a/src/prisma/seeder/src/development/seedDevImages.ts +++ b/src/prisma/seeder/src/development/seedDevImages.ts @@ -12,13 +12,11 @@ export default async function seedDevImages(prisma: PrismaClient) { create: { name: `test_collection_${i}`, description: 'just a test', - visibility: { - create: { - purpose: 'IMAGE', - published: true, - regularLevel: { create: {} }, - adminLevel: { create: {} }, - } + visibilityAdmin: { + create: {} + }, + visibilityRead: { + create: {} } } }) diff --git a/src/prisma/seeder/src/dobbelOmega/migrateImageCollections.ts b/src/prisma/seeder/src/dobbelOmega/migrateImageCollections.ts index 4eeb756d5..966549318 100644 --- a/src/prisma/seeder/src/dobbelOmega/migrateImageCollections.ts +++ b/src/prisma/seeder/src/dobbelOmega/migrateImageCollections.ts @@ -34,13 +34,11 @@ export default async function migrateImageCollections(pnPrisma: PrismaClientPn, createdAt: imageCollection.updatedAt, updatedAt: imageCollection.updatedAt, //TODO: Link to right committee through visibility - visibility: { - create: { - purpose: 'IMAGE', - published: true, - regularLevel: { create: {} }, - adminLevel: { create: {} } - } + visibilityRead: { + create: {} //Assuming all collections from vevn to be public on migration is probably fine + }, + visibilityAdmin: { + create: {} //TODO: not everyone should be able to update this.... } } }) diff --git a/src/prisma/seeder/src/dobbelOmega/migrateImages.ts b/src/prisma/seeder/src/dobbelOmega/migrateImages.ts index 8f70a248a..c97d2d258 100644 --- a/src/prisma/seeder/src/dobbelOmega/migrateImages.ts +++ b/src/prisma/seeder/src/dobbelOmega/migrateImages.ts @@ -33,18 +33,12 @@ export default async function migrateImages( update: {}, create: { name: 'Søppel fra Veven', - description: 'Denne samlingen inneholder bilder som ikke tilhører noen samling', - visibility: { - create: { - purpose: 'IMAGE', - published: true, - regularLevel: { - create: {} - }, - adminLevel: { - create: {} - }, - } + description: 'Denne samlingen inneholder bilder som ikke tilhørete noen samling i omegaweb-basic', + visibilityRead: { + create: {}, + }, + visibilityAdmin: { + create: {} //TODO: Require vevcom or something... } }, }) diff --git a/src/prisma/seeder/src/seedSpecialVisibility.ts b/src/prisma/seeder/src/seedSpecialVisibility.ts deleted file mode 100644 index 486f1ed34..000000000 --- a/src/prisma/seeder/src/seedSpecialVisibility.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Permission, SpecialVisibilityPurpose, PrismaClient } from '@prisma/client' - -const SpecialVisibilityConfig = { - OMBUL: { - regularLevel: 'OMBUL_READ', - adminLevel: 'OMBUL_CREATE' - }, - PUBLIC: { - regularLevel: null, - adminLevel: 'FRONTPAGE_ADMIN' // just changed this to make ts happy - // bypass and purposes will be removed - }, - COMMITTEE: { - regularLevel: 'COMMITTEE_READ', - adminLevel: 'COMMITTEE_CREATE' - }, - USER: { - regularLevel: 'USERS_READ', - adminLevel: 'USERS_CREATE' - } -} satisfies {[VisibilityType in SpecialVisibilityPurpose]: { - regularLevel: Permission | null, - adminLevel: Permission | null -}} - - -export default async function SeedSpecialVisibility(prisma: PrismaClient) { - const keys = Object.keys(SpecialVisibilityConfig) as SpecialVisibilityPurpose[] - await Promise.all(keys.map((special) => - prisma.visibility.upsert({ - where: { - specialPurpose: special - }, - update: { - regularLevel: { - update: { - permission: SpecialVisibilityConfig[special].regularLevel - } - }, - adminLevel: { - update: { - permission: SpecialVisibilityConfig[special].adminLevel - } - } - }, - create: { - purpose: 'SPECIAL', - published: true, - specialPurpose: special, - regularLevel: { - create: { - permission: SpecialVisibilityConfig[special].regularLevel - } - }, - adminLevel: { - create: { - permission: SpecialVisibilityConfig[special].adminLevel - } - } - } - }) - )) -} diff --git a/src/prisma/seeder/src/seeder.ts b/src/prisma/seeder/src/seeder.ts index cc9eeb0ae..8d2169104 100644 --- a/src/prisma/seeder/src/seeder.ts +++ b/src/prisma/seeder/src/seeder.ts @@ -12,7 +12,6 @@ import dobbelOmega from './dobbelOmega/dobbelOmega' import seedNotificationChannels from './seedNotificationsChannels' import seedDevGroups from './development/seedDevGroups' import seedClasses from './seedClasses' -import SeedSpecialVisibility from './seedSpecialVisibility' import seedMail from './seedMail' import seedStudyProgramme from './seedStudyProgramme' import seedOmegaMembershipGroups from './seedOmegaMembershipGroups' @@ -37,7 +36,6 @@ export default async function seed( if (enableLogging) console.log('seeding standard data....') await seedOrder(prisma) - await SeedSpecialVisibility(prisma) await SeedSpecialImageCollections(prisma) await seedImages(prisma) await seedCms(prisma) diff --git a/src/services/cms/articleSections/implement.ts b/src/services/cms/articleSections/implement.ts index 95fff1c13..5717c6406 100644 --- a/src/services/cms/articleSections/implement.ts +++ b/src/services/cms/articleSections/implement.ts @@ -10,7 +10,7 @@ import type { z } from 'zod' import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation' type ParamsSchema = typeof articleSectionSchemas.params -type OwnedArticleSections = Prisma.ArticleSectionGetPayload<{ +type OwnedArticleSection = Prisma.ArticleSectionGetPayload<{ include: { cmsImage: true, cmsParagraph: true, @@ -41,7 +41,7 @@ export function implementUpdateArticleSectionOperations< prisma: PrismaPossibleTransaction, implementationParams: z.infer } - ) => Promise + ) => Promise destroyOnEmpty: boolean }) { const ownershipCheckArticleSection = async ( diff --git a/src/services/cms/articles/implement.ts b/src/services/cms/articles/implement.ts index dbdaf6160..41875a227 100644 --- a/src/services/cms/articles/implement.ts +++ b/src/services/cms/articles/implement.ts @@ -8,7 +8,7 @@ import type { articleSchemas } from './schemas' import type { z } from 'zod' type ParamsSchema = typeof articleSchemas.params -type OwnedArticles = Prisma.ArticleGetPayload<{ +type OwnedArticle = Prisma.ArticleGetPayload<{ include: { coverImage: true, articleSections: { @@ -43,7 +43,7 @@ export function implementUpdateArticleOperations< prisma: PrismaPossibleTransaction, implementationParams: z.infer } - ) => Promise + ) => Promise }) { const ownershipCheckArticle = async ( args: Omit, 'data'> diff --git a/src/services/images/collections/ConfigVars.ts b/src/services/images/collections/ConfigVars.ts deleted file mode 100644 index c76df8682..000000000 --- a/src/services/images/collections/ConfigVars.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { SpecialCollection, SpecialVisibilityPurpose } from '@prisma/client' - -/** - * Each special image collection links to a special visibility known at compile time - */ -export const specialCollectionsSpecialVisibilityMap = { - OMBULCOVERS: { - specialVisibility: 'OMBUL' - }, - PROFILEIMAGES: { - specialVisibility: 'USER' - }, - STANDARDIMAGES: { - specialVisibility: 'PUBLIC' - }, - COMMITTEELOGOS: { - specialVisibility: 'COMMITTEE' - } -} satisfies {[T in SpecialCollection]: { - specialVisibility: SpecialVisibilityPurpose -}} diff --git a/src/services/images/collections/actions.ts b/src/services/images/collections/actions.ts index bb12b2255..57b47fde0 100644 --- a/src/services/images/collections/actions.ts +++ b/src/services/images/collections/actions.ts @@ -1,9 +1,7 @@ 'use server' import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { checkVisibility } from '@/auth/checkVisibility' import { getUser } from '@/auth/session/getUser' -import { getVisibilityFilter } from '@/auth/getVisibilityFilter' import { createImageCollection } from '@/services/images/collections/create' import { destroyImageCollection } from '@/services/images/collections/destroy' import { @@ -12,10 +10,8 @@ import { readSpecialImageCollection } from '@/services/images/collections/read' import { updateImageCollection } from '@/services/images/collections/update' import { createImageCollectionValidation, updateImageCollectionValidation } from '@/services/images/collections/validation' -import { includeVisibility } from '@/services/visibility/read' import { SpecialCollection } from '@prisma/client' import type { CreateImageCollectionTypes, UpdateImageCollectionTypes } from '@/services/images/collections/validation' -import type { VisibilityCollapsed } from '@/services/visibility/types' import type { ExpandedImageCollection, ImageCollectionCursor, ImageCollectionPageReturn } from '@/services/images/collections/types' @@ -50,16 +46,9 @@ export async function destroyImageCollectionAction(collectionId: number): Promis */ export async function readImageCollectionAction( idOrName: number | string -): Promise> { - const collection = await safeServerCall(() => includeVisibility( - () => readImageCollection(idOrName), - data => data.visibilityId - )) +): Promise> { + const collection = await safeServerCall(() => readImageCollection(idOrName)) if (!collection.success) return collection - if (!checkVisibility(await getUser(), collection.data.visibility, 'REGULAR')) { - return createActionError('UNAUTHORIZED', 'You do not have permission to view this collection') - } - return collection } @@ -71,10 +60,7 @@ export async function readImageCollectionAction( export async function readImageCollectionsPageAction( readPageInput: ReadPageInput ): Promise> { - const { memberships, permissions } = await getUser() - const visibilityFilter = getVisibilityFilter(memberships, permissions) - console.log(visibilityFilter) - return await safeServerCall(() => readImageCollectionsPage(readPageInput, visibilityFilter)) + return await safeServerCall(() => readImageCollectionsPage(readPageInput)) } /** diff --git a/src/services/images/collections/create.ts b/src/services/images/collections/create.ts index 66eaa424a..83e91d717 100644 --- a/src/services/images/collections/create.ts +++ b/src/services/images/collections/create.ts @@ -2,7 +2,7 @@ import '@pn-server-only' import { createImageCollectionValidation } from './validation' import { prisma } from '@/prisma/client' import { prismaCall } from '@/services/prismaCall' -import { createVisibility } from '@/services/visibility/create' +import { visibilityOperations } from '@/services/visibility/operations' import type { CreateImageCollectionTypes } from './validation' import type { ImageCollection } from '@prisma/client' @@ -10,13 +10,20 @@ export async function createImageCollection( rawdata: CreateImageCollectionTypes['Detailed'] ): Promise { const data = createImageCollectionValidation.detailedValidate(rawdata) - const visivility = await createVisibility('IMAGE') + const visibilityRead = await visibilityOperations.create({ bypassAuth: true }) + const visibilityAdmin = await visibilityOperations.create({ bypassAuth: true }) + return await prismaCall(() => prisma.imageCollection.create({ data: { ...data, - visibility: { + visibilityRead: { + connect: { + id: visibilityRead.id + } + }, + visibilityAdmin: { connect: { - id: visivility.id + id: visibilityAdmin.id } } } diff --git a/src/services/images/collections/destroy.ts b/src/services/images/collections/destroy.ts index 07e1b7a53..1c823a04e 100644 --- a/src/services/images/collections/destroy.ts +++ b/src/services/images/collections/destroy.ts @@ -2,7 +2,7 @@ import '@pn-server-only' import { prisma } from '@/prisma/client' import { ServerError } from '@/services/error' import { prismaCall } from '@/services/prismaCall' -import { destroyVisibility } from '@/services/visibility/destroy' +import { visibilityOperations } from '@/services/visibility/operations' import type { ImageCollection } from '@prisma/client' export async function destroyImageCollection(collectionId: number): Promise { @@ -21,7 +21,7 @@ export async function destroyImageCollection(collectionId: number): Promise( { page }: ReadPageInput, - visibilityFilter: VisibilityFilter ): Promise { const collections = await prismaCall(() => prisma.imageCollection.findMany({ - where: visibilityFilter, include: { coverImage: true, images: { @@ -100,16 +96,26 @@ export async function readSpecialImageCollection(special: SpecialCollection): Pr })) if (!collection) { logger.warn(`Special collection ${special} did not exist, creating it`) - const permissionLevels = specialCollectionsSpecialVisibilityMap[special] - const visability = await readSpecialVisibility(permissionLevels.specialVisibility) + + //Note: these visibilities do not actually do anything for special collections, + //but the schema requires them to exist - believe this implementation to be better than for + //regular collections to maybe be in invalid state with no visibility. + // TODO: use create method. + const visibilityAdmin = await visibilityOperations.create({ bypassAuth: true }) + const visibilityRead = await visibilityOperations.create({ bypassAuth: true }) const newCollection = await prismaCall(() => prisma.imageCollection.create({ data: { name: special, special, - visibility: { + visibilityRead: { + connect: { + id: visibilityRead.id + } + }, + visibilityAdmin: { connect: { - id: visability.id + id: visibilityAdmin.id } } } diff --git a/src/services/visibility/ConfigVars.ts b/src/services/visibility/ConfigVars.ts deleted file mode 100644 index cf5b67e51..000000000 --- a/src/services/visibility/ConfigVars.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Permission, VisibilityPurpose, SpecialVisibilityPurpose } from '@prisma/client' - -/** - * These are the permissions that buypass the visibiity system. i.e. for reading images - * if the user has the permission it does not matter if the visibility is fullfilled. - */ -export const BypassPermissions = { - IMAGE: 'IMAGE_ADMIN', - NEWS_ARTICLE: 'FRONTPAGE_ADMIN', // just changed this to make ts happy - bypass and purposes will be removed - ARTICLE_CATEGORY: 'FRONTPAGE_ADMIN', // just changed this to make ts happy - bypass and purposes will be removed - EVENT: 'EVENT_ADMIN', - SPECIAL: null // null means there is no bypass permission -} as const satisfies { [key in VisibilityPurpose]: Permission | null } - -export type BypassPermissions = typeof BypassPermissions[keyof typeof BypassPermissions] - -export const purposeTextsConfig = { - IMAGE: 'Bilder', - NEWS_ARTICLE: 'Nyheter', - ARTICLE_CATEGORY: 'Artikkelkategorier', - EVENT: 'Arrangementer (Hvad der hender)', - SPECIAL: 'Spessielle ting' -} as const satisfies { [key in VisibilityPurpose]: string } - -/** - * Which permissions link to special visibility purposes - * If the special visibility were to disappear, it will be regenerated from this. - */ -export const specialVisibilityConfig = { - OMBUL: { - regularLevel: 'OMBUL_READ', - adminLevel: 'OMBUL_CREATE' - }, - PUBLIC: { - regularLevel: null, - adminLevel: 'FRONTPAGE_ADMIN' // just changed this to make ts happy - }, - COMMITTEE: { - regularLevel: 'COMMITTEE_READ', - adminLevel: 'COMMITTEE_CREATE' - }, - USER: { - regularLevel: 'USERS_READ', - adminLevel: 'USERS_CREATE' - } -} satisfies {[T in SpecialVisibilityPurpose]: { - regularLevel: Permission | null, - adminLevel: Permission | null -}} diff --git a/src/services/visibility/actions.ts b/src/services/visibility/actions.ts deleted file mode 100644 index 4585c2c29..000000000 --- a/src/services/visibility/actions.ts +++ /dev/null @@ -1,141 +0,0 @@ -'use server' - -import { createActionError, safeServerCall } from '@/services/actionError' -import { checkVisibility } from '@/auth/checkVisibility' -import { getUser } from '@/auth/session/getUser' -import { groupTypesConfig } from '@/services/groups/constants' -import { groupOperations } from '@/services/groups/operations' -import { purposeTextsConfig } from '@/services/visibility/ConfigVars' -import { readVisibilityCollapsed } from '@/services/visibility/read' -import type { ExpandedGroup, GroupsStructured } from '@/services/groups/types' -import type { ActionReturn } from '@/services/actionTypes' -import type { - GroupMatrix, - VisibilityLevelType, - VisibilityRequiermentForAdmin, - VisibilityStructuredForAdmin -} from '@/services/visibility/types' -import type { GroupType } from '@prisma/client' - -export async function readVisibilityForAdminAction(id: number): Promise> { - const [visibilityRes, groupsRes] = await Promise.all([ - safeServerCall(() => readVisibilityCollapsed(id)), - // TODO: Fix Authing here. The bypass should be false - safeServerCall(() => groupOperations.readGroupsStructured({ bypassAuth: true })) - ]) - if (!visibilityRes.success || !groupsRes.success) return createActionError('UNKNOWN ERROR', 'noe gikk galt') - - const visibility = visibilityRes.data - const groups = groupsRes.data - const purpose = purposeTextsConfig[visibility.purpose] - - if (!checkVisibility(await getUser(), visibility, 'ADMIN')) { - return createActionError('UNAUTHORIZED', 'You do not have permission to admin this collection') - } - if (visibility.type === 'SPECIAL') { - return { - success: true, - data: { - published: visibility.published, - purpose, - type: 'SPECIAL', - message: 'Denne syneligheten er spessiell', - regular: `Brukere med ${visibility.regular} har vanlig tilgang`, - admin: `Brukere med ${visibility.admin} har admin tilgang`, - } - } - } - const standardGroupingsRefular = ['CLASS', 'OMEGA_MEMBERSHIP_GROUP', 'STUDY_PROGRAMME'] satisfies GroupType[] - const standardGroupingsAdmin = ['COMMITTEE', 'OMEGA_MEMBERSHIP_GROUP'] satisfies GroupType[] - return { - success: true, - data: { - published: visibility.published, - purpose, - type: 'REGULAR', - regular: expandOneLevel(visibility.regular, groups, standardGroupingsRefular), - admin: expandOneLevel(visibility.admin, groups, standardGroupingsAdmin), - groups, - } - } -} - -function expandOneLevel( - matrix: GroupMatrix, - groups: GroupsStructured, - standardGroupings: GroupType[] -): VisibilityRequiermentForAdmin[] { - const res: VisibilityRequiermentForAdmin[] = [] - standardGroupings.forEach(groupType => { - if (!groups[groupType]) return - const standardRequriment: VisibilityRequiermentForAdmin = { - name: groupTypesConfig[groupType].name, - groups: groups[groupType].groups.map(group => ({ - ...group, - selected: false - })) - } - //Find out if there is a row in the matrix with just the group type. - for (let i = 0; i < matrix.length; i++) { - if (matrix[i].every(groupId => groupTypeOfId(groupId, groups) === groupType)) { - standardRequriment.groups.forEach(group => { - group.selected = matrix[i].includes(group.id) - }) - //Remove the row from the matrix since it has been handled - matrix.splice(i, 1) - } - } - res.push(standardRequriment) - }) - - //Handle all non standard groupings - matrix.forEach(row => { - const groups_ = row.reduce((acc, id) => { - const group = findGroupOfId(id, groups) - if (group) acc.push(group) - return acc - }, [] as ExpandedGroup[]) - const nonStandardRequriment: VisibilityRequiermentForAdmin = { - name: 'ekstra', - groups: groups_.map(group => ({ - ...group, - selected: true - })) - } - res.push(nonStandardRequriment) - }) - - return res -} - -function findGroupOfId(id: number, groups: GroupsStructured): ExpandedGroup | null { - let found: ExpandedGroup | null = null - Object.values(groups).forEach(groupType => { - groupType.groups.forEach(group => { - if (group.id === id) { - found = group - } - }) - }) - return found -} - -function groupTypeOfId(id: number, groups: GroupsStructured): GroupType { - let type: GroupType | null = null - Object.values(groups).forEach((groupType) => { - groupType.groups.forEach(group => { - if (group.id === id) { - type = group.groupType - } - }) - }) - return type || 'MANUAL_GROUP' -} - -export async function updateVisibilityAction( - level: VisibilityLevelType, - formdata: FormData -): Promise> { - console.log(formdata) - return { success: true, data: undefined } -} diff --git a/src/services/visibility/create.ts b/src/services/visibility/create.ts deleted file mode 100644 index de946d19c..000000000 --- a/src/services/visibility/create.ts +++ /dev/null @@ -1,33 +0,0 @@ -import '@pn-server-only' -import { updateVisibility } from './update' -import { prismaCall } from '@/services/prismaCall' -import { prisma } from '@/prisma/client' -import type { Visibility, VisibilityPurpose } from '@prisma/client' -import type { VisibilityLevelMatrices } from './types' - -/** - * A function to create visibility - * @param data - the visibility to create with.. If not given it will create a empty visibility (accessable to all) - * @returns - */ -export async function createVisibility( - purpose: VisibilityPurpose, - data?: VisibilityLevelMatrices -): Promise { - const visibility = await prismaCall(() => prisma.visibility.create({ - data: { - purpose, - regularLevel: { - create: {} - }, - adminLevel: { - create: {} - } - } - })) - await updateVisibility(visibility.id, data || { - admin: [], - regular: [] - }) - return visibility -} diff --git a/src/services/visibility/destroy.ts b/src/services/visibility/destroy.ts deleted file mode 100644 index bdfa7d279..000000000 --- a/src/services/visibility/destroy.ts +++ /dev/null @@ -1,32 +0,0 @@ -import '@pn-server-only' -import { ServerError } from '@/services/error' -import { prismaCall } from '@/services/prismaCall' -import { prisma } from '@/prisma/client' - -/** - * Destroy a visibility. This will also destroy the visibility levels associated with the visibility, - * to prevent orphaned visibility levels. - * @param id - The id of the visibility to destroy - */ -export async function destroyVisibility(id: number): Promise { - const visibility = await prismaCall(() => prisma.visibility.delete({ - where: { - id - } - })) - if (visibility.specialPurpose) { - throw new ServerError('BAD PARAMETERS', 'Kan ikke slette en spesiell visibility') - } - await Promise.all([ - prismaCall(() => prisma.visibilityLevel.deleteMany({ - where: { - id: visibility.adminLevelId - } - })), - prismaCall(() => prisma.visibilityLevel.deleteMany({ - where: { - id: visibility.regularLevelId - } - })) - ]) -} diff --git a/src/services/visibility/implement.ts b/src/services/visibility/implement.ts new file mode 100644 index 000000000..5bd231d03 --- /dev/null +++ b/src/services/visibility/implement.ts @@ -0,0 +1,69 @@ +import { visibilityOperations } from './operations' +import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation' +import type { AutherResult } from '@/auth/auther/Auther' +import type { Prisma } from '@prisma/client' +import type { visibilitySchemas } from './schemas' +import type { z } from 'zod' + +type ParamsSchema = typeof visibilitySchemas.params +type OwnedVisibility = Prisma.ArticleGetPayload<{ + include: { + coverImage: true, + articleSections: { + include: { + cmsImage: true, + cmsParagraph: true, + cmsLink: true, + } + } + } +}> +/** + * This utility implements the read and update operations for visibility + */ +export function implementVisibilityOperations< + const ImplementationParamsSchema extends z.ZodTypeAny +>({ + implementationParamsSchema, + authorizers, + ownedVisibility, +}: { + implementationParamsSchema: ImplementationParamsSchema, + authorizers: { + update: ( + args: { + prisma: PrismaPossibleTransaction, + implementationParams: z.infer + } + ) => AutherResult | Promise + read: ( + args: { + prisma: PrismaPossibleTransaction, + implementationParams: z.infer + } + ) => AutherResult | Promise + }, + ownedVisibility: ( + args: { + prisma: PrismaPossibleTransaction, + implementationParams: z.infer + } + ) => Promise +}) { + const ownershipCheckVisibility = async ( + args: Omit, 'data'> + ) => (await ownedVisibility(args)).id === args.params.visibilityId + + return { + read: visibilityOperations.read.implement({ + implementationParamsSchema, + authorizer: authorizers.read, + ownershipCheck: ownershipCheckVisibility + }), + update: visibilityOperations.update.implement({ + implementationParamsSchema, + authorizer: authorizers.update, + ownershipCheck: ownershipCheckVisibility + }) + } as const +} diff --git a/src/services/visibility/operations.ts b/src/services/visibility/operations.ts new file mode 100644 index 000000000..d34454325 --- /dev/null +++ b/src/services/visibility/operations.ts @@ -0,0 +1,81 @@ +import '@pn-server-only' +import { visibilitySchemas } from './schemas' +import { defineOperation, defineSubOperation } from '@/services/serviceOperation' +import { readCurrentOmegaOrder } from '@/services/omegaOrder/read' +import { ServerOnly } from '@/auth/auther/ServerOnly' +import { ServerError } from '@/services/error' +import type { VisibilityMatrix } from './types' + +export const visibilityOperations = { + create: defineOperation({ + authorizer: ServerOnly, + operation: ({ prisma }) => + prisma.visibility.create({ data: {} }) + }), + + destroy: defineOperation({ + authorizer: ServerOnly, + paramsSchema: visibilitySchemas.params, + operation: ({ prisma, params }) => + prisma.visibility.delete({ where: { id: params.visibilityId } }) + }), + + update: defineSubOperation({ + paramsSchema: () => visibilitySchemas.params, + dataSchema: () => visibilitySchemas.update, + operation: () => async ({ prisma, params, data }) => { + //Remove all current visibilityRequirements (and by cascade all visibilityRequirementGroups) + await prisma.visibilityRequirement.deleteMany({ + where: { visibilityId: params.visibilityId } + }) + + const currentOrder = await readCurrentOmegaOrder() + + return prisma.visibility.update({ + where: { id: params.visibilityId }, + data: { + requirements: { + create: data.requirements.map(requirement => ({ + conditions: { + create: requirement.conditions.map(condition => ({ + groupId: condition.groupId, + type: condition.type, + order: condition.type === 'ORDER' ? condition.order : currentOrder.order + })) + } + })) + } + } + }) + } + }), + + read: defineSubOperation({ + paramsSchema: () => visibilitySchemas.params, + operation: () => async ({ prisma, params }): Promise => { + const visibility = await prisma.visibility.findUnique({ + where: { id: params.visibilityId }, + include: { + requirements: { + include: { + conditions: true + } + } + } + }) + if (!visibility) throw new ServerError('NOT FOUND', 'Fant ikke synlighet') + return { + requirements: visibility.requirements.map(requirement => ({ + conditions: requirement.conditions.map(condition => (condition.type === 'ORDER' ? { + groupId: condition.groupId, + type: condition.type, + order: condition.order + } : { + groupId: condition.groupId, + type: condition.type, + })) + })) + } + } + }) +} as const diff --git a/src/services/visibility/read.ts b/src/services/visibility/read.ts deleted file mode 100644 index 824c1e163..000000000 --- a/src/services/visibility/read.ts +++ /dev/null @@ -1,127 +0,0 @@ -import '@pn-server-only' -import { specialVisibilityConfig } from './ConfigVars' -import { prismaCall } from '@/services/prismaCall' -import { ServerError } from '@/services/error' -import { prisma } from '@/prisma/client' -import type { SpecialVisibilityPurpose, VisibilityRequirmenetGroup } from '@prisma/client' -import type { VisibilityCollapsed } from './types' - -const levelSelector = { - select: { - permission: true, - requirements: { - select: { - visibilityRequirmenetGroups: true - } - } - } -} - -/** - * Reads visibility with levels, and with relation to groups, and returns the data - * in a matrix format if type is REGULAR, or as permissions if type is SPECIAL i.e has - * a special purpose. - * @param id - the id of the visibility to read - * @returns - */ -export async function readVisibilityCollapsed(id: number): Promise { - const visibility = await prismaCall(() => prisma.visibility.findUnique({ - where: { id }, - include: { - regularLevel: levelSelector, - adminLevel: levelSelector, - } - })) - if (!visibility) throw new ServerError('NOT FOUND', `Visibility ikke funnet for id ${id}`) - if (visibility.specialPurpose) { - return { - type: 'SPECIAL', - published: visibility.published, - id: visibility.id, - regular: visibility.regularLevel.permission, - admin: visibility.adminLevel.permission, - purpose: visibility.purpose - } - } - return { - type: 'REGULAR', - published: visibility.published, - purpose: visibility.purpose, - id: visibility.id, - regular: collapseVisibilityLevel(visibility.regularLevel), - admin: collapseVisibilityLevel(visibility.adminLevel) - } -} - -/** - * This function reads a special visibility, and creates it if it does not exist - * (this should not happen in production) - * @param specialPurpose - The purpose of the special visibility - * @returns - The visibility - */ -export async function readSpecialVisibility(specialPurpose: SpecialVisibilityPurpose): Promise { - const visibility = await prismaCall(() => prisma.visibility.findFirst({ - where: { specialPurpose }, - include: { - regularLevel: levelSelector, - adminLevel: levelSelector, - } - })) - if (!visibility) { - const created = await prismaCall(() => prisma.visibility.create({ - data: { - purpose: 'SPECIAL', - specialPurpose, - published: true, - regularLevel: { - create: { - permission: specialVisibilityConfig[specialPurpose].regularLevel - } - }, - adminLevel: { - create: { - permission: specialVisibilityConfig[specialPurpose].adminLevel - } - }, - }, - include: { - regularLevel: levelSelector, - adminLevel: levelSelector, - } - })) - return { - type: 'SPECIAL', - published: created.published, - id: created.id, - regular: created.regularLevel.permission, - admin: created.adminLevel.permission, - purpose: created.purpose - } - } - return { - type: 'SPECIAL', - published: visibility.published, - id: visibility.id, - regular: visibility.regularLevel.permission, - admin: visibility.adminLevel.permission, - purpose: visibility.purpose - } -} - -/** - * A higher order function that includes visibility in the result of a server call - * @param serverCall - A function to get some data, with a relation to visibility - * @param getVisibilityId - A methode to retrive the visibility id from the result of the server call - * @returns - The data from the server call, with the visibility included - */ -export async function includeVisibility(serverCall: () => Promise, getVisibilityId: (result: T) => number) { - const result = await serverCall() - const visibility = await readVisibilityCollapsed(getVisibilityId(result)) - return { ...result, visibility } -} - -function collapseVisibilityLevel(level: {requirements: {visibilityRequirmenetGroups: VisibilityRequirmenetGroup[]}[]}) { - return level.requirements.map(requirement => - requirement.visibilityRequirmenetGroups.map(groupItem => groupItem.groupId) - ) -} diff --git a/src/services/visibility/schemas.ts b/src/services/visibility/schemas.ts new file mode 100644 index 000000000..27f7af8cc --- /dev/null +++ b/src/services/visibility/schemas.ts @@ -0,0 +1,32 @@ +import { VisibilityRequirementGroupType } from '@prisma/client' +import { z } from 'zod' + + +const baseSchema = z.object({ + requirements: z.array( + z.object({ + conditions: z.array( + z.union([ + z.object({ + type: z.literal(VisibilityRequirementGroupType.ORDER), + order: z.number().min(0), + groupId: z.number() + }), + z.object({ + type: z.literal(VisibilityRequirementGroupType.ACTIVE), + groupId: z.number() + }) + ]) + ) + }) + ) +}) + +export const visibilitySchemas = { + update: baseSchema.pick({ + requirements: true + }), + params: z.object({ + visibilityId: z.number() + }) +} as const diff --git a/src/services/visibility/types.ts b/src/services/visibility/types.ts index 39e4bb48e..0f268e089 100644 --- a/src/services/visibility/types.ts +++ b/src/services/visibility/types.ts @@ -1,55 +1,18 @@ -import type { ExpandedGroup, GroupsStructured } from '@/services/groups/types' -import type { Matrix } from '@/lib/checkMatrix' -import type { Permission, VisibilityPurpose } from '@prisma/client' +import type { VisibilityRequirementGroupType } from '@prisma/client' -export type GroupMatrix = Matrix - -export type VisibilityLevelType = 'ADMIN' | 'REGULAR' - -export type VisibilityLevelMatrices = { - regular: GroupMatrix, - admin: GroupMatrix -} - -/** - * A type that represents a visibility in a simple way. - * Either type is SPECIAL and and levels are based on Permissions, - * or type is REGULAR and the levels are represented by matrix of ids of groups - */ -export type VisibilityCollapsed = { - id: number, - purpose: VisibilityPurpose - published: boolean, -} & ({ - type: 'REGULAR', - regular: GroupMatrix, - admin: GroupMatrix +export type VisibilityCondition = { + type: Extract + groupId: number + order: number } | { - type: 'SPECIAL' - regular: Permission | null, - admin: Permission | null -}) + type: Extract + groupId: number +} -export type VisibilityRequiermentForAdmin = { - name: string - groups: (ExpandedGroup & { - selected: boolean - })[] +export type VisibilityRequirement = { + conditions: VisibilityCondition[] } -export type VisibilityStructuredForAdmin = { - published: boolean - purpose: string -} & ( - { - type: 'REGULAR' - groups: GroupsStructured - regular: VisibilityRequiermentForAdmin[] - admin: VisibilityRequiermentForAdmin[] - } | { - type: 'SPECIAL' - message: string - regular: string - admin: string - } -) +export type VisibilityMatrix = { + requirements: VisibilityRequirement[] +} diff --git a/src/services/visibility/update.ts b/src/services/visibility/update.ts deleted file mode 100644 index 9b8697dd2..000000000 --- a/src/services/visibility/update.ts +++ /dev/null @@ -1,62 +0,0 @@ -import '@pn-server-only' -import { prismaCall } from '@/services/prismaCall' -import { prisma } from '@/prisma/client' -import type { VisibilityLevelMatrices } from './types' - -export async function updateVisibility(id: number, data: VisibilityLevelMatrices): Promise { - //TODO chack that the admin is a subset of the regular - - await prismaCall(() => prisma.$transaction([ - //first remove all the old conditions. This will also remove all join tables to groups on cascade. - prisma.visibilityRequirement.deleteMany({ - where: { - visibilityId: id - } - }), - //Then create many new requirements - prisma.visibility.update({ - where: { - id - }, - data: { - regularLevel: { - create: { - requirements: { - create: data.regular.map(row => ({ - visibilityRequirementGroups: { - create: row.map(groupId => ({ - groupId - })) - } - })) - } - } - }, - adminLevel: { - create: { - requirements: { - create: data.admin.map(row => ({ - visibilityRequirementGroups: { - create: row.map(groupId => ({ - groupId - })) - } - })) - } - } - } - } - }) - ])) -} - -export async function updateVisibilityPublished(id: number, published: boolean): Promise { - await prismaCall(() => prisma.visibility.update({ - where: { - id - }, - data: { - published - } - })) -}