Skip to content
Open
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
13 changes: 2 additions & 11 deletions src/app/image-collections/dynamic/[name]/CollectionAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 })
})
Expand Down
19 changes: 16 additions & 3 deletions src/app/news/[nameAndId]/EditNews.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
189 changes: 139 additions & 50 deletions src/app/news/[nameAndId]/EditNews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,176 @@ 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
}

/**
* 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 (
<div className={styles.EditNews}>
<div className={styles.update}>
<Form
action={updateAction}
navigateOnSuccess={(data) => `/news/${data ? formatVevenUri(data.articleName, data.id) : ''}`}
submitText="oppdater"
>
<TextInput
color="white"
defaultValue={news.articleName}
label="navn"
name="name"
/>
<DateInput
color="white"
defaultValue={news.endDateTime}
label="sluttdato"
name="endDateTime"
/>
<Textarea defaultValue={news.description || ''} label="beskrivelse" name="description" />
</Form>
<Form
action={
configureAction(
destroyNewsAction,
{ params: { id: news.id } }
)
}
successCallback={() => {
push('/news')
refresh()
}}
submitText="slett nyhet"
confirmation={{
confirm: true,
text: 'Er du sikker på at du vil slette denne nyheten? Dette kan ikke angres.'
}}
submitColor="red"
>
</Form>
</div>
<div className={styles.visibility}>
Her kommer visibility settings

{
canUpdate && (
<Form
action={updateAction}
navigateOnSuccess={(data) => `/news/${data ? formatVevenUri(data.articleName, data.id) : ''}`}
submitText="oppdater"
>
<TextInput
color="white"
defaultValue={news.articleName}
label="navn"
name="name"
/>
<DateInput
color="white"
defaultValue={news.endDateTime}
label="sluttdato"
name="endDateTime"
/>
<Textarea defaultValue={news.description || ''} label="beskrivelse" name="description" />
</Form>
)
}
{
canDestroy && (
<Form
action={
configureAction(
destroyNewsAction,
{ params: { id: news.id } }
)
}
successCallback={() => {
push('/news')
refresh()
}}
submitText="slett nyhet"
confirmation={{
confirm: true,
text: 'Er du sikker på at du vil slette denne nyheten? Dette kan ikke angres.'
}}
submitColor="red"
>
</Form>
)
}
</div>
{
doubleLevelVisibility && (
<div className={styles.visibility}>
{
canUpdateRegularVisibility && (
<div>
<h3>Vanlig visning</h3>
<VisibilityAdmin
visibility={doubleLevelVisibility.regularLevel}
visibilityId={news.visibilityRegularId}
updateVisibilityAction={configureAction(
updateNewsRegularLevelVisibilityAction,
{ implementationParams: { id: news.id } }
)}
/>
</div>
)
}
{
canUpdateAdminVisibility && (
<div>
<h3>Adminvisning</h3>
<VisibilityAdmin
visibility={doubleLevelVisibility.adminLevel}
visibilityId={news.visibilityAdminId}
updateVisibilityAction={configureAction(
updateNewsAdminLevelVisibilityAction,
{ implementationParams: { id: news.id } }
)}
/>
</div>
)
}
</div>
)
}

<div className={styles.publish}>

<p>{news.published ? 'Publisert' : 'Ikke publisert'}</p>
{
canSetPublished && (
<Form
action={() => setPublishedAction({ data: { published: !news.published } })}
refreshOnSuccess
submitText={news.published ? 'avpubliser' : 'publiser'}
submitColor={news.published ? 'red' : 'green'}
confirmation={{
confirm: true,
text: news.published
? 'Er du sikker på at du vil avpublisere denne nyheten? ' +
'Den vil da bare være synlig for de som kan administrere den.'
: 'Er du sikker på at du vil publisere denne nyheten? ' +
'Det sender ut et varsel til alle som kan se den.'
}}
/>
)
}
</div>
</div>
)
Expand Down
11 changes: 9 additions & 2 deletions src/app/news/[nameAndId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CurrentNews from '@/app/news/CurrentNews'
import Article from '@/cms/Article/Article'
import {
readNewsAction,
readNewsDoubleLevelVisibilityAction,
updateNewsArticleAction,
updateNewsArticleAddSectionAction,
updateNewsArticleCmsImageAction,
Expand All @@ -19,6 +20,7 @@ import SlideInOnView from '@/components/SlideInOnView/SlideInOnView'
import { decodeVevenUriHandleError } from '@/lib/urlEncoding'
import { configureAction } from '@/services/configureAction'
import { newsAuth } from '@/services/news/auth'
import { EMPTY_VISIBILITY } from '@/auth/visibility/emptyVisibility'
import { ServerSession } from '@/auth/session/ServerSession'
import { notFound } from 'next/navigation'

Expand All @@ -34,7 +36,12 @@ export default async function NewsArticle({ params }: PropTypes) {
if (!res.success) notFound()
const news = res.data

const canEdit = newsAuth.updateArticle.dynamicFields({}).auth(
const readDoubleLevelVisibility = await readNewsDoubleLevelVisibilityAction({ params: { id: news.id } })
const doubleLevelVisibility = readDoubleLevelVisibility.success ? readDoubleLevelVisibility.data : null

const canEdit = newsAuth.updateArticle.dynamicFields({
doubleLevelMatrix: doubleLevelVisibility ?? EMPTY_VISIBILITY
}).auth(
await ServerSession.fromNextAuth()
).toJsObject()
return (
Expand Down Expand Up @@ -88,7 +95,7 @@ export default async function NewsArticle({ params }: PropTypes) {
}}
/>
<SlideInOnView>
<EditNews news={news}>
<EditNews news={news} doubleLevelVisibility={doubleLevelVisibility}>
<div className={styles.moreNews}>
<h1>Flere nyheter</h1>
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}))
15 changes: 15 additions & 0 deletions src/auth/visibility/emptyVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { DoubleLevelVisibilityMatrix } from '@/services/visibility/types'

/**
* A DoubleLevelVisibilityMatrix with one empty requirement on each level. A requirement with no
* conditions can never be satisfied (see checkVisibility), so an authorizer fed this denies
* everyone who does not hold the bypass permission - the safe direction to fail in when the real
* visibility could not be read, and it still lets the rest of an admin panel render.
*
* Only ever safe to authorize against: never hand it to an editor, since saving it would overwrite
* the real requirements with these empty ones.
*/
export const EMPTY_VISIBILITY: DoubleLevelVisibilityMatrix = {
regularLevel: { requirements: [{ conditions: [] }] },
adminLevel: { requirements: [{ conditions: [] }] },
}
6 changes: 6 additions & 0 deletions src/prisma/schema/news.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
4 changes: 4 additions & 0 deletions src/prisma/schema/permission.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ enum Permission {

//Flair/badge/cape
FLAIR_ADMIN

// News
NEWS_CREATE
NEWS_ADMIN
}

model GroupPermission {
Expand Down
Loading
Loading