Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG NODE_VERSION="24.12"
ARG NODE_VERSION="25"
ARG ALPINE_VERSION="3.23"

FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS base
Expand Down
4,647 changes: 2,575 additions & 2,072 deletions package-lock.json

Large diffs are not rendered by default.

45 changes: 23 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@
"prisma-studio": "DB_URI=\"postgresql://user:password@localhost/devdb\" npx prisma studio"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.3.0",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@react-email/components": "^0.5.7",
"@react-email/render": "^1.4.0",
"bcrypt": "^5.1.1",
"eslint-plugin-react-hooks": "^6.1.1",
"@react-email/components": "^1.0.10",
"@react-email/render": "^2.0.4",
"bcrypt": "^6.0.0",
"eslint-plugin-react-hooks": "^7.0.1",
"canvas-confetti": "^1.9.4",
"html5-qrcode": "^2.3.8",
"jsonwebtoken": "^9.0.3",
Expand All @@ -45,8 +45,8 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-fontawesome": "^1.7.1",
"react-intersection-observer": "^9.16.0",
"react-pdf": "^9.1.1",
"react-intersection-observer": "^10.0.3",
"react-pdf": "^10.4.1",
"react-simplemde-editor": "^5.2.0",
"rehype-format": "^5.0.1",
"rehype-stringify": "^10.0.1",
Expand All @@ -56,32 +56,33 @@
"server-only": "^0.0.1",
"sharp": "^0.34.5",
"unified": "^11.0.5",
"uuid": "^10.0.0",
"uuid": "^13.0.0",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
"zod": "^3.24.1",
"zod-form-data": "^2.0.4"
"zod": "^4.3.6",
"zod-form-data": "^3.0.1"
},
"devDependencies": {
"@babel/preset-typescript": "^7.28.5",
"@jest/globals": "^29.7.0",
"@types/bcrypt": "^5.0.2",
"@jest/globals": "^30.3.0",
"@types/bcrypt": "^6.0.0",
"@types/canvas-confetti": "^1.9.0",
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/luxon": "^3.7.1",
"@types/node": "^24.10.7",
"@types/node": "^25.5.0",
"@types/nodemailer": "^7.0.9",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@types/uuid": "^10.0.0",
"eslint": "^9.39.2",
"@types/uuid": "^11.0.0",
"eslint": "^9.39.4",
"eslint-config-next": "^16.1.6",
"jest": "^29.7.0",
"jest": "^30.3.0",
"prisma": "^7.3.0",
"ts-jest": "^29.4.6",
"ts-jest": "^29.4.9",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.0"
}
}
4 changes: 3 additions & 1 deletion src/app/_components/Cms/Article/AddSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export default function AddSection({
await addSectionToArticleAction({
data: {
includeParts: {
[includePart]: true,
cmsImage: includePart === 'cmsImage',
cmsParagraph: includePart === 'cmsParagraph',
cmsLink: includePart === 'cmsLink',
}
}
})
Expand Down
4 changes: 2 additions & 2 deletions src/app/_components/PdfDocument/PdfDocument.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
import { Document, Page, pdfjs } from 'react-pdf'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons'
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'
import 'react-pdf/dist/Page/AnnotationLayer.css'
import 'react-pdf/dist/Page/TextLayer.css'

pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`

Expand Down
6 changes: 4 additions & 2 deletions src/lib/fields/zpn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { FIELD_IS_PRESENT_VALUE } from './constants'
import { dateMatchCron } from '@/lib/dates/cron'
import { z } from 'zod'
import { zfd } from 'zod-form-data'
import type { EnumLike } from 'zod'

// EnumLike was removed from zod v4 top-level exports; define it locally
type EnumLike = Readonly<Record<string, string | number>>

export namespace Zpn {
/**
Expand All @@ -12,7 +14,7 @@ export namespace Zpn {
export const checkboxOrBoolean = ({ message }: { label: string, message?: string }) => z.union([
z.boolean(), // mosltly for the backend
zfd.repeatable(z.literal('on', {
errorMap: message ? () => ({ message }) : undefined,
error: message ?? undefined,
}).or(z.literal(FIELD_IS_PRESENT_VALUE)).array()) // mostly for the frontend (forms)
]).transform(value => {
if (typeof value === 'boolean') return value
Expand Down
12 changes: 8 additions & 4 deletions src/services/Validation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ServerError } from './error'
import type { ErrorMessage } from './error'

Check failure on line 2 in src/services/Validation.ts

View workflow job for this annotation

GitHub Actions / lint

'ErrorMessage' is defined but never used
import { z } from 'zod'
import { zfd } from 'zod-form-data'

Expand All @@ -17,7 +18,9 @@
type PureTsTypeOfSchema<
T extends z.ZodRawShape,
Partialized extends boolean = false
> = Partialized extends true ? Partial<z.infer<ReturnType<typeof z.object<T>>>> : z.infer<ReturnType<typeof z.object<T>>>
> = Partialized extends true
? z.infer<ReturnType<ReturnType<typeof z.object<T>>["partial"]>>

Check failure on line 22 in src/services/Validation.ts

View workflow job for this annotation

GitHub Actions / lint

Strings must use singlequote
: z.infer<ReturnType<typeof z.object<T>>>

/**
* Type for the Transformer to transfer between type and detailed types.
Expand Down Expand Up @@ -169,7 +172,7 @@
const parse = this.detailedSchema.refine(
this.refiner ? this.refiner.fcn : () => true, this.refiner ? this.refiner.message : 'Noe uforusett skjedde'
).safeParse(data)
if (!parse.success) throw new ServerError('BAD PARAMETERS', parse.error.issues)
if (!parse.success) throw new ServerError('BAD PARAMETERS', parse.error.issues.map(issue => ({ message: issue.message, path: issue.path.filter((p): p is string | number => typeof p === 'string' || typeof p === 'number') })))

Check failure on line 175 in src/services/Validation.ts

View workflow job for this annotation

GitHub Actions / lint

Identifier name 'p' is too short (< 2)

Check failure on line 175 in src/services/Validation.ts

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 232. Maximum allowed is 125
return parse.data
}
}
Expand Down Expand Up @@ -224,9 +227,10 @@

detailedValidate(data: PureTsTypeOfSchema<Detailed, true> | unknown) {
const parse = this.detailedSchema.partial().refine(
this.refiner ? this.refiner.fcn : () => true, this.refiner ? this.refiner.message : 'Noe uforusett skjedde'
this.refiner ? this.refiner.fcn : () => true,
this.refiner ? this.refiner.message : 'Noe uforusett skjedde'
).safeParse(data)
if (!parse.success) throw new ServerError('BAD PARAMETERS', parse.error.issues)
if (!parse.success) throw new ServerError('BAD PARAMETERS', parse.error.issues.map(issue => ({ message: issue.message, path: issue.path.filter((p): p is string | number => typeof p === 'string' || typeof p === 'number') })))

Check failure on line 233 in src/services/Validation.ts

View workflow job for this annotation

GitHub Actions / lint

Identifier name 'p' is too short (< 2)

Check failure on line 233 in src/services/Validation.ts

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 232. Maximum allowed is 125
return parse.data
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/services/actionError.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { errorCodes, type ErrorCode, type ErrorMessage } from '@/services/error'
import { ParseError, Smorekopp } from '@/services/error'
import type { AuthStatus } from '@/auth/authorizer/AuthResult'
import type { SafeParseError } from 'zod'
import type { ZodSafeParseError as SafeParseError } from 'zod'
import type { ActionError, ActionReturn } from './actionTypes'

/**
Expand Down Expand Up @@ -34,7 +34,7 @@
success: false,
httpCode: 400,
errorCode: 'BAD PARAMETERS',
error: parse.error.issues,
error: parse.error.issues.map(issue => ({ message: issue.message, path: issue.path.filter((p): p is string | number => typeof p === "string" || typeof p === "number") })),

Check failure on line 37 in src/services/actionError.ts

View workflow job for this annotation

GitHub Actions / lint

Strings must use singlequote

Check failure on line 37 in src/services/actionError.ts

View workflow job for this annotation

GitHub Actions / lint

Strings must use singlequote

Check failure on line 37 in src/services/actionError.ts

View workflow job for this annotation

GitHub Actions / lint

Identifier name 'p' is too short (< 2)

Check failure on line 37 in src/services/actionError.ts

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 179. Maximum allowed is 125
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/services/career/jobAds/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { z } from 'zod'

const baseSchema = z.object({
companyId: z.coerce.number({
errorMap: () => ({ message: 'Velg en bedrift' }),
error: 'Velg en bedrift',
}).int().positive().int(),
articleName: z.string().max(50, 'max lengde 50').min(2, 'min lengde 2'),
description: z.string().max(200, 'max lengde 200').min(2, 'min lengde 2').or(z.literal('')),
Expand Down
19 changes: 6 additions & 13 deletions src/services/cms/articleSections/implement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type { articleSectionSchemas } from './schemas'
import type { AuthorizerDynamicFieldsBound } from '@/auth/authorizer/Authorizer'
import type { Prisma } from '@/prisma-generated-pn-types'
import type { z } from 'zod'
import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation'
import type {
ArgsAuthGetterAndOwnershipCheck,
} from '@/services/serviceOperation'

type ParamsSchema = typeof articleSectionSchemas.params
type OwnedArticleSection = Prisma.ArticleSectionGetPayload<{
Expand All @@ -31,16 +33,10 @@ export function implementUpdateArticleSectionOperations<
}: {
implementationParamsSchema: ImplementationParamsSchema,
authorizer: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => AuthorizerDynamicFieldsBound | Promise<AuthorizerDynamicFieldsBound>,
ownedArticleSections: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => Promise<OwnedArticleSection[]>
destroyOnEmpty: boolean
}) {
Expand All @@ -56,10 +52,7 @@ export function implementUpdateArticleSectionOperations<
}

const getOwnedIds = async (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => {
const ownedArticleSectionsComputed = await ownedArticleSections(args)
return {
Expand Down
17 changes: 7 additions & 10 deletions src/services/cms/articles/implement.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { articleOperations } from './operations'
import { implementUpdateArticleSectionOperations } from '@/cms/articleSections/implement'
import { cmsImageOperations } from '@/cms/images/operations'
import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation'
import type {
ArgsAuthGetterAndOwnershipCheck,
} from '@/services/serviceOperation'
import type { AuthorizerDynamicFieldsBound } from '@/auth/authorizer/Authorizer'
import type { Prisma } from '@/prisma-generated-pn-types'
import type { articleSchemas } from './schemas'
Expand Down Expand Up @@ -33,16 +35,10 @@ export function implementUpdateArticleOperations<
}: {
implementationParamsSchema: ImplementationParamsSchema,
authorizer: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => AuthorizerDynamicFieldsBound | Promise<AuthorizerDynamicFieldsBound>,
ownedArticles: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => Promise<OwnedArticle[]>
}) {
const ownershipCheckArticle = async (
Expand Down Expand Up @@ -72,7 +68,8 @@ export function implementUpdateArticleOperations<
implementationParamsSchema,
authorizer,
ownershipCheck: async (args) => {
const coverCmsImagesIds = (await ownedArticles(args)).map(article => article.coverImage.id)
const ownedArts = await ownedArticles(args)
const coverCmsImagesIds = ownedArts.map(article => article.coverImage.id)
return coverCmsImagesIds.includes(args.params.cmsImageId)
}
}),
Expand Down
2 changes: 1 addition & 1 deletion src/services/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SafeParseError } from 'zod'
import type { ZodSafeParseError as SafeParseError } from 'zod'
import type { AuthStatus } from '@/auth/authorizer/AuthResult'

export const errorCodes = [
Expand Down
2 changes: 1 addition & 1 deletion src/services/images/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const imageOperations = {
const { file, ...meta } = data
const buffer = Buffer.from(await file.arrayBuffer())
const avifBuffer = await sharp(buffer).toFormat('avif').avif(avifConvertionOptions).toBuffer()
const avifFile = new File([avifBuffer], 'image.avif', { type: 'image/avif' })
const avifFile = new File([new Uint8Array(avifBuffer)], 'image.avif', { type: 'image/avif' })

const uploadPromises = [
createOneInStore(avifFile, ['avif'], imageSizes.small),
Expand Down
18 changes: 11 additions & 7 deletions src/services/serviceOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export function defineSubOperation<
): ServiceOperation<OpensTransaction, Return, ParamsSchema, DataSchema, ImplementationParamsSchema> => {
const expectedArgsArePresent = (
args: ServiceOperationExecuteArgs<'UNSAFE', ParamsSchema, DataSchema, ImplementationParamsSchema>
): args is ServiceOperationExecuteArgs<'SAFE', ParamsSchema, DataSchema, ImplementationParamsSchema> => {
): args is Omit<ArgsAuthGetterAndOwnershipCheck<OpensTransaction, ParamsSchema, DataSchema, ImplementationParamsSchema>, 'prisma'> => {
const paramsMatch = Boolean(args.params) === Boolean(serviceOperationConfig.paramsSchema)
const dataMatches = Boolean(args.data) === Boolean(serviceOperationConfig.dataSchema)
const implementationParamsMatch =
Expand Down Expand Up @@ -403,7 +403,9 @@ export function defineSubOperation<
}

const authorizer = await prismaErrorWrapper(
() => implementationArgs.authorizer({ ...args, prisma })
() => implementationArgs.authorizer(
{ ...args, prisma }
)
)
const authResult = authorizer.auth(session)

Expand All @@ -417,10 +419,9 @@ export function defineSubOperation<
})()

const ownershipCheckResult = await prismaErrorWrapper(
() => implementationArgs.ownershipCheck({
...args,
prisma,
})
() => implementationArgs.ownershipCheck(
{ ...args, prisma }
)
)
if (!ownershipCheckResult) {
throw new Smorekopp('DISSALLOWED', `
Expand All @@ -432,7 +433,10 @@ export function defineSubOperation<
return prismaErrorWrapper(() =>
serviceOperationConfig.operation(
implementationArgs.operationImplementationFields!
)({ ...args, prisma, bypassAuth, session }, prismaWhereFilter)
)(
{ ...args, prisma, bypassAuth, session },
prismaWhereFilter
)
)
})
}
Expand Down
19 changes: 6 additions & 13 deletions src/services/visibility/implement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { visibilityOperations } from './operations'
import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation'
import type {
ArgsAuthGetterAndOwnershipCheck,
} from '@/services/serviceOperation'
import type { AuthorizerDynamicFieldsBound } from '@/auth/authorizer/Authorizer'
import type { Prisma } from '@/prisma-generated-pn-types'
import type { visibilitySchemas } from './schemas'
Expand Down Expand Up @@ -31,23 +33,14 @@ export function implementVisibilityOperations<
implementationParamsSchema: ImplementationParamsSchema,
authorizers: {
update: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => AuthorizerDynamicFieldsBound | Promise<AuthorizerDynamicFieldsBound>
read: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => AuthorizerDynamicFieldsBound | Promise<AuthorizerDynamicFieldsBound>
},
ownedVisibility: (
args: {
prisma: PrismaPossibleTransaction<false>,
implementationParams: z.infer<ImplementationParamsSchema>
}
args: Omit<ArgsAuthGetterAndOwnershipCheck<false, z.ZodTypeAny, z.ZodTypeAny, ImplementationParamsSchema>, 'params' | 'data'>
) => Promise<OwnedVisibility>
}) {
const ownershipCheckVisibility = async (
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"ignoreDeprecations": "6.0",
"lib": [
"dom",
"dom.iterable",
Expand Down Expand Up @@ -93,7 +94,8 @@
"@/*": [
"./src/*"
]
}
},
"noUncheckedSideEffectImports": false
},
"include": [
"next-env.d.ts",
Expand Down
Loading