From 9070c21fb886be40c7d32d85af36970bb472531f Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:02:43 +0200 Subject: [PATCH 1/3] feat: implement visibility in news service --- ...reLevelFromDoubleLevelVisibilityDynamic.ts | 23 +++ src/prisma/schema/news.prisma | 6 + src/prisma/schema/permission.prisma | 4 + src/prisma/schema/visibility.prisma | 3 + .../seeder/src/development/seedDevNews.ts | 6 + .../seeder/src/dobbelOmega/migateArticles.ts | 6 + src/services/news/actions.ts | 37 +---- src/services/news/auth.ts | 33 ++-- src/services/news/operations.ts | 151 +++++++++++++++--- src/services/news/schemas.ts | 3 + src/services/permissions/constants.ts | 12 +- 11 files changed, 221 insertions(+), 63 deletions(-) create mode 100644 src/auth/authorizer/RequireLevelFromDoubleLevelVisibilityDynamic.ts diff --git a/src/auth/authorizer/RequireLevelFromDoubleLevelVisibilityDynamic.ts b/src/auth/authorizer/RequireLevelFromDoubleLevelVisibilityDynamic.ts new file mode 100644 index 000000000..bb4971d7d --- /dev/null +++ b/src/auth/authorizer/RequireLevelFromDoubleLevelVisibilityDynamic.ts @@ -0,0 +1,23 @@ +import { AuthorizerFactory } from './Authorizer' +import { checkVisibility } from '@/auth/visibility/checkVisibility' +import type { DoubleLevelVisibilityMatrix } from '@/services/visibility/types' +import type { Permission } from '@/prisma-generated-pn-types' + +/** + * Like RequireLevelFromDoubleLevelVisibility, but the level to check is picked at call time + * (dynamicFields) rather than fixed at definition time (staticFields) - for resources where which + * level applies depends on the resource's own data, e.g. a news article that requires only the + * regular level once published, but the admin level while still a draft. + */ +export const RequireLevelFromDoubleLevelVisibilityDynamic = AuthorizerFactory< + { bypassPermission: Permission | null }, + { level: 'REGULAR' | 'ADMIN', doubleLevelMatrix: DoubleLevelVisibilityMatrix }, + 'USER_NOT_REQUIERED_FOR_AUTHORIZED' +> (({ session, dynamicFields, staticFields }) => ({ + success: checkVisibility( + session.memberships, + dynamicFields.level === 'REGULAR' ? + dynamicFields.doubleLevelMatrix.regularLevel : dynamicFields.doubleLevelMatrix.adminLevel + ) || (staticFields.bypassPermission ? session.permissions.includes(staticFields.bypassPermission) : false), + session, +})) diff --git a/src/prisma/schema/news.prisma b/src/prisma/schema/news.prisma index fca5bc885..710f3f102 100644 --- a/src/prisma/schema/news.prisma +++ b/src/prisma/schema/news.prisma @@ -2,9 +2,15 @@ model NewsArticle { id Int @id @default(autoincrement()) description String? endDateTime DateTime //when the article is no longer considered current + published Boolean @default(false) article Article @relation(fields: [articleId, articleName], references: [id, name], onDelete: Restrict) articleId Int @unique articleName String + visibilityAdmin Visibility @relation(name: "NewsArticleAdminVisibility", fields: [visibilityAdminId], references: [id], onDelete: Restrict) + visibilityAdminId Int @unique + visibilityRegular Visibility @relation(name: "NewsArticleReadVisibility", fields: [visibilityRegularId], references: [id], onDelete: Restrict) + visibilityRegularId Int @unique + @@unique([articleId, articleName]) } diff --git a/src/prisma/schema/permission.prisma b/src/prisma/schema/permission.prisma index 548ee0efb..d069bfd48 100644 --- a/src/prisma/schema/permission.prisma +++ b/src/prisma/schema/permission.prisma @@ -166,6 +166,10 @@ enum Permission { //Flair/badge/cape FLAIR_ADMIN + + // News + NEWS_CREATE + NEWS_ADMIN } model GroupPermission { diff --git a/src/prisma/schema/visibility.prisma b/src/prisma/schema/visibility.prisma index af41a3539..597a8bf82 100644 --- a/src/prisma/schema/visibility.prisma +++ b/src/prisma/schema/visibility.prisma @@ -6,6 +6,9 @@ model Visibility { imageCollectionAdmin ImageCollection? @relation(name: "ImageCollectionAdminVisibility") imageCollectionRead ImageCollection? @relation(name: "ImageCollectionReadVisibility") + + newsArticleAdmin NewsArticle? @relation(name: "NewsArticleAdminVisibility") + newsArticleRead NewsArticle? @relation(name: "NewsArticleReadVisibility") } model VisibilityRequirement { diff --git a/src/prisma/seeder/src/development/seedDevNews.ts b/src/prisma/seeder/src/development/seedDevNews.ts index 236be7fed..a4ff901a5 100644 --- a/src/prisma/seeder/src/development/seedDevNews.ts +++ b/src/prisma/seeder/src/development/seedDevNews.ts @@ -65,6 +65,12 @@ async function upsertTestNews( } } } + }, + visibilityRegular: { + create: {} + }, + visibilityAdmin: { + create: {} } } }) diff --git a/src/prisma/seeder/src/dobbelOmega/migateArticles.ts b/src/prisma/seeder/src/dobbelOmega/migateArticles.ts index a6d750255..bf42c6169 100644 --- a/src/prisma/seeder/src/dobbelOmega/migateArticles.ts +++ b/src/prisma/seeder/src/dobbelOmega/migateArticles.ts @@ -115,6 +115,12 @@ export default async function migrateArticles( } }, endDateTime: articlePn.endDateTime, + visibilityRegular: { + create: {} + }, + visibilityAdmin: { + create: {} + } } }) } diff --git a/src/services/news/actions.ts b/src/services/news/actions.ts index 909bd65e3..feeb3286a 100644 --- a/src/services/news/actions.ts +++ b/src/services/news/actions.ts @@ -1,10 +1,6 @@ 'use server' import { newsOperations } from './operations' -import { createActionError } from '@/services/actionError' -import { notificationOperations } from '@/services/notifications/operations' import { makeAction } from '@/services/serverAction' -import type { SimpleNewsArticle } from '@/services/news/types' -import type { ActionReturn } from '@/services/actionTypes' export const createNewsAction = makeAction(newsOperations.create) export const destroyNewsAction = makeAction(newsOperations.destroy) @@ -12,6 +8,11 @@ export const readOldNewsPageAction = makeAction(newsOperations.readOldPage) export const readNewsCurrentAction = makeAction(newsOperations.readCurrent) export const readNewsAction = makeAction(newsOperations.read) export const updateNewsAction = makeAction(newsOperations.update) +export const publishNewsAction = makeAction(newsOperations.publish) + +export const readNewsDoubleLevelVisibilityAction = makeAction(newsOperations.visibility.readDoubleLevelMatrix) +export const updateNewsRegularLevelVisibilityAction = makeAction(newsOperations.visibility.updateRegularLevel) +export const updateNewsAdminLevelVisibilityAction = makeAction(newsOperations.visibility.updateAdminLevel) export const updateNewsArticleAction = makeAction( newsOperations.updateArticle.update @@ -43,31 +44,3 @@ export const updateNewsArticleCmsParagraphAction = makeAction( export const updateNewsArticleCmsLinkAction = makeAction( newsOperations.updateArticle.articleSections.cmsLink ) - -export async function publishNewsAction( - // disable eslint rule temporarily until todo is resolved - // eslint-disable-next-line @typescript-eslint/no-unused-vars - id: number, - // disable eslint rule temporarily until todo is resolved - // eslint-disable-next-line @typescript-eslint/no-unused-vars - shouldPublish: boolean -): Promise>> { - notificationOperations.createSpecial.internalCall({ - params: { - special: 'NEW_NEWS_ARTICLE', - }, - data: { - title: 'Ny nyhetsartikkel', // TODO: Add info about the article - message: 'En ny nyhetsartikkel er publisert', - }, - }) - - return createActionError('UNKNOWN ERROR', 'Not implemented') -} - -// disable eslint rule temporarily until todo is resolved -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function updateVisibilityAction(id: number, visible: unknown): Promise> { - //TODO: add visible field to news - return createActionError('UNKNOWN ERROR', 'Not implemented') -} diff --git a/src/services/news/auth.ts b/src/services/news/auth.ts index 3b532ba82..62258dac8 100644 --- a/src/services/news/auth.ts +++ b/src/services/news/auth.ts @@ -1,15 +1,24 @@ -import { RequireNothing } from '@/auth/authorizer/RequireNothing' - +import { RequireLevelFromDoubleLevelVisibility } from '@/auth/authorizer/RequireLevelFromDoubleLevelVisibility' +import { RequireLevelFromDoubleLevelVisibilityDynamic } from '@/auth/authorizer/RequireLevelFromDoubleLevelVisibilityDynamic' +import { RequirePermission } from '@/auth/authorizer/RequirePermission' +import { RequireVisibilityFilter } from '@/auth/authorizer/RequireVisibilityFilter' export const newsAuth = { - // Should prob. be a permission: - create: RequireNothing.staticFields({}), - // visibility 1 (admin) - destroy: RequireNothing.staticFields({}), - update: RequireNothing.staticFields({}), - updateArticle: RequireNothing.staticFields({}), - //visibility 2 (read) - readCurrent: RequireNothing.staticFields({}), // auth filter!! - readOldPage: RequireNothing.staticFields({}), // auth filter!! - read: RequireNothing.staticFields({}), + create: RequirePermission.staticFields({ permission: 'NEWS_CREATE' }), + + readDoubleLevelMatrix: + RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'REGULAR', bypassPermission: 'NEWS_ADMIN' }), + updateRegularLevel: + RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'ADMIN', bypassPermission: 'NEWS_ADMIN' }), + updateAdminLevel: + RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'ADMIN', bypassPermission: 'NEWS_ADMIN' }), + + destroy: RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'ADMIN', bypassPermission: 'NEWS_ADMIN' }), + update: RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'ADMIN', bypassPermission: 'NEWS_ADMIN' }), + updateArticle: RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'ADMIN', bypassPermission: 'NEWS_ADMIN' }), + publish: RequireLevelFromDoubleLevelVisibility.staticFields({ level: 'ADMIN', bypassPermission: 'NEWS_ADMIN' }), + + read: RequireLevelFromDoubleLevelVisibilityDynamic.staticFields({ bypassPermission: 'NEWS_ADMIN' }), + readCurrent: RequireVisibilityFilter.staticFields({ bypassPermission: 'NEWS_ADMIN' }), + readOldPage: RequireVisibilityFilter.staticFields({ bypassPermission: 'NEWS_ADMIN' }), } as const diff --git a/src/services/news/operations.ts b/src/services/news/operations.ts index ee137b9ef..14a8e38ec 100644 --- a/src/services/news/operations.ts +++ b/src/services/news/operations.ts @@ -3,17 +3,61 @@ import { newsSchemas } from './schemas' import { defaultNewsArticleOldCutoff, newsArticleRealtionsIncluder, simpleNewsArticleRealtionsIncluder } from './constants' import { newsAuth } from './auth' import { articleOperations } from '@/cms/articles/operations' +import { notificationOperations } from '@/services/notifications/operations' +import { visibilityOperations } from '@/services/visibility/operations' +import { implementDoubleLevelVisibilityOperations, toMatrix, visibilityIncluder } from '@/services/visibility/implement' import { defineOperation } from '@/services/serviceOperation' import { cursorPageingSelection } from '@/lib/paging/cursorPageingSelection' import { ServerError } from '@/services/error' import { implementUpdateArticleOperations } from '@/cms/articles/implement' import { z } from 'zod' +const visibility = implementDoubleLevelVisibilityOperations({ + implementationParamsSchema: newsSchemas.params, + authorizers: { + readDoubleLevelMatrix: ({ doubleLevelMatrix }) => newsAuth.readDoubleLevelMatrix.dynamicFields({ + doubleLevelMatrix, + }), + updateRegularLevel: ({ doubleLevelMatrix }) => newsAuth.updateRegularLevel.dynamicFields({ + doubleLevelMatrix, + }), + updateAdminLevel: ({ doubleLevelMatrix }) => newsAuth.updateAdminLevel.dynamicFields({ + doubleLevelMatrix, + }) + }, + readDoubleLevel: async ({ prisma, implementationParams, include }) => { + const news = await prisma.newsArticle.findUniqueOrThrow({ + where: { id: implementationParams.id }, + include: { + visibilityRegular: { include }, + visibilityAdmin: { include } + } + }) + return { + regularLevel: news.visibilityRegular, + adminLevel: news.visibilityAdmin + } + } +}) + const read = defineOperation({ - authorizer: () => newsAuth.read.dynamicFields({}), - paramsSchema: z.object({ - id: z.number() - }), + authorizer: async ({ params, prisma }) => { + const news = await prisma.newsArticle.findUniqueOrThrow({ + where: { id: params.id }, + include: { + visibilityRegular: { include: visibilityIncluder }, + visibilityAdmin: { include: visibilityIncluder } + } + }) + return newsAuth.read.dynamicFields({ + level: news.published ? 'REGULAR' : 'ADMIN', + doubleLevelMatrix: { + regularLevel: toMatrix(news.visibilityRegular), + adminLevel: toMatrix(news.visibilityAdmin) + } + }) + }, + paramsSchema: newsSchemas.params, operation: async ({ prisma, params }) => { const news = await prisma.newsArticle.findUnique({ where: { @@ -27,6 +71,7 @@ const read = defineOperation({ }) export const newsOperations = { + visibility, create: defineOperation({ authorizer: () => newsAuth.create.dynamicFields({}), dataSchema: newsSchemas.create, @@ -42,6 +87,9 @@ export const newsOperations = { operationImplementationFields: { special: null } }) + const visibilityRegular = await visibilityOperations.create.internalCall({}) + const visibilityAdmin = await visibilityOperations.create.internalCall({}) + const news = await prisma.newsArticle.create({ data: { description, @@ -51,6 +99,16 @@ export const newsOperations = { } }, endDateTime: endDateTime || backupEndDateTime, + visibilityRegular: { + connect: { + id: visibilityRegular.id + } + }, + visibilityAdmin: { + connect: { + id: visibilityAdmin.id + } + }, }, include: newsArticleRealtionsIncluder, }) @@ -58,25 +116,44 @@ export const newsOperations = { } }), destroy: defineOperation({ - authorizer: () => newsAuth.destroy.dynamicFields({}), - paramsSchema: z.object({ - id: z.number() + authorizer: async ({ params, prisma }) => newsAuth.destroy.dynamicFields({ + doubleLevelMatrix: await visibility.readDoubleLevelMatrixInternal({ params, prisma }) }), + paramsSchema: newsSchemas.params, + opensTransaction: true, operation: async ({ prisma, params }) => { - const news = await prisma.newsArticle.delete({ - where: { id: params.id }, + const news = await prisma.newsArticle.findUnique({ where: { id: params.id } }) + if (!news) throw new ServerError('NOT FOUND', `article ${params.id} not found`) + + await prisma.$transaction(async tx => { + await tx.newsArticle.delete({ where: { id: params.id } }) + await visibilityOperations.destroy.internalCall({ + prisma: tx, + params: { visibilityId: news.visibilityAdminId }, + }) + await visibilityOperations.destroy.internalCall({ + prisma: tx, + params: { visibilityId: news.visibilityRegularId }, + }) }) + await articleOperations.destroy.internalCall({ params: { articleId: news.articleId } }) } }), readCurrent: defineOperation({ authorizer: () => newsAuth.readCurrent.dynamicFields({}), - operation: async ({ prisma }) => { + operation: async ({ prisma }, prismaWhereFilter) => { const news = await prisma.newsArticle.findMany({ where: { endDateTime: { gte: new Date(), - } + }, + ...(prismaWhereFilter ? { + OR: [ + { published: true, visibilityRegular: prismaWhereFilter }, + { published: false, visibilityAdmin: prismaWhereFilter }, + ] + } : {}), }, orderBy: { article: { @@ -94,12 +171,18 @@ export const newsOperations = { readOldPage: defineOperation({ paramsSchema: newsSchemas.readOldPage, authorizer: () => newsAuth.readOldPage.dynamicFields({}), - operation: async ({ prisma, params }) => { + operation: async ({ prisma, params }, prismaWhereFilter) => { const news = await prisma.newsArticle.findMany({ where: { endDateTime: { lt: new Date(), - } + }, + ...(prismaWhereFilter ? { + OR: [ + { published: true, visibilityRegular: prismaWhereFilter }, + { published: false, visibilityAdmin: prismaWhereFilter }, + ] + } : {}), }, ...cursorPageingSelection(params.paging.page), orderBy: { @@ -117,10 +200,10 @@ export const newsOperations = { }), read, update: defineOperation({ - authorizer: () => newsAuth.update.dynamicFields({}), - paramsSchema: z.object({ - id: z.number(), + authorizer: async ({ params, prisma }) => newsAuth.update.dynamicFields({ + doubleLevelMatrix: await visibility.readDoubleLevelMatrixInternal({ params, prisma }) }), + paramsSchema: newsSchemas.params, dataSchema: newsSchemas.update, operation: async ({ prisma, params, data }) => prisma.newsArticle.update({ @@ -136,12 +219,44 @@ export const newsOperations = { } }) }), + publish: defineOperation({ + authorizer: async ({ params, prisma }) => newsAuth.publish.dynamicFields({ + doubleLevelMatrix: await visibility.readDoubleLevelMatrixInternal({ params, prisma }) + }), + paramsSchema: newsSchemas.params, + operation: async ({ prisma, params }) => { + const news = await prisma.newsArticle.update({ + where: { id: params.id }, + data: { published: true }, + include: simpleNewsArticleRealtionsIncluder, + }) + + await notificationOperations.createSpecial.internalCall({ + params: { + special: 'NEW_NEWS_ARTICLE', + }, + data: { + title: 'Ny nyhetsartikkel', // TODO: Add info about the article + message: 'En ny nyhetsartikkel er publisert', + }, + }) + + return { + ...news, + coverImage: news.article.coverImage.image + } + } + }), updateArticle: implementUpdateArticleOperations({ implementationParamsSchema: z.object({ newsId: z.number(), }), - // eslint-disable-next-line @typescript-eslint/no-unused-vars - authorizer: ({ implementationParams }) => newsAuth.updateArticle.dynamicFields({}), + authorizer: async ({ implementationParams, prisma }) => newsAuth.updateArticle.dynamicFields({ + doubleLevelMatrix: await visibility.readDoubleLevelMatrixInternal({ + params: { id: implementationParams.newsId }, + prisma + }) + }), ownedArticles: async ({ implementationParams }) => { const news = await read({ params: { id: implementationParams.newsId }, bypassAuth: true }) return [news.article] diff --git a/src/services/news/schemas.ts b/src/services/news/schemas.ts index dc9811e99..d473e6a56 100644 --- a/src/services/news/schemas.ts +++ b/src/services/news/schemas.ts @@ -9,6 +9,9 @@ const baseSchema = z.object({ }) export const newsSchemas = { + params: z.object({ + id: z.number() + }), create: baseSchema.pick({ name: true, description: true, diff --git a/src/services/permissions/constants.ts b/src/services/permissions/constants.ts index 4f21d7ee9..136515d8a 100644 --- a/src/services/permissions/constants.ts +++ b/src/services/permissions/constants.ts @@ -493,8 +493,18 @@ export const permissionConfig = { APPLICATION_WRITE: { name: 'Søknadsskriver', description: ` - Kan skrive søknader til alle aktive søknadsperioder. + Kan skrive søknader til alle aktive søknadsperioder. `, category: 'applications', }, + NEWS_CREATE: { + name: 'Lage nyhetsartikkel', + description: 'kan lage nyhetsartikler', + category: 'public', + }, + NEWS_ADMIN: { + name: 'Nyhetsadministrator', + description: 'kan administrere alle nyhetsartikler uavhengig av synlighet', + category: 'public', + }, } satisfies Record From 8e7908cf3d0b0408e8a9fd6cbfbd658bd7dbcadc Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad Date: Wed, 29 Jul 2026 12:59:08 +0200 Subject: [PATCH 2/3] chore: show visibility admin on frontend --- .../dynamic/[name]/CollectionAdmin.tsx | 13 +- src/app/news/[nameAndId]/EditNews.module.scss | 19 +- src/app/news/[nameAndId]/EditNews.tsx | 189 +++++++++++++----- src/app/news/[nameAndId]/page.tsx | 11 +- src/auth/visibility/emptyVisibility.ts | 15 ++ src/services/news/actions.ts | 2 +- src/services/news/auth.ts | 3 +- src/services/news/operations.ts | 38 ++-- src/services/news/schemas.ts | 3 + 9 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 src/auth/visibility/emptyVisibility.ts diff --git a/src/app/image-collections/dynamic/[name]/CollectionAdmin.tsx b/src/app/image-collections/dynamic/[name]/CollectionAdmin.tsx index 2144dbc49..53703af68 100644 --- a/src/app/image-collections/dynamic/[name]/CollectionAdmin.tsx +++ b/src/app/image-collections/dynamic/[name]/CollectionAdmin.tsx @@ -10,6 +10,7 @@ import useEditMode from '@/hooks/useEditMode' import { dynamicImageAuth } from '@/services/images/dynamic/auth' import Button from '@/components/UI/Button' import { configureAction } from '@/services/configureAction' +import { EMPTY_VISIBILITY } from '@/auth/visibility/emptyVisibility' import { updateDynamicImageCollectionAction, destroyDynamicImageCollectionAction, @@ -29,22 +30,12 @@ type PropTypes = { doubleLevelVisibility: DoubleLevelVisibilityMatrix | null, refreshImages: () => void, } -/** - * Fallback in case one was not able to read visibility. In this case, - * the auths might be wrong, but this allows the admin to at least still render. - */ -const UNREADABLE_VISIBILITY: DoubleLevelVisibilityMatrix = { - regularLevel: { requirements: [{ conditions: [] }] }, - adminLevel: { requirements: [{ conditions: [] }] }, -} export default function CollectionAdmin({ collection, doubleLevelVisibility, refreshImages }: PropTypes) { const { id: collectionId } = collection const router = useRouter() - const doubleLevelMatrix = doubleLevelVisibility ?? UNREADABLE_VISIBILITY + const doubleLevelMatrix = doubleLevelVisibility ?? EMPTY_VISIBILITY - // One authorizer check per action - each button/form below is gated by the exact same - // authorizer its own action uses server-side, not a single blanket "can edit collection" check. const canUploadOne = useEditMode({ authorizer: dynamicImageAuth.uploadImage.dynamicFields({ doubleLevelMatrix }) }) diff --git a/src/app/news/[nameAndId]/EditNews.module.scss b/src/app/news/[nameAndId]/EditNews.module.scss index d7cfd193e..ef042e159 100644 --- a/src/app/news/[nameAndId]/EditNews.module.scss +++ b/src/app/news/[nameAndId]/EditNews.module.scss @@ -6,15 +6,28 @@ background-color: ohma.$colors-secondary; color: ohma.$colors-white; display: flex; + flex-wrap: wrap; + gap: 1em; .update { - margin-right: 1em; max-width: 200px; } .visibility { - margin-right: 1em; flex: 1; + display: flex; + flex-wrap: wrap; + gap: 1em; + h3 { + margin-bottom: .5em; + } button[type="submit"] { - width: 200px; + width: 200px; + } + } + .publish { + max-width: 200px; + p { + margin-bottom: .5em; + font-style: italic; } } } \ No newline at end of file diff --git a/src/app/news/[nameAndId]/EditNews.tsx b/src/app/news/[nameAndId]/EditNews.tsx index 0458b5241..ed12b5786 100644 --- a/src/app/news/[nameAndId]/EditNews.tsx +++ b/src/app/news/[nameAndId]/EditNews.tsx @@ -5,16 +5,26 @@ import TextInput from '@/components/UI/TextInput' import useEditMode from '@/hooks/useEditMode' import Textarea from '@/components/UI/Textarea' import DateInput from '@/components/UI/DateInput' -import { destroyNewsAction, updateNewsAction } from '@/services/news/actions' +import VisibilityAdmin from '@/components/Visibility/VisibilityAdmin/VisibilityAdmin' +import { + destroyNewsAction, + setNewsPublishedAction, + updateNewsAction, + updateNewsAdminLevelVisibilityAction, + updateNewsRegularLevelVisibilityAction, +} from '@/services/news/actions' import { formatVevenUri } from '@/lib/urlEncoding' import { newsAuth } from '@/services/news/auth' import { configureAction } from '@/services/configureAction' +import { EMPTY_VISIBILITY } from '@/auth/visibility/emptyVisibility' import { useRouter } from 'next/navigation' +import type { DoubleLevelVisibilityMatrix } from '@/services/visibility/types' import type { ExpandedNewsArticle } from '@/services/news/types' import type { ReactNode } from 'react' type PropTypes = { news: ExpandedNewsArticle + doubleLevelVisibility: DoubleLevelVisibilityMatrix | null children?: ReactNode } @@ -22,70 +32,149 @@ type PropTypes = { * This component renders children if editmode is off and news admin tools if editmode is on * pass it not: id of article to make sure not to display that article */ -export default function EditNews({ news, children }: PropTypes) { +export default function EditNews({ news, doubleLevelVisibility, children }: PropTypes) { const { refresh, push } = useRouter() - //TODO: chack visibility - const canEdit = useEditMode({ - authorizer: newsAuth.update.dynamicFields({}) + + const doubleLevelMatrix = doubleLevelVisibility ?? EMPTY_VISIBILITY + const canUpdate = useEditMode({ + authorizer: newsAuth.update.dynamicFields({ doubleLevelMatrix }) + }) + const canDestroy = useEditMode({ + authorizer: newsAuth.destroy.dynamicFields({ doubleLevelMatrix }) + }) + const canUpdateRegularVisibility = useEditMode({ + authorizer: newsAuth.updateRegularLevel.dynamicFields({ doubleLevelMatrix }) + }) + const canUpdateAdminVisibility = useEditMode({ + authorizer: newsAuth.updateAdminLevel.dynamicFields({ doubleLevelMatrix }) + }) + const canSetPublished = useEditMode({ + authorizer: newsAuth.setPublished.dynamicFields({ doubleLevelMatrix }) }) - if (!canEdit) return children - // TODO: VISINILITY ADMIN + // The editors are bound to doubleLevelVisibility rather than doubleLevelMatrix on purpose: + // saving the fallback would overwrite the real requirements with fabricated ones. + const canEditVisibility = doubleLevelVisibility !== null && + (canUpdateRegularVisibility || canUpdateAdminVisibility) + if (!canUpdate && !canDestroy && !canEditVisibility && !canSetPublished) return children const updateAction = configureAction( updateNewsAction, { params: { id: news.id } } ) + const setPublishedAction = configureAction( + setNewsPublishedAction, + { params: { id: news.id } } + ) + return (
-
`/news/${data ? formatVevenUri(data.articleName, data.id) : ''}`} - submitText="oppdater" - > - - -