From e22c9835b12c9b6f09cef08e667262a2869c8d2c Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:17:01 +0100 Subject: [PATCH 1/9] refactor: remove getUser and refactor some services like omegaquotes --- src/app/(auth)/register-email/page.tsx | 9 +- src/app/(auth)/register/page.tsx | 14 +- src/app/(auth)/verify-email/page.tsx | 7 +- src/app/admin/mail/[filter]/[id]/page.tsx | 2 +- src/app/admin/mail/[filter]/page.tsx | 2 +- src/app/admin/mail/page.tsx | 10 +- src/app/admin/send-mail/mailForm.tsx | 4 +- .../(user-admin)/getProfileForAdmin.ts | 2 +- src/auth/auther/AuthResult.ts | 17 ++- src/auth/session/getUser.ts | 133 ------------------ src/services/notifications/actions.ts | 3 +- src/services/notifications/auth.ts | 3 +- src/services/notifications/channel/auth.ts | 3 +- .../notifications/channel/operations.ts | 5 +- src/services/notifications/email/dispatch.tsx | 4 +- src/services/notifications/email/schemas.ts | 17 +++ .../notifications/email/validation.ts | 29 ---- src/services/notifications/operations.ts | 8 ++ src/services/omegaquotes/actions.ts | 44 +----- src/services/omegaquotes/auth.ts | 8 ++ .../{CofigVars.ts => constants.ts} | 0 src/services/omegaquotes/create.ts | 43 ------ src/services/omegaquotes/operations.ts | 60 ++++++++ src/services/omegaquotes/read.ts | 19 --- src/services/omegaquotes/schemas.ts | 14 ++ src/services/omegaquotes/validation.ts | 20 --- src/services/screens/pages/actions.ts | 29 +--- src/services/screens/pages/auth.ts | 10 ++ src/services/sendmail/actions.ts | 23 --- 29 files changed, 163 insertions(+), 379 deletions(-) delete mode 100644 src/auth/session/getUser.ts create mode 100644 src/services/notifications/email/schemas.ts delete mode 100644 src/services/notifications/email/validation.ts create mode 100644 src/services/omegaquotes/auth.ts rename src/services/omegaquotes/{CofigVars.ts => constants.ts} (100%) delete mode 100644 src/services/omegaquotes/create.ts create mode 100644 src/services/omegaquotes/operations.ts delete mode 100644 src/services/omegaquotes/read.ts create mode 100644 src/services/omegaquotes/schemas.ts delete mode 100644 src/services/omegaquotes/validation.ts create mode 100644 src/services/screens/pages/auth.ts delete mode 100644 src/services/sendmail/actions.ts diff --git a/src/app/(auth)/register-email/page.tsx b/src/app/(auth)/register-email/page.tsx index 79334b980..69e17f192 100644 --- a/src/app/(auth)/register-email/page.tsx +++ b/src/app/(auth)/register-email/page.tsx @@ -1,16 +1,15 @@ +import { RequireUser } from '@/auth/auther/RequireUser' import EmailRegistrationForm from './EmailregistrationForm' -import { getUser } from '@/auth/session/getUser' import { readUserAction } from '@/services/users/actions' import { notFound, redirect } from 'next/navigation' +import { Session } from '@/auth/session/Session' export default async function Registeremail() { - const { authorized, user } = await getUser({ - userRequired: true, - }) + const { authorized, session } = RequireUser.staticFields({}).dynamicFields({}).auth(await Session.fromNextAuth()) if (!authorized) notFound() - const updatedUser = await readUserAction({ params: { id: user.id } }) + const updatedUser = await readUserAction({ params: { id: session.user.id } }) if (!updatedUser.success) { return notFound() diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 2d198a5c6..e62430aee 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,35 +1,27 @@ import RegistrationForm from './RegistrationForm' -import { getUser } from '@/auth/session/getUser' import { QueryParams } from '@/lib/queryParams/queryParams' import { unwrapActionReturn } from '@/app/redirectToErrorPage' import { readUserAction } from '@/services/users/actions' +import { Session } from '@/auth/session/Session' import { notFound, redirect } from 'next/navigation' import type { SearchParamsServerSide } from '@/lib/queryParams/types' type PropTypes = SearchParamsServerSide export default async function Register({ searchParams }: PropTypes) { - const { user, authorized } = await getUser({ - userRequired: false, - shouldRedirect: false, - }) - + const user = (await Session.fromNextAuth()).user const callbackUrl = QueryParams.callbackUrl.decode(await searchParams) - - if (!authorized || !user) { + if (!user) { return notFound() } - const updatedUser = unwrapActionReturn(await readUserAction({ params: { id: user.id } })) - if (updatedUser.acceptedTerms) { redirect(callbackUrl ?? '/users/me') } - if (!updatedUser.emailVerified) { const linkEnding = callbackUrl ? `?callbackUrl=${callbackUrl}` : '' redirect(`/register-email${linkEnding}`) diff --git a/src/app/(auth)/verify-email/page.tsx b/src/app/(auth)/verify-email/page.tsx index 851eb7d56..d0334f0a6 100644 --- a/src/app/(auth)/verify-email/page.tsx +++ b/src/app/(auth)/verify-email/page.tsx @@ -1,11 +1,11 @@ import { EmailVerifiedWrapper } from './EmailVerifiedWrapper' -import { getUser } from '@/auth/session/getUser' import { QueryParams } from '@/lib/queryParams/queryParams' import { unwrapActionReturn } from '@/app/redirectToErrorPage' import { verifyEmailAction } from '@/services/auth/actions' import { notFound, redirect } from 'next/navigation' import Link from 'next/link' import type { SearchParamsServerSide } from '@/lib/queryParams/types' +import { Session } from '@/auth/session/Session' type PropTypes = SearchParamsServerSide @@ -15,10 +15,7 @@ export default async function Register({ searchParams }: PropTypes) { notFound() } - const { user } = await getUser({ - userRequired: false, - shouldRedirect: false, - }) + const user = (await Session.fromNextAuth()).user const userId = user?.id const updatedUser = unwrapActionReturn(await verifyEmailAction({ params: { token } })) diff --git a/src/app/admin/mail/[filter]/[id]/page.tsx b/src/app/admin/mail/[filter]/[id]/page.tsx index 1b864d80b..a1b4c284d 100644 --- a/src/app/admin/mail/[filter]/[id]/page.tsx +++ b/src/app/admin/mail/[filter]/[id]/page.tsx @@ -1,4 +1,4 @@ -'use server' +'use server' //why is this use server??? import MailFlow from './MailFlow' import styles from './page.module.scss' diff --git a/src/app/admin/mail/[filter]/page.tsx b/src/app/admin/mail/[filter]/page.tsx index d56d2cb4f..586086216 100644 --- a/src/app/admin/mail/[filter]/page.tsx +++ b/src/app/admin/mail/[filter]/page.tsx @@ -1,4 +1,4 @@ -'use server' +'use server' //why is this use server??? import { RedirectType, redirect } from 'next/navigation' diff --git a/src/app/admin/mail/page.tsx b/src/app/admin/mail/page.tsx index 6c273c744..8d318e86d 100644 --- a/src/app/admin/mail/page.tsx +++ b/src/app/admin/mail/page.tsx @@ -1,21 +1,17 @@ -'use server' - +'use server' //why is this use server??? import styles from './page.module.scss' import CreateMailAlias from './createMailAliasForm' import CreateMailingList from './createMailingListForm' import CreateMailaddressExternal from './createMailaddressExternalForm' import MailListView from './mailListView' -import { getUser } from '@/auth/session/getUser' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readMailAliases } from '@/services/mail/alias/read' import { readMailingLists } from '@/services/mail/list/read' import { readMailAddressExternal } from '@/services/mail/mailAddressExternal/read' +import { Session } from '@/auth/session/Session' export default async function MailSettings() { - const { permissions } = await getUser({ - userRequired: true, - shouldRedirect: true, - }) + const { permissions } = await Session.fromNextAuth() const createMailAlias = permissions.includes('MAILALIAS_CREATE') const createMailingList = permissions.includes('MAILINGLIST_CREATE') diff --git a/src/app/admin/send-mail/mailForm.tsx b/src/app/admin/send-mail/mailForm.tsx index 39fb94e99..f53c273d4 100644 --- a/src/app/admin/send-mail/mailForm.tsx +++ b/src/app/admin/send-mail/mailForm.tsx @@ -1,6 +1,6 @@ 'use client' import styles from './mailForm.module.scss' -import sendMail from '@/services/sendmail/actions' +import { sendMailAction } from '@/services/notifications/actions' import Form from '@/components/Form/Form' import TextInput from '@/components/UI/TextInput' import Textarea from '@/components/UI/Textarea' @@ -8,7 +8,7 @@ import Textarea from '@/components/UI/Textarea' export default function MailForm() { return
{ + public redirectOnUnauthorized({ returnUrl }: { returnUrl?: string }) : AuthResult { if (!this.authorized) { - if (this.session.user) redirectToErrorPage('UNAUTHORIZED') - redirect(`/login?callbackUrl=${encodeURI(returnUrlIfFail)}`) + if (this.session.user) { + if (!this.session.user.acceptedTerms) { + if (returnUrl) { + redirect(`/register?callbackUrl=${encodeURI(returnUrl)}`) + } + redirect('/register') + } + redirectToErrorPage('UNAUTHORIZED', this.errorMessage) + } + if (returnUrl) { + redirect(`/login?callbackUrl=${encodeURI(returnUrl)}`) + } + redirect('/login') } return new AuthResult(this.session, true) } diff --git a/src/auth/session/getUser.ts b/src/auth/session/getUser.ts deleted file mode 100644 index aea1d1955..000000000 --- a/src/auth/session/getUser.ts +++ /dev/null @@ -1,133 +0,0 @@ -import '@pn-server-only' -import { authOptions } from '@/auth/nextAuth/authOptions' -import checkMatrix from '@/lib/checkMatrix' -import { permissionOperations } from '@/services/permissions/operations' -import { getServerSession } from 'next-auth' -import { notFound, redirect } from 'next/navigation' -import type { Matrix } from '@/lib/checkMatrix' -import type { Permission } from '@prisma/client' -import type { MembershipFiltered } from '@/services/groups/memberships/types' -import type { UserFiltered } from '@/services/users/types' - -type GetUserArgsType = { - requiredPermissions?: Matrix, - userRequired?: UserRequired, - shouldRedirect?: ShouldRedirect, - redirectUrl?: string, - returnUrl?: string, -} - -type AuthorizedGetUserReturnType = ({ - user: UserFiltered, - status: 'AUTHORIZED', -} | ( - UserRequired extends true ? never : { - user: null, - status: 'AUTHORIZED_NO_USER', - } -)) & { - authorized: true, - permissions: Permission[], - memberships: MembershipFiltered[], -} - -type UnAuthorizedGetUserReturnType = ({ - user: null, - status: 'UNAUTHENTICATED', -} | { - user: UserFiltered, - status: 'UNAUTHORIZED', -}) & { - authorized: false, - permissions: Permission[], - memberships: MembershipFiltered[], -} - -export type GetUserReturnType = ( - AuthorizedGetUserReturnType -) | ( - UnAuthorizedGetUserReturnType -) - - -export type AuthStatus = GetUserReturnType['status'] - -/** - * Returns the user object from the current session. If there is no session or the - * user does not have adequate permissions `null` is returned. Except if `required` - * is true, then the user gets redirected. - * - * This function is for server side components and actions. For client side - * components use `useUser`. - * - * @param requiredPermissions - A list of lists that the user must have - * [[A, B], [C, D]] means the user must have (either A or B) and (either C or D). - * If non are given, the user is considered authorized. - * - * @param userRequired - Wheter or not to a user session is required. - * - * @param redirect - Wheter or not to redirect the user if there is no session, by default false. - * @param redirectUrl - The url to redirect the user to, by default to 404 page. - * @param returnUrl - If set, the user is redirected to the login page and then back to the given url. - * - * @returns The user object and auth status - * (either `AUTHORIZED`, `AUTHORIZED_NO_USER`, `UNAUTHENTICATED`, or `UNAUTHORIZED`). - * - * @deprecated - Deprecated as the new service mehtod system handles this. - * For getting the user in the app router a utility function will be developed. - */ -// This function is overloaded to get correct typing for when required is set to true or false. -export async function getUser( - args?: GetUserArgsType -): Promise> -export async function getUser( - args?: GetUserArgsType -): Promise> -export async function getUser({ - requiredPermissions = [], - userRequired = false, - shouldRedirect = false, - redirectUrl = undefined, - returnUrl = undefined, -}: GetUserArgsType = {}): Promise> { - const { - user = null, - permissions = await permissionOperations.readDefaultPermissions({ - bypassAuth: true, - }), - memberships = [], - } = await getServerSession(authOptions) ?? {} - - if (shouldRedirect && user && !user.acceptedTerms) { - if (returnUrl) { - redirect(`/register?callbackUrl=${returnUrl}`) - } - redirect('/register') - } - - if ((user || !userRequired) && checkMatrix(permissions, requiredPermissions)) { - // Cannot have ternary expression for just status because then ts gets confused. - return user - ? { user, authorized: true, status: 'AUTHORIZED', permissions, memberships } - : { user, authorized: true, status: 'AUTHORIZED_NO_USER', permissions, memberships } - } - - //TODO: visibility checks - - if (shouldRedirect) { - if (!user && returnUrl) { - redirect(`/login?callbackUrl=${encodeURI(returnUrl)}`) - } - - if (redirectUrl) { - redirect(redirectUrl) - } - - notFound() //TODO: Should probably redirect to an unauthorized page when we have one. - } - - // Cannot have ternary expression for just status because then ts gets confused. - return user - ? { user, authorized: false, status: 'UNAUTHORIZED', permissions, memberships } - : { user, authorized: false, status: 'UNAUTHENTICATED', permissions, memberships } -} diff --git a/src/services/notifications/actions.ts b/src/services/notifications/actions.ts index f1362690d..1113558bd 100644 --- a/src/services/notifications/actions.ts +++ b/src/services/notifications/actions.ts @@ -1,5 +1,4 @@ 'use server' - import { makeAction } from '@/services/serverAction' import { notificationChannelOperations } from '@/services/notifications/channel/operations' import { notificationOperations } from '@/services/notifications/operations' @@ -13,3 +12,5 @@ export const createNotificationAction = makeAction(notificationOperations.create export const readNotificationSubscriptionsAction = makeAction(notificationSubscriptionOperations.read) export const updateNotificationSubscriptionsAction = makeAction(notificationSubscriptionOperations.update) + +export const sendMailAction = makeAction(notificationOperations.sendMail) diff --git a/src/services/notifications/auth.ts b/src/services/notifications/auth.ts index 641137b1e..330166bce 100644 --- a/src/services/notifications/auth.ts +++ b/src/services/notifications/auth.ts @@ -1,7 +1,6 @@ import { RequirePermission } from '@/auth/auther/RequirePermission' -import '@pn-server-only' export const notificationAuth = { create: RequirePermission.staticFields({ permission: 'NOTIFICATION_CREATE' }), sendMail: RequirePermission.staticFields({ permission: 'MAIL_SEND' }), -} +} as const diff --git a/src/services/notifications/channel/auth.ts b/src/services/notifications/channel/auth.ts index 821826a3f..f215095d6 100644 --- a/src/services/notifications/channel/auth.ts +++ b/src/services/notifications/channel/auth.ts @@ -3,7 +3,8 @@ import '@pn-server-only' export const notificationChannelAuth = { create: RequirePermission.staticFields({ permission: 'NOTIFICATION_CHANNEL_CREATE' }), - read: RequirePermission.staticFields({ permission: 'NOTIFICATION_CHANNEL_READ' }), + readMany: RequirePermission.staticFields({ permission: 'NOTIFICATION_CHANNEL_READ' }), + readDefault: RequirePermission.staticFields({ permission: 'NOTIFICATION_CHANNEL_READ' }), update: RequirePermission.staticFields({ permission: 'NOTIFICATION_CHANNEL_UPDATE' }), destroy: RequirePermission.staticFields({ permission: 'NOTIFICATION_CHANNEL_UPDATE' }), } diff --git a/src/services/notifications/channel/operations.ts b/src/services/notifications/channel/operations.ts index 8fb04b5c8..36d5b1fde 100644 --- a/src/services/notifications/channel/operations.ts +++ b/src/services/notifications/channel/operations.ts @@ -105,14 +105,14 @@ export const notificationChannelOperations = { }), readMany: defineOperation({ - authorizer: () => notificationChannelAuth.read.dynamicFields({}), + authorizer: () => notificationChannelAuth.readMany.dynamicFields({}), operation: async ({ prisma }) => await prisma.notificationChannel.findMany({ include: availableNotificationMethodIncluder, }) }), readDefault: defineOperation({ - authorizer: () => notificationChannelAuth.read.dynamicFields({}), + authorizer: () => notificationChannelAuth.readDefault.dynamicFields({}), operation: async ({ prisma }) => await prisma.notificationChannel.findMany({ where: { defaultMethods: { @@ -217,6 +217,7 @@ export const notificationChannelOperations = { } }), + // TODO: It should probably be possible to delete a channel from the frontend (if not default) - Johan // It doesn't seem that this function is used yet. -Theodor destroy: defineOperation({ authorizer: () => notificationChannelAuth.destroy.dynamicFields({}), diff --git a/src/services/notifications/email/dispatch.tsx b/src/services/notifications/email/dispatch.tsx index 2819fc6ba..edc6625e0 100644 --- a/src/services/notifications/email/dispatch.tsx +++ b/src/services/notifications/email/dispatch.tsx @@ -1,6 +1,6 @@ import { sendBulkMail } from './send' import { DEFAULT_NOTIFICATION_ALIAS } from './constants' -import { sendEmailValidation } from './validation' +import { emailSchemas } from './schemas' import { DefaultEmailTemplate } from './templates/default' import { repalceSpecialSymbols } from '@/services/notifications/operations' import { prismaCall } from '@/services/prismaCall' @@ -36,7 +36,7 @@ export async function dispatchEmailNotifications( const senderAlias = results.mailAlias ? results.mailAlias.address : DEFAULT_NOTIFICATION_ALIAS const mails = await Promise.all(users.map(async user => { - const parsed = sendEmailValidation.detailedValidate({ + const parsed = emailSchemas.sendMail.parse({ from: senderAlias, to: user.email, subject: repalceSpecialSymbols(notificaion.title, user), diff --git a/src/services/notifications/email/schemas.ts b/src/services/notifications/email/schemas.ts new file mode 100644 index 000000000..fbfe8dd68 --- /dev/null +++ b/src/services/notifications/email/schemas.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' + +const baseSchema = z.object({ + from: z.string().email('Ikke en gyldig e-post'), + to: z.string().email('Ikke en gyldig e-post'), + subject: z.string().min(2, 'Minimum 2 tegn').max(100, 'Maksimum 100 tegn'), + text: z.string().min(2, 'Minimum 2 tegn'), +}) + +export const emailSchemas = { + sendMail: baseSchema.pick({ + from: true, + to: true, + subject: true, + text: true, + }) +} as const diff --git a/src/services/notifications/email/validation.ts b/src/services/notifications/email/validation.ts deleted file mode 100644 index f2e389a79..000000000 --- a/src/services/notifications/email/validation.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ValidationBase } from '@/services/Validation' -import { z } from 'zod' -import type { ValidationTypes } from '@/services/Validation' - -const baseEmailValidation = new ValidationBase({ - type: { - from: z.string(), - to: z.string(), - subject: z.string(), - text: z.string(), - }, - details: { - from: z.string().email('Ikke en gyldig e-post'), - to: z.string().email('Ikke en gyldig e-post'), - subject: z.string().min(2, 'Minimum 2 tegn').max(100, 'Maksimum 100 tegn'), - text: z.string().min(2, 'Minimum 2 tegn'), - } -}) - -export const sendEmailValidation = baseEmailValidation.createValidation({ - keys: [ - 'from', - 'to', - 'subject', - 'text', - ], - transformer: data => data, -}) -export type SendEmailValidation = ValidationTypes diff --git a/src/services/notifications/operations.ts b/src/services/notifications/operations.ts index 7992efcb1..807286f43 100644 --- a/src/services/notifications/operations.ts +++ b/src/services/notifications/operations.ts @@ -12,6 +12,8 @@ import { SpecialNotificationChannel } from '@prisma/client' import type { Notification } from '@prisma/client' import type { ExpandedNotificationChannel, NotificationResult } from './types' import type { UserFiltered } from '@/services/users/types' +import { sendMail } from './email/send' +import { emailSchemas } from './email/schemas' const dispathMethod = { email: dispatchEmailNotifications, @@ -109,6 +111,12 @@ export const notificationOperations = { } }), + sendMail: defineOperation({ + authorizer: () => notificationAuth.sendMail.dynamicFields({}), + dataSchema: emailSchemas.sendMail, + operation: ({ data }) => sendMail(data) + }), + /** * Createses a notification to a special notification channel. * diff --git a/src/services/omegaquotes/actions.ts b/src/services/omegaquotes/actions.ts index e4e486bee..a0e9311e3 100644 --- a/src/services/omegaquotes/actions.ts +++ b/src/services/omegaquotes/actions.ts @@ -1,42 +1,6 @@ 'use server' +import { omegaquoteOperations } from './operations' +import { makeAction } from '@/services/serverAction' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' -import { createQuote } from '@/services/omegaquotes/create' -import { readQuotesPage } from '@/services/omegaquotes/read' -import { createOmegaquotesValidation } from '@/services/omegaquotes/validation' -import type { OmegaquoteCursor, OmegaquoteFiltered } from '@/services/omegaquotes/types' -import type { ReadPageInput } from '@/lib/paging/types' -import type { ActionReturn } from '@/services/actionTypes' -import type { CreateOmegaguotesTypes } from '@/services/omegaquotes/validation' -import type { OmegaQuote } from '@prisma/client' - -export async function createQuoteAction( - rawdata: FormData | CreateOmegaguotesTypes['Type'] -): Promise> { - const { user, status, authorized } = await getUser({ - requiredPermissions: [['OMEGAQUOTES_WRITE']], - userRequired: true, - }) - if (!authorized) return createActionError(status) - - const parse = createOmegaquotesValidation.typeValidate(rawdata) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - const results = await safeServerCall(() => createQuote(user.id, data)) - - return results -} - -export async function readQuotesPageAction( - readPageInput: ReadPageInput -): Promise> { - //TODO: REFACTOR when new permission system is working - const { status, authorized } = await getUser({ - requiredPermissions: [['OMEGAQUOTES_READ']] - }) - if (!authorized) return createActionError(status) - - return await safeServerCall(() => readQuotesPage(readPageInput)) -} +export const createQuoteAction = makeAction(omegaquoteOperations.create) +export const readQuotesPageAction = makeAction(omegaquoteOperations.readPage) diff --git a/src/services/omegaquotes/auth.ts b/src/services/omegaquotes/auth.ts new file mode 100644 index 000000000..4e82153a2 --- /dev/null +++ b/src/services/omegaquotes/auth.ts @@ -0,0 +1,8 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' +import { RequirePermissionAndUserId } from '@/auth/auther/RequirePermissionAndUserId' + + +export const omegaQuotesAuth = { + create: RequirePermissionAndUserId.staticFields({ permission: 'OMEGAQUOTES_WRITE' }), + readPage: RequirePermission.staticFields({ permission: 'OMEGAQUOTES_READ' }) +} as const diff --git a/src/services/omegaquotes/CofigVars.ts b/src/services/omegaquotes/constants.ts similarity index 100% rename from src/services/omegaquotes/CofigVars.ts rename to src/services/omegaquotes/constants.ts diff --git a/src/services/omegaquotes/create.ts b/src/services/omegaquotes/create.ts deleted file mode 100644 index ae9714d69..000000000 --- a/src/services/omegaquotes/create.ts +++ /dev/null @@ -1,43 +0,0 @@ -import '@pn-server-only' -import { createOmegaquotesValidation } from './validation' -import { prismaCall } from '@/services/prismaCall' -import { prisma } from '@/prisma/client' -import { notificationOperations } from '@/services/notifications/operations' -import type { CreateOmegaguotesTypes } from './validation' -import type { OmegaQuote } from '@prisma/client' - -/** - * A function to create a quote - * @param userId - The user id of the user creating the quote - * @param data - The data of the quote to be created - * @returns - */ -export async function createQuote( - userId: number, - rawdata: CreateOmegaguotesTypes['Detailed'] -): Promise { - const data = createOmegaquotesValidation.detailedValidate(rawdata) - const results = await prismaCall(() => prisma.omegaQuote.create({ - data: { - ...data, - userPoster: { - connect: { - id: userId - } - } - } - })) - - notificationOperations.createSpecial({ - params: { - special: 'NEW_OMEGAQUOTE', - }, - data: { - title: 'Ny Omegaquote♪', - message: `${results.quote}\n - ${results.author}`, - }, - bypassAuth: true, - }) - - return results -} diff --git a/src/services/omegaquotes/operations.ts b/src/services/omegaquotes/operations.ts new file mode 100644 index 000000000..8cb9fb9a9 --- /dev/null +++ b/src/services/omegaquotes/operations.ts @@ -0,0 +1,60 @@ +import { defineOperation } from '@/services/serviceOperation' +import { omegaQuotesAuth } from './auth' +import { omegaquoteSchemas } from './schemas' +import { notificationOperations } from '@/services/notifications/operations' +import { z } from 'zod' +import { readPageInputSchemaObject } from '@/lib/paging/schema' +import { omegaQuoteFilterSelection } from './constants' +import { cursorPageingSelection } from '@/lib/paging/cursorPageingSelection' + +export const omegaquoteOperations = { + create: defineOperation({ + authorizer: ({ params }) => omegaQuotesAuth.create.dynamicFields({ userId: params.userPosterId }), + dataSchema: omegaquoteSchemas.create, + paramsSchema: z.object({ + userPosterId: z.number() + }), + operation: async ({ prisma, data, params }) => { + const results = await prisma.omegaQuote.create({ + data: { + ...data, + userPoster: { + connect: { + id: params.userPosterId + } + } + } + }) + + notificationOperations.createSpecial({ + params: { + special: 'NEW_OMEGAQUOTE', + }, + data: { + title: 'Ny Omegaquote♪', + message: `${results.quote}\n - ${results.author}`, + }, + bypassAuth: true, + }) + return results + } + }), + readPage: defineOperation({ + paramsSchema: readPageInputSchemaObject( + z.number(), + z.object({ + id: z.number(), + }), + z.undefined() + ), + authorizer: () => omegaQuotesAuth.readPage.dynamicFields({}), + operation: async ({ prisma, params }) => + prisma.omegaQuote.findMany({ + orderBy: { + timestamp: 'desc', + }, + ...cursorPageingSelection(params.paging.page), + select: omegaQuoteFilterSelection, + }) + }), +} as const diff --git a/src/services/omegaquotes/read.ts b/src/services/omegaquotes/read.ts deleted file mode 100644 index b0d477632..000000000 --- a/src/services/omegaquotes/read.ts +++ /dev/null @@ -1,19 +0,0 @@ -import '@pn-server-only' -import { omegaQuoteFilterSelection } from './CofigVars' -import { cursorPageingSelection } from '@/lib/paging/cursorPageingSelection' -import { prismaCall } from '@/services/prismaCall' -import { prisma } from '@/prisma/client' -import type { ReadPageInput } from '@/lib/paging/types' -import type { OmegaquoteCursor, OmegaquoteFiltered } from '@/services/omegaquotes/types' - -export async function readQuotesPage( - { page }: ReadPageInput -): Promise { - return await prismaCall(() => prisma.omegaQuote.findMany({ - orderBy: { - timestamp: 'desc', - }, - ...cursorPageingSelection(page), - select: omegaQuoteFilterSelection, - })) -} diff --git a/src/services/omegaquotes/schemas.ts b/src/services/omegaquotes/schemas.ts new file mode 100644 index 000000000..c4649917b --- /dev/null +++ b/src/services/omegaquotes/schemas.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' + +export const baseSchemas = z.object({ + quote: z.string().min(1, 'Sitatet kan ikke være tomt'), + author: z.string().min(1, 'Noen må siteres'), +}) + +export const omegaquoteSchemas = { + create: baseSchemas.pick({ + quote: true, + author: true, + }) +} + diff --git a/src/services/omegaquotes/validation.ts b/src/services/omegaquotes/validation.ts deleted file mode 100644 index 4f8ad1a11..000000000 --- a/src/services/omegaquotes/validation.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ValidationBase } from '@/services/Validation' -import { z } from 'zod' -import type { ValidationTypes } from '@/services/Validation' - -export const baseOmegaquotesValidation = new ValidationBase({ - type: { - quote: z.string(), - author: z.string(), - }, - details: { - quote: z.string().min(1, 'Sitatet kan ikke være tomt'), - author: z.string().min(1, 'Noen må siteres'), - } -}) - -export const createOmegaquotesValidation = baseOmegaquotesValidation.createValidation({ - keys: ['quote', 'author'], - transformer: data => data -}) -export type CreateOmegaguotesTypes = ValidationTypes diff --git a/src/services/screens/pages/actions.ts b/src/services/screens/pages/actions.ts index fadec1980..36dd9e175 100644 --- a/src/services/screens/pages/actions.ts +++ b/src/services/screens/pages/actions.ts @@ -1,7 +1,5 @@ 'use server' - -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createPage } from '@/services/screens/pages/create' import { destroyPage } from '@/services/screens/pages/destroy' import { readPage, readPages } from '@/services/screens/pages/read' @@ -15,11 +13,6 @@ import type { CreateScreenTypes } from '@/services/screens/validation' import type { ScreenPage } from '@prisma/client' export async function createPageAction(formdata: CreateScreenTypes['Type']): Promise> { - const { status, authorized } = await getUser({ - requiredPermissions: [['SCREEN_ADMIN']] - }) - if (!authorized) return createActionError(status) - const parse = createScreenValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) const data = parse.data @@ -28,38 +21,18 @@ export async function createPageAction(formdata: CreateScreenTypes['Type']): Pro } export async function destroyPageAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCREEN_ADMIN']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => destroyPage(id)) } export async function readPageAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCREEN_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readPage(id)) } export async function readPagesAction(): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCREEN_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readPages()) } export async function updatePageAction(id: number, formdata: UpdatePageTypes['Type']): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCREEN_ADMIN']] - }) - if (!authorized) return createActionError(status) - const parse = updatePageValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) const data = parse.data diff --git a/src/services/screens/pages/auth.ts b/src/services/screens/pages/auth.ts new file mode 100644 index 000000000..b4192ed61 --- /dev/null +++ b/src/services/screens/pages/auth.ts @@ -0,0 +1,10 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +export const screenPageAuth = { + //TODO: Service not refactored to serviceoperations.... use these authers then. + create: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), + destroy: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), + read: RequirePermission.staticFields({ permission: 'SCREEN_READ' }), + readAll: RequirePermission.staticFields({ permission: 'SCREEN_READ' }), + update: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), +} as const diff --git a/src/services/sendmail/actions.ts b/src/services/sendmail/actions.ts deleted file mode 100644 index 27bc12211..000000000 --- a/src/services/sendmail/actions.ts +++ /dev/null @@ -1,23 +0,0 @@ -'use server' - -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' -import { sendMail as transportSendMail } from '@/services/notifications/email/send' -import { sendEmailValidation } from '@/services/notifications/email/validation' -import type { ActionReturn } from '@/services/actionTypes' - -export default async function sendMail(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAIL_SEND']], - }) - - if (!authorized) { - return createActionError(status) - } - - const parse = sendEmailValidation.typeValidate(rawdata) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - return await safeServerCall(() => transportSendMail(data)) -} From 80264f01b1716ee5a00f20e6ae1488ee026ec188 Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:46:04 +0100 Subject: [PATCH 2/9] refactor: ombul service --- src/services/ombul/actions.ts | 92 +------------- src/services/ombul/auth.ts | 6 +- src/services/ombul/create.ts | 87 ------------- src/services/ombul/destroy.ts | 37 ------ src/services/ombul/operations.ts | 190 ++++++++++++++++++++++++++++- src/services/ombul/schemas.ts | 38 ++++++ src/services/ombul/update.ts | 80 ------------ src/services/ombul/validation.ts | 75 ------------ src/services/omegaOrder/actions.ts | 14 +-- src/services/omegaOrder/auth.ts | 8 ++ src/services/omegaid/actions.ts | 13 +- 11 files changed, 250 insertions(+), 390 deletions(-) delete mode 100644 src/services/ombul/create.ts delete mode 100644 src/services/ombul/destroy.ts create mode 100644 src/services/ombul/schemas.ts delete mode 100644 src/services/ombul/update.ts delete mode 100644 src/services/ombul/validation.ts create mode 100644 src/services/omegaOrder/auth.ts diff --git a/src/services/ombul/actions.ts b/src/services/ombul/actions.ts index 8761cdedf..9794212a1 100644 --- a/src/services/ombul/actions.ts +++ b/src/services/ombul/actions.ts @@ -1,96 +1,12 @@ 'use server' - import { ombulOperations } from './operations' import { makeAction } from '@/services/serverAction' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' -import { createOmbul } from '@/services/ombul/create' -import { destroyOmbul } from '@/services/ombul/destroy' -import { updateOmbul, updateOmbulFile } from '@/services/ombul/update' -import { createOmbulValidation, updateOmbulFileValidation, updateOmbulValidation } from '@/services/ombul/validation' -import type { ExpandedOmbul } from '@/services/ombul/types' -import type { ActionReturn } from '@/services/actionTypes' -import type { CreateOmbulTypes, UpdateOmbulFileTypes, UpdateOmbulTypes } from '@/services/ombul/validation' -import type { Ombul } from '@prisma/client' - -/** - * Create a new Ombul. - * @param rawData includes a pdf file with the ombul issue optionaly year and issueNumber - * @param CoverImageId is the id of the Image that will be used as the cover of the ombul - */ -export async function createOmbulAction(rawdata: FormData | CreateOmbulTypes['Type']): Promise> { - //Auth route - const { status, authorized } = await getUser({ - requiredPermissions: [['OMBUL_CREATE']] - }) - if (!authorized) return createActionError(status) - - const parse = createOmbulValidation.typeValidate(rawdata) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - return await safeServerCall(() => createOmbul(data)) -} - -export async function destroyOmbulAction(id: number): Promise> { - const { status, authorized } = await getUser({ - requiredPermissions: [['OMBUL_DESTROY']] - }) - - if (!authorized) return createActionError(status) - - return await safeServerCall(() => destroyOmbul(id)) -} +export const createOmbulAction = makeAction(ombulOperations.create) +export const destroyOmbulAction = makeAction(ombulOperations.destroy) export const readOmbulAction = makeAction(ombulOperations.read) export const readLatestOmbulAction = makeAction(ombulOperations.readLatest) export const readOmbulsAction = makeAction(ombulOperations.readAll) - export const updateOmbulCmsCoverImageAction = makeAction(ombulOperations.updateCmsCoverImage) - -/** - * A action to update an ombul - * @param id - The id of the ombul to update - * @param rawdata - The new data for the ombul including: name, year, issueNumber, description, - * @returns The updated ombul - */ -export async function updateOmbulAction( - id: number, - rawdata: FormData | UpdateOmbulTypes['Type'] -): Promise> { - // Auth route - const { status, authorized } = await getUser({ - requiredPermissions: [['OMBUL_UPDATE']] - }) - if (!authorized) return createActionError(status) - - //Parse the data - const parse = updateOmbulValidation.typeValidate(rawdata) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - return await safeServerCall(() => updateOmbul(id, data)) -} - -/** - * A action that updates the ombul file (i.e. the pdf file) of an ombul - * @param id - The id of the ombul to update - * @param rawData - The new data for the new ombul file with field name 'file' - * @returns The updated ombul - */ -export async function updateOmbulFileAction( - id: number, - rawData: FormData | UpdateOmbulFileTypes['Type'] -): Promise> { - // auth route - const { status, authorized } = await getUser({ - requiredPermissions: [['OMBUL_UPDATE']] - }) - if (!authorized) return createActionError(status) - - const parse = updateOmbulFileValidation.typeValidate(rawData) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - return await safeServerCall(() => updateOmbulFile(id, data)) -} +export const updateOmbulAction = makeAction(ombulOperations.update) +export const updateOmbulFileAction = makeAction(ombulOperations.updateFile) diff --git a/src/services/ombul/auth.ts b/src/services/ombul/auth.ts index 13b0c7a4e..a08d6caaf 100644 --- a/src/services/ombul/auth.ts +++ b/src/services/ombul/auth.ts @@ -4,5 +4,9 @@ export const ombulAuth = { read: RequirePermission.staticFields({ permission: 'OMBUL_READ' }), readAll: RequirePermission.staticFields({ permission: 'OMBUL_READ' }), readLatest: RequirePermission.staticFields({ permission: 'OMBUL_READ' }), - updateCmsCoverImage: RequirePermission.staticFields({ permission: 'OMBUL_UPDATE' }) + updateCmsCoverImage: RequirePermission.staticFields({ permission: 'OMBUL_UPDATE' }), + destroy: RequirePermission.staticFields({ permission: 'OMBUL_DESTROY' }), + create: RequirePermission.staticFields({ permission: 'OMBUL_CREATE' }), + update: RequirePermission.staticFields({ permission: 'OMBUL_UPDATE' }), + updateFile: RequirePermission.staticFields({ permission: 'OMBUL_UPDATE' }) } as const diff --git a/src/services/ombul/create.ts b/src/services/ombul/create.ts deleted file mode 100644 index ee36c09a6..000000000 --- a/src/services/ombul/create.ts +++ /dev/null @@ -1,87 +0,0 @@ -import '@pn-server-only' -import { createOmbulValidation } from './validation' -import { prismaCall } from '@/services/prismaCall' -import { readSpecialImageCollection } from '@/services/images/collections/read' -import { cmsImageOperations } from '@/cms/images/operations' -import { prisma } from '@/prisma/client' -import { createFile } from '@/services/store/createFile' -import { imageOperations } from '@/services/images/operations' -import { notificationOperations } from '@/services/notifications/operations' -import type { CreateOmbulTypes } from './validation' -import type { Ombul } from '@prisma/client' - -/** - * Create a new Ombul. - */ -export async function createOmbul( - rawdata: CreateOmbulTypes['Detailed'] -): Promise { - const { ombulFile: file, ombulCoverImage: cover, ...config } = createOmbulValidation.detailedValidate(rawdata) - // Get the latest issue number if not provided - const { year, issueNumber: givenIssueNumber, ...restOfConf } = config - - let latestIssueNumber = 1 - if (!givenIssueNumber) { - const ombul = await prismaCall(() => prisma.ombul.findFirst({ - where: { - year - }, - orderBy: { - issueNumber: 'desc' - } - })) - if (ombul) { - latestIssueNumber = ombul.issueNumber + 1 - } - } - const issueNumber = givenIssueNumber || latestIssueNumber - - //upload the file to the store volume - const ret = await createFile(file, 'ombul', ['pdf']) - const fsLocation = ret.fsLocation - - // create coverimage - const ombulCoverCollection = await readSpecialImageCollection('OMBULCOVERS') - const coverImage = await imageOperations.create({ - params: { - collectionId: ombulCoverCollection.id, - }, - data: { - name: fsLocation, - alt: `cover of ${config.name}`, - file: cover, - }, - }) - - const cmsCoverImage = await cmsImageOperations.create({ - data: { imageId: coverImage.id }, - bypassAuth: true - }) - - const ombul = await prismaCall(() => prisma.ombul.create({ - data: { - ...restOfConf, - year, - issueNumber, - coverImage: { - connect: { - id: cmsCoverImage.id - } - }, - fsLocation, - } - })) - - notificationOperations.createSpecial({ - params: { - special: 'NEW_OMBUL', - }, - data: { - title: 'Ny ombul', - message: `Ny ombul er ute! ${ombul.name}`, - }, - bypassAuth: true, - }) - - return ombul -} diff --git a/src/services/ombul/destroy.ts b/src/services/ombul/destroy.ts deleted file mode 100644 index 9ea90aaa8..000000000 --- a/src/services/ombul/destroy.ts +++ /dev/null @@ -1,37 +0,0 @@ -import '@pn-server-only' -import { prismaCall } from '@/services/prismaCall' -import { ServerError } from '@/services/error' -import { prisma } from '@/prisma/client' -import { destroyFile } from '@/services/store/destroyFile' -import type { ExpandedOmbul } from '@/services/ombul/types' - -/** - * A function to destroy an ombul, also deletes the file from the store, and the cmsImage on cascade - * @param id - The id of the ombul to destroy - * @returns - */ -export async function destroyOmbul(id: number): Promise { - const ombul = await prismaCall(() => prisma.ombul.findUnique({ - where: { - id - }, - include: { - coverImage: { - include: { - image: true - } - } - } - })) - if (!ombul) throw new ServerError('NOT FOUND', 'Ombul ikke funnet.') - - await destroyFile('ombul', ombul.fsLocation) - - await prisma.ombul.delete({ - where: { - id - } - }) - - return ombul -} diff --git a/src/services/ombul/operations.ts b/src/services/ombul/operations.ts index d856b8c4d..b64c3c14b 100644 --- a/src/services/ombul/operations.ts +++ b/src/services/ombul/operations.ts @@ -1,8 +1,15 @@ import '@pn-server-only' +import { z } from 'zod' import { ombulAuth } from './auth' import { defineOperation } from '@/services/serviceOperation' import { cmsImageOperations } from '@/cms/images/operations' -import { z } from 'zod' +import { ServerError } from '@/services/error' +import { destroyFile } from '@/services/store/destroyFile' +import { ombulSchemas } from './schemas' +import { createFile } from '@/services/store/createFile' +import { readSpecialImageCollection } from '@/services/images/collections/read' +import { imageOperations } from '@/services/images/operations' +import { notificationOperations } from '@/services/notifications/operations' const read = defineOperation({ authorizer: () => ombulAuth.read.dynamicFields({}), @@ -81,9 +88,188 @@ const updateCmsCoverImage = cmsImageOperations.update.implement({ } }) +/** + * A function to destroy an ombul, also deletes the file from the store, and the cmsImage on cascade + * @param id - The id of the ombul to destroy + * @returns + */ +const destroy = defineOperation({ + authorizer: () => ombulAuth.destroy.dynamicFields({}), + paramsSchema: z.object({ + id: z.number() + }), + operation: async ({ params, prisma }) => { + const ombul = await prisma.ombul.findUnique({ + where: { + id: params.id + }, + include: { + coverImage: { + include: { + image: true + } + } + } + }) + if (!ombul) throw new ServerError('NOT FOUND', 'Ombul ikke funnet.') + + await destroyFile('ombul', ombul.fsLocation) + + await prisma.ombul.delete({ + where: { + id: params.id + } + }) + + return ombul + } +}) + +const create = defineOperation({ + authorizer: () => ombulAuth.create.dynamicFields({}), + dataSchema: ombulSchemas.create, + operation: async ({ data, prisma }) => { + // Get the latest issue number if not provided + const { ombulCoverImage, ombulFile, year, issueNumber: givenIssueNumber, ...restOfConf } = data + + let latestIssueNumber = 1 + if (!givenIssueNumber) { + const ombul = await prisma.ombul.findFirst({ + where: { + year + }, + orderBy: { + issueNumber: 'desc' + } + }) + if (ombul) { + latestIssueNumber = ombul.issueNumber + 1 + } + } + const issueNumber = givenIssueNumber || latestIssueNumber + + //upload the file to the store volume + const ret = await createFile(ombulFile, 'ombul', ['pdf']) + const fsLocation = ret.fsLocation + + // create coverimage + const ombulCoverCollection = await readSpecialImageCollection('OMBULCOVERS') + const coverImage = await imageOperations.create({ + params: { + collectionId: ombulCoverCollection.id, + }, + data: { + name: fsLocation, + alt: `cover of ${restOfConf.name}`, + file: ombulCoverImage, + }, + }) + + const cmsCoverImage = await cmsImageOperations.create({ + data: { imageId: coverImage.id }, + bypassAuth: true + }) + + const ombul = await prisma.ombul.create({ + data: { + ...restOfConf, + year, + issueNumber, + coverImage: { + connect: { + id: cmsCoverImage.id + } + }, + fsLocation, + } + }) + + notificationOperations.createSpecial({ + params: { + special: 'NEW_OMBUL', + }, + data: { + title: 'Ny ombul', + message: `Ny ombul er ute! ${ombul.name}`, + }, + bypassAuth: true, + }) + + return ombul + } +}) + +const update = defineOperation({ + authorizer: () => ombulAuth.update.dynamicFields({}), + dataSchema: ombulSchemas.update, + paramsSchema: z.object({ + id: z.number() + }), + operation: ({ data, prisma, params }) => + prisma.ombul.update({ + where: { + id: params.id + }, + data, + include: { + coverImage: { + include: { + image: true + } + } + } + }) +}) + +const updateFile = defineOperation({ + authorizer: () => ombulAuth.updateFile.dynamicFields({}), + dataSchema: ombulSchemas.updateFile, + paramsSchema: z.object({ + id: z.number() + }), + operation: async ({ data, prisma, params }) => { + const ret = await createFile(data.ombulFile, 'ombul', ['pdf']) + const fsLocation = ret.fsLocation + + const ombul = await prisma.ombul.findUnique({ + where: { + id: params.id + } + }) + if (!ombul) throw new ServerError('NOT FOUND', 'Ombul ikke funnet') + + const oldFsLocation = ombul.fsLocation + + const ombulUpdated = await prisma.ombul.update({ + where: { + id: params.id + }, + data: { + fsLocation + }, + include: { + coverImage: { + include: { + image: true + } + } + } + }) + + //delete the old file + await destroyFile('ombul', oldFsLocation) + + return ombulUpdated + } +}) + export const ombulOperations = { read, readAll, readLatest, - updateCmsCoverImage + updateCmsCoverImage, + destroy, + create, + update, + updateFile } as const diff --git a/src/services/ombul/schemas.ts b/src/services/ombul/schemas.ts new file mode 100644 index 000000000..82f0145bd --- /dev/null +++ b/src/services/ombul/schemas.ts @@ -0,0 +1,38 @@ +import { imageFileSchema } from '@/services/images/schemas' +import { maxOmbulFileSize } from '@/services/ombul/ConfigVars' +import { z } from 'zod' +import { File } from 'node:buffer' + +export const baseSchema = z.object({ + ombulFile: z.instanceof(File).refine(file => file.size < maxOmbulFileSize, 'Fil må være mindre enn 10mb'), + ombulCoverImage: imageFileSchema, + year: z.coerce.number().int().refine(val => + (val === undefined) || (val >= 1919 && val <= (new Date()).getFullYear()), + 'Må være mellom 1919 og nåværende år' + ), + issueNumber: z.coerce.number().optional().refine(val => + val === undefined || val <= 30, 'max 30' + ), + name: z.string().min(2, 'Minimum lengde er 2').max(25, 'Maximum lengde er 25').trim(), + description: z.string().min(2, 'Minimum lengde er 2').max(100, 'Maximum lengde er 100').trim() +}) + +export const ombulSchemas = { + create: baseSchema.pick({ + ombulFile: true, + ombulCoverImage: true, + year: true, + issueNumber: true, + name: true, + description: true + }), + update: baseSchema.pick({ + year: true, + issueNumber: true, + name: true, + description: true + }), + updateFile: baseSchema.pick({ + ombulFile: true + }) +} diff --git a/src/services/ombul/update.ts b/src/services/ombul/update.ts deleted file mode 100644 index 2d2cab8b6..000000000 --- a/src/services/ombul/update.ts +++ /dev/null @@ -1,80 +0,0 @@ -import '@pn-server-only' -import { updateOmbulFileValidation, updateOmbulValidation } from './validation' -import { ServerError } from '@/services/error' -import { prismaCall } from '@/services/prismaCall' -import { prisma } from '@/prisma/client' -import { createFile } from '@/services/store/createFile' -import { destroyFile } from '@/services/store/destroyFile' -import type { UpdateOmbulFileTypes, UpdateOmbulTypes } from './validation' -import type { ExpandedOmbul } from './types' - -/** - * A function Update an ombul - * @param id - The id of the ombul to update - * @param data - The new data for the ombul including: name, year, issueNumber, description. .... - * @returns The updated ombul - */ -export async function updateOmbul( - id: number, - rawdata: UpdateOmbulTypes['Detailed'] -): Promise { - const data = updateOmbulValidation.detailedValidate(rawdata) - return await prismaCall(() => prisma.ombul.update({ - where: { - id - }, - data, - include: { - coverImage: { - include: { - image: true - } - } - } - })) -} - -/** - * Update the ombul file (i.e. the pdf file) of an ombul - * @param id - The id of the ombul to update - * @param file - The new file for the ombul (pdf file) - * @returns The updated ombul - */ -export async function updateOmbulFile( - id: number, - data: UpdateOmbulFileTypes['Detailed'] -): Promise { - const { ombulFile: file } = updateOmbulFileValidation.detailedValidate(data) - const ret = await createFile(file, 'ombul', ['pdf']) - const fsLocation = ret.fsLocation - - const ombul = await prisma.ombul.findUnique({ - where: { - id - } - }) - if (!ombul) throw new ServerError('NOT FOUND', 'Ombul ikke funnet') - - const oldFsLocation = ombul.fsLocation - - const ombulUpdated = await prismaCall(() => prisma.ombul.update({ - where: { - id - }, - data: { - fsLocation - }, - include: { - coverImage: { - include: { - image: true - } - } - } - })) - - //delete the old file - await destroyFile('ombul', oldFsLocation) - - return ombulUpdated -} diff --git a/src/services/ombul/validation.ts b/src/services/ombul/validation.ts deleted file mode 100644 index 0f5ffc3e2..000000000 --- a/src/services/ombul/validation.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { imageFileSchema } from '@/services/images/schemas' -import { ValidationBase } from '@/services/Validation' -import { maxOmbulFileSize } from '@/services/ombul/ConfigVars' -import { z } from 'zod' -import { File } from 'node:buffer' -import type { ValidationTypes } from '@/services/Validation' - -export const baseOmbulValidation = new ValidationBase({ - type: { - ombulFile: z.instanceof(File), - ombulCoverImage: imageFileSchema, - year: z.string().optional(), - issueNumber: z.string().optional(), - name: z.string(), - description: z.string(), - }, - details: { - ombulFile: z.instanceof(File).refine(file => file.size < maxOmbulFileSize, 'Fil må være mindre enn 10mb'), - ombulCoverImage: imageFileSchema, - year: z.number().refine(val => - (val === undefined) || (val >= 1919 && val <= (new Date()).getFullYear()), - 'Må være mellom 1919 og nåværende år' - ), - issueNumber: z.number().optional().refine(val => - val === undefined || val <= 30, 'max 30' - ), - name: z.string().min(2, 'Minimum lengde er 2').max(25, 'Maximum lengde er 25').trim(), - description: z.string().min(2, 'Minimum lengde er 2').max(100, 'Maximum lengde er 100').trim() - } -}) - -const transformer = (data: { - year?: string | undefined, - issueNumber?: string | undefined, -}) => ({ - year: data.year ? parseInt(data.year, 10) : new Date().getFullYear(), - issueNumber: data.issueNumber ? parseInt(data.issueNumber, 10) : undefined -}) - -export const createOmbulValidation = baseOmbulValidation.createValidation({ - keys: [ - 'ombulFile', - 'ombulCoverImage', - 'year', - 'issueNumber', - 'name', - 'description' - ], - transformer: data => ({ - ...data, - ...transformer(data) - }), -}) -export type CreateOmbulTypes = ValidationTypes - -export const updateOmbulValidation = baseOmbulValidation.createValidationPartial({ - keys: [ - 'year', - 'issueNumber', - 'name', - 'description' - ], - transformer: (data) => ({ - ...data, - ...transformer(data) - }) -}) -export type UpdateOmbulTypes = ValidationTypes - -export const updateOmbulFileValidation = baseOmbulValidation.createValidation({ - keys: ['ombulFile'], - transformer: data => data -}) -export type UpdateOmbulFileTypes = ValidationTypes - diff --git a/src/services/omegaOrder/actions.ts b/src/services/omegaOrder/actions.ts index 6bf55f39c..17b2b1701 100644 --- a/src/services/omegaOrder/actions.ts +++ b/src/services/omegaOrder/actions.ts @@ -1,26 +1,14 @@ 'use server' - -import { createActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { safeServerCall } from '@/services/actionError' import { createOmegaOrder } from '@/services/omegaOrder/create' import { readCurrentOmegaOrder } from '@/services/omegaOrder/read' import type { ActionReturn } from '@/services/actionTypes' import type { OmegaOrder } from '@prisma/client' export async function createOmegaOrderAction(): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['OMEGA_ORDER_CREATE']] - }) - if (!authorized) return createActionError(status) - return safeServerCall(createOmegaOrder) } export async function readCurrentOmegaOrderAction(): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['OMEGA_ORDER_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readCurrentOmegaOrder()) } diff --git a/src/services/omegaOrder/auth.ts b/src/services/omegaOrder/auth.ts new file mode 100644 index 000000000..5d3ba7275 --- /dev/null +++ b/src/services/omegaOrder/auth.ts @@ -0,0 +1,8 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +export const omegaOrderAuth = { + //TODO: use authers when refactoring to operations. + create: RequirePermission.staticFields({ permission: 'OMEGA_ORDER_CREATE' }), + readCurrent: RequirePermission.staticFields({ permission: 'OMEGA_ORDER_READ' }), + readAll: RequirePermission.staticFields({ permission: 'OMEGA_ORDER_READ' }) +} as const diff --git a/src/services/omegaid/actions.ts b/src/services/omegaid/actions.ts index e104af06f..d6afdbe8e 100644 --- a/src/services/omegaid/actions.ts +++ b/src/services/omegaid/actions.ts @@ -1,17 +1,15 @@ 'use server' - import { createActionError } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' import { ServerError } from '@/services/error' import { generateOmegaId } from '@/services/omegaid/generate' import type { ActionReturn } from '@/services/actionTypes' +import { Session } from '@/auth/session/Session' export async function generateOmegaIdAction(): Promise> { - const { user, authorized, status } = await getUser({ - userRequired: true, - }) - - if (!authorized) return createActionError(status) + //TODO: when changed to makeAction + operation it should take in a params: userId and + //then auth on userId using the RequireUserId auther. + const user = (await Session.fromNextAuth()).user + if (!user) return createActionError('User not found') const token = generateOmegaId(user) @@ -21,6 +19,7 @@ export async function generateOmegaIdAction(): Promise> { } } +//Suffix with ...Action when refactoring to operations. export async function readOmegaJWTPublicKey(): Promise { const key = process.env.JWT_PUBLIC_KEY From c0d9f90042b30e8081f3d5ec991cbfea60e1b75a Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:00:24 +0100 Subject: [PATCH 3/9] refactor: final part of committee service --- src/services/groups/committees/actions.ts | 47 +------- src/services/groups/committees/auth.ts | 4 +- src/services/groups/committees/create.ts | 77 ------------ src/services/groups/committees/operations.ts | 110 ++++++++++++++++++ src/services/groups/committees/update.ts | 42 ------- src/services/groups/committees/validation.ts | 46 +++----- src/services/groups/memberships/actions.ts | 14 +-- .../groups/studyProgrammes/actions.ts | 18 +-- src/services/images/collections/actions.ts | 6 - 9 files changed, 136 insertions(+), 228 deletions(-) delete mode 100644 src/services/groups/committees/create.ts delete mode 100644 src/services/groups/committees/update.ts diff --git a/src/services/groups/committees/actions.ts b/src/services/groups/committees/actions.ts index 28bd46edd..c67be66c7 100644 --- a/src/services/groups/committees/actions.ts +++ b/src/services/groups/committees/actions.ts @@ -1,39 +1,16 @@ 'use server' - import { makeAction } from '@/services/serverAction' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' -import { createCommittee } from '@/services/groups/committees/create' import { committeeOperations } from '@/services/groups/committees/operations' -import { updateCommittee } from '@/services/groups/committees/update' -import { createCommitteeValidation, updateCommitteeValidation } from '@/services/groups/committees/validation' -import type { ExpandedCommittee } from '@/services/groups/committees/types' -import type { ActionReturn } from '@/services/actionTypes' -import type { CreateCommitteeTypes, UpdateCommitteeTypes } from '@/services/groups/committees/validation' - -export async function createCommitteeAction( - rawData: FormData | CreateCommitteeTypes['Type'] -): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['COMMITTEE_CREATE']], - shouldRedirect: false, - }) - - if (!authorized) return createActionError(status) - - const parse = createCommitteeValidation.typeValidate(rawData) - if (!parse.success) return createZodActionError(parse) - - return await safeServerCall(() => createCommittee(parse.data)) -} +export const createCommitteeAction = makeAction(committeeOperations.create) export const readAllCommitteesAction = makeAction(committeeOperations.readAll) export const readCommitteeAction = makeAction(committeeOperations.read) export const readCommitteeArticleAction = makeAction(committeeOperations.readArticle) - export const readCommitteeParagraphAction = makeAction(committeeOperations.readParagraph) export const readCommitteeMembersAction = makeAction(committeeOperations.readMembers) export const updateCommitteeParagraphAction = makeAction(committeeOperations.updateParagraphContent) +export const destroyCommitteeAction = makeAction(committeeOperations.destroy) +export const updateCommitteeAction = makeAction(committeeOperations.update) export const updateCommitteeLogoAction = makeAction(committeeOperations.updateLogo) @@ -67,21 +44,3 @@ export const updateCommitteeArticleCmsParagraphAction = makeAction( export const updateCommitteeArticleCmsLinkAction = makeAction( committeeOperations.updateArticle.articleSections.cmsLink ) - -export async function updateCommitteeAction( - id: number, - rawdata: FormData | UpdateCommitteeTypes['Type'] -) { - const { status, authorized } = await getUser({ - requiredPermissions: [['COMMITTEE_UPDATE']] - }) - if (!authorized) return createActionError(status) - - const parse = updateCommitteeValidation.typeValidate(rawdata) - if (!parse.success) return createZodActionError(parse) - const data = parse.data - - return await safeServerCall(() => updateCommittee(id, data)) -} - -export const destroyCommitteeAction = makeAction(committeeOperations.destroy) diff --git a/src/services/groups/committees/auth.ts b/src/services/groups/committees/auth.ts index c98cd4af5..9eebb39b8 100644 --- a/src/services/groups/committees/auth.ts +++ b/src/services/groups/committees/auth.ts @@ -2,6 +2,8 @@ import { RequirePermission } from '@/auth/auther/RequirePermission' import { RequirePermissionOrGroupAdmin } from '@/auth/auther/RequirePermissionOrGroupAdmin' export const committeeAuth = { + create: RequirePermission.staticFields({ permission: 'COMMITTEE_CREATE' }), + update: RequirePermission.staticFields({ permission: 'COMMITTEE_UPDATE' }), readAll: RequirePermission.staticFields({ permission: 'COMMITTEE_READ' }), read: RequirePermission.staticFields({ permission: 'COMMITTEE_READ' }), readMembers: RequirePermission.staticFields({ permission: 'COMMITTEE_READ' }), @@ -10,5 +12,5 @@ export const committeeAuth = { destroy: RequirePermission.staticFields({ permission: 'COMMITTEE_DESTROY' }), updateParagraphContent: RequirePermissionOrGroupAdmin.staticFields({ permission: 'COMMITTEE_UPDATE' }), updateLogo: RequirePermissionOrGroupAdmin.staticFields({ permission: 'COMMITTEE_UPDATE' }), - updateArticle: RequirePermissionOrGroupAdmin.staticFields({ permission: 'COMMITTEE_UPDATE' }) + updateArticle: RequirePermissionOrGroupAdmin.staticFields({ permission: 'COMMITTEE_UPDATE' }), } diff --git a/src/services/groups/committees/create.ts b/src/services/groups/committees/create.ts deleted file mode 100644 index 631d7349e..000000000 --- a/src/services/groups/committees/create.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createCommitteeValidation } from './validation' -import { prisma } from '@/prisma/client' -import { prismaCall } from '@/services/prismaCall' -import { readCurrentOmegaOrder } from '@/services/omegaOrder/read' -import { imageOperations } from '@/services/images/operations' -import { cmsParagraphOperations } from '@/cms/paragraphs/operations' -import { articleOperations } from '@/cms/articles/operations' -import { GroupType } from '@prisma/client' -import type { ExpandedCommittee } from './types' -import type { CreateCommitteeTypes } from './validation' - -export async function createCommittee(rawdata: CreateCommitteeTypes['Detailed']): Promise { - const { name, shortName, logoImageId } = createCommitteeValidation.detailedValidate(rawdata) - let defaultLogoImageId: number - if (!logoImageId) { - defaultLogoImageId = await imageOperations.readSpecial({ - params: { special: 'DAFAULT_COMMITTEE_LOGO' }, - }).then(res => res.id) - } - const article = await articleOperations.create({ data: {}, bypassAuth: true }) - - const paragraph = await cmsParagraphOperations.create({ - data: { name: `Paragraph for ${name}` }, - bypassAuth: true - }) - const applicationParagraph = await cmsParagraphOperations.create({ - data: { name: `Søknadstekst for ${name}` }, - bypassAuth: true - }) - - const order = (await readCurrentOmegaOrder()).order - - return await prismaCall(() => prisma.committee.create({ - data: { - name, - shortName, - logoImage: { - create: { - name: `Komitélogoen til ${name}`, - image: { - connect: { - id: logoImageId ?? defaultLogoImageId, - }, - }, - }, - }, - paragraph: { - connect: { - id: paragraph.id, - } - }, - group: { - create: { - groupType: GroupType.COMMITTEE, - order, - } - }, - committeeArticle: { - connect: { - id: article.id - } - }, - applicationParagraph: { - connect: { - id: applicationParagraph.id - } - } - }, - include: { - logoImage: { - include: { - image: true, - }, - }, - }, - })) -} diff --git a/src/services/groups/committees/operations.ts b/src/services/groups/committees/operations.ts index 34bb8a938..fe8eb13c3 100644 --- a/src/services/groups/committees/operations.ts +++ b/src/services/groups/committees/operations.ts @@ -9,6 +9,9 @@ import { articleRealtionsIncluder } from '@/cms/articles/constants' import { implementUpdateArticleOperations } from '@/cms/articles/implement' import { articleOperations } from '@/cms/articles/operations' import { z } from 'zod' +import { committeeSchemas } from './validation' +import { readCurrentOmegaOrder } from '@/services/omegaOrder/read' +import { GroupType } from '@prisma/client' const readAll = defineOperation({ @@ -195,7 +198,114 @@ const destroy = defineOperation({ } }) +const create = defineOperation({ + authorizer: () => committeeAuth.create.dynamicFields({}), + dataSchema: committeeSchemas.create, + operation: async ({ prisma, data }) => { + const { name, shortName, logoImageId } = data + const defaultLogoImageId = await imageOperations.readSpecial({ + params: { special: 'DAFAULT_COMMITTEE_LOGO' }, + }).then(res => res.id) + const article = await articleOperations.create({ data: {}, bypassAuth: true }) + + const paragraph = await cmsParagraphOperations.create({ + data: { name: `Paragraph for ${name}` }, + bypassAuth: true + }) + const applicationParagraph = await cmsParagraphOperations.create({ + data: { name: `Søknadstekst for ${name}` }, + bypassAuth: true + }) + + const order = (await readCurrentOmegaOrder()).order + + return await prisma.committee.create({ + data: { + name, + shortName, + logoImage: { + create: { + name: `Komitélogoen til ${name}`, + image: { + connect: { + id: logoImageId ?? defaultLogoImageId, + }, + }, + }, + }, + paragraph: { + connect: { + id: paragraph.id, + } + }, + group: { + create: { + groupType: GroupType.COMMITTEE, + order, + } + }, + committeeArticle: { + connect: { + id: article.id + } + }, + applicationParagraph: { + connect: { + id: applicationParagraph.id + } + } + }, + include: { + logoImage: { + include: { + image: true, + }, + }, + }, + }) + } +}) + +const update = defineOperation({ + authorizer: () => committeeAuth.update.dynamicFields({}), + paramsSchema: z.object({ + id: z.number() + }), + dataSchema: committeeSchemas.update, + operation: async ({ prisma, params, data }) => { + const { name, shortName, logoImageId } = data + + const defaultLogoImageId = await imageOperations.readSpecial({ + params: { special: 'DAFAULT_COMMITTEE_LOGO' }, + }).then(res => res.id) + + return await prisma.committee.update({ + where: { + id: params.id, + }, + data: { + name, + shortName, + logoImage: { + update: { + imageId: logoImageId ?? defaultLogoImageId, + }, + }, + }, + include: { + logoImage: { + include: { + image: true, + }, + }, + }, + }) + } +}) + export const committeeOperations = { + create, + update, readAll, read, readFromGroupIds, diff --git a/src/services/groups/committees/update.ts b/src/services/groups/committees/update.ts deleted file mode 100644 index bd25dc76e..000000000 --- a/src/services/groups/committees/update.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { updateCommitteeValidation } from './validation' -import { prisma } from '@/prisma/client' -import { prismaCall } from '@/services/prismaCall' -import { imageOperations } from '@/services/images/operations' -import type { ExpandedCommittee } from './types' -import type { UpdateCommitteeTypes } from './validation' - -export async function updateCommittee( - id: number, - rawdata: UpdateCommitteeTypes['Detailed'] -): Promise { - const { name, shortName, logoImageId } = updateCommitteeValidation.detailedValidate(rawdata) - - let defaultLogoImageId: number - if (!logoImageId) { - defaultLogoImageId = await imageOperations.readSpecial({ - params: { special: 'DAFAULT_COMMITTEE_LOGO' }, //TODO: pass session - }).then(res => res.id) - } - - return await prismaCall(() => prisma.committee.update({ - where: { - id, - }, - data: { - name, - shortName, - logoImage: { - update: { - imageId: logoImageId ?? defaultLogoImageId, - }, - }, - }, - include: { - logoImage: { - include: { - image: true, - }, - }, - }, - })) -} diff --git a/src/services/groups/committees/validation.ts b/src/services/groups/committees/validation.ts index 8b52fde5a..0f2be217e 100644 --- a/src/services/groups/committees/validation.ts +++ b/src/services/groups/committees/validation.ts @@ -1,34 +1,20 @@ -import { ValidationBase } from '@/services/Validation' import { z } from 'zod' -import type { ValidationTypes } from '@/services/Validation' -const baseCommitteeValidation = new ValidationBase({ - type: { - name: z.string(), - shortName: z.string(), - logoImageId: z.string().optional(), - }, - details: { - name: z.string().max(32).min(1).trim(), - shortName: z.string().max(32).min(1).trim(), - logoImageId: z.number().optional(), - } +const baseSchema = z.object({ + name: z.string().max(32).min(1).trim(), + shortName: z.string().max(32).min(1).trim(), + logoImageId: z.coerce.number().optional(), }) -export const createCommitteeValidation = baseCommitteeValidation.createValidation({ - keys: ['name', 'shortName', 'logoImageId'], - transformer: data => ({ - ...data, - logoImageId: data.logoImageId ? parseInt(data.logoImageId, 10) : undefined - }) -}) -export type CreateCommitteeTypes = ValidationTypes - -export const updateCommitteeValidation = baseCommitteeValidation.createValidationPartial({ - keys: ['name', 'shortName', 'logoImageId'], - transformer: data => ({ - ...data, - logoImageId: data.logoImageId ? parseInt(data.logoImageId, 10) : undefined - }) -}) -export type UpdateCommitteeTypes = ValidationTypes +export const committeeSchemas = { + create: baseSchema.pick({ + name: true, + shortName: true, + logoImageId: true + }), + update: baseSchema.pick({ + name: true, + shortName: true, + logoImageId: true + }).partial() +} diff --git a/src/services/groups/memberships/actions.ts b/src/services/groups/memberships/actions.ts index 9510ca9a3..c85994351 100644 --- a/src/services/groups/memberships/actions.ts +++ b/src/services/groups/memberships/actions.ts @@ -1,13 +1,14 @@ 'use server' -import { createActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { safeServerCall } from '@/services/actionError' import { createMembershipsForGroup } from '@/services/groups/memberships/create' import { destoryMembershipOfUser } from '@/services/groups/memberships/destroy' import { updateMembership } from '@/services/groups/memberships/update' import type { ExpandedMembership } from '@/services/groups/memberships/types' import type { ActionReturn } from '@/services/actionTypes' +// TODO: All the following actions should be authed on RequirePermissionOrGroupAdmin when refactored. + /** * WARNING: This action will lead to error if used with group types not in CanEasalyManageMembership */ @@ -21,11 +22,6 @@ export async function createMembershipsForGroupAction({ admin: boolean }[] }): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['GROUP_ADMIN']] - }) - if (!authorized) return createActionError(status) - return safeServerCall(() => createMembershipsForGroup(groupId, users)) } @@ -43,8 +39,6 @@ export async function destroyMembership({ userId: number, orderArg: number }): Promise> { - //TODO: make function to check that. user is admin of group - return await safeServerCall(() => destoryMembershipOfUser({ groupId, userId, @@ -56,7 +50,6 @@ export async function updateMembershipAdminAcion(membership: { groupId: number userId: number }, admin: boolean): Promise> { - //TODO: make function to check that user is admin of group return await safeServerCall(() => updateMembership({ ...membership, orderArg: 'ACTIVE' @@ -67,7 +60,6 @@ export async function updateMembershipActiveAction(membership: { groupId: number userId: number }, active: boolean): Promise> { - //TODO: make function to check that user is admin of group return await safeServerCall(() => updateMembership({ ...membership, orderArg: 'ACTIVE' diff --git a/src/services/groups/studyProgrammes/actions.ts b/src/services/groups/studyProgrammes/actions.ts index f4363d6b1..65ff4eea6 100644 --- a/src/services/groups/studyProgrammes/actions.ts +++ b/src/services/groups/studyProgrammes/actions.ts @@ -1,7 +1,6 @@ 'use server' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createStudyProgramme } from '@/services/groups/studyProgrammes/create' import { readStudyProgrammes } from '@/services/groups/studyProgrammes/read' import { updateStudyProgramme } from '@/services/groups/studyProgrammes/update' @@ -10,11 +9,6 @@ import type { ActionReturn } from '@/services/actionTypes' import type { StudyProgramme } from '@prisma/client' export async function createStudyProgrammeAction(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['STUDY_PROGRAMME_CREATE']], - }) - if (!authorized) return createActionError(status) - const parse = createStudyProgrammeValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) @@ -22,20 +16,10 @@ export async function createStudyProgrammeAction(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['STUDY_PROGRAMME_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readStudyProgrammes()) } export async function updateStudyProgrammeAction(id: number, rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['STUDY_PROGRAMME_UPDATE']], - }) - if (!authorized) return createActionError(status) - const parse = updateStudyProgrammeValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) diff --git a/src/services/images/collections/actions.ts b/src/services/images/collections/actions.ts index 57b47fde0..fb951932b 100644 --- a/src/services/images/collections/actions.ts +++ b/src/services/images/collections/actions.ts @@ -1,7 +1,6 @@ 'use server' import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' import { createImageCollection } from '@/services/images/collections/create' import { destroyImageCollection } from '@/services/images/collections/destroy' import { @@ -22,11 +21,6 @@ import type { ImageCollection } from '@prisma/client' export async function createImageCollectionAction( rawdata: FormData | CreateImageCollectionTypes['Type'] ): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['IMAGE_COLLECTION_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createImageCollectionValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) const data = parse.data From 2ea609f33bdc241af52e15c2413d70b26d25735d Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 27 Oct 2025 19:09:43 +0100 Subject: [PATCH 4/9] chore: change some frontend code to call authers --- src/app/ombul/[...yearAndName]/page.tsx | 15 ++++++------- src/app/omegaquotes/page.tsx | 29 ++++++++++++++----------- src/auth/session/useUser.ts | 2 -- src/services/actionError.ts | 2 +- src/services/error.ts | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/app/ombul/[...yearAndName]/page.tsx b/src/app/ombul/[...yearAndName]/page.tsx index 4b1fad257..8e140fd34 100644 --- a/src/app/ombul/[...yearAndName]/page.tsx +++ b/src/app/ombul/[...yearAndName]/page.tsx @@ -5,11 +5,12 @@ import { readOmbulAction, updateOmbulAction, updateOmbulCmsCoverImageAction } fr import PdfDocument from '@/components/PdfDocument/PdfDocument' import SlideInOnView from '@/components/SlideInOnView/SlideInOnView' import EditableTextField from '@/components/EditableTextField/EditableTextField' -import { getUser } from '@/auth/session/getUser' import CmsImage from '@/components/Cms/CmsImage/CmsImage' import { configureAction } from '@/services/configureAction' import Link from 'next/link' import { notFound } from 'next/navigation' +import { ombulAuth } from '@/services/ombul/auth' +import { Session } from '@/auth/session/Session' type PropTypes = { params: Promise<{ @@ -18,11 +19,6 @@ type PropTypes = { } export default async function Ombul({ params }: PropTypes) { - const { permissions } = await getUser({ - requiredPermissions: [['OMBUL_READ']], - shouldRedirect: true, - }) - const year = parseInt(decodeURIComponent((await params).yearAndName[0]), 10) const name = decodeURIComponent((await params).yearAndName[1]) if (!year || !name || (await params).yearAndName.length > 2) notFound() @@ -37,9 +33,12 @@ export default async function Ombul({ params }: PropTypes) { const path = `/store/ombul/${ombul.fsLocation}` - const canUpdate = permissions.includes('OMBUL_UPDATE') //TODO: use update auther. + const canUpdate = ombulAuth.update.dynamicFields({}).auth(await Session.fromNextAuth()).authorized - const changeDescription = updateOmbulAction.bind(null, ombul.id) + const changeDescription = configureAction( + updateOmbulAction, + { params: { id: ombul.id } } + ) return (
diff --git a/src/app/omegaquotes/page.tsx b/src/app/omegaquotes/page.tsx index bd4faca12..6ee1852c8 100644 --- a/src/app/omegaquotes/page.tsx +++ b/src/app/omegaquotes/page.tsx @@ -4,28 +4,31 @@ import CreateOmegaquoteForm from './CreateOmegaquoteForm' import { OmegaquotePagingProvider } from '@/contexts/paging/OmegaquotesPaging' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readQuotesPageAction } from '@/services/omegaquotes/actions' -import { getUser } from '@/auth/session/getUser' +import { omegaQuotesAuth } from '@/services/omegaquotes/auth' +import { Session } from '@/auth/session/Session' import { notFound } from 'next/navigation' import { v4 as uuid } from 'uuid' import type { PageSizeOmegaquote } from '@/contexts/paging/OmegaquotesPaging' export default async function OmegaQuotes() { - const { permissions } = await getUser({ - shouldRedirect: true, - requiredPermissions: [['OMEGAQUOTES_READ']], - }) - - const showCreateButton = permissions.includes('OMEGAQUOTES_WRITE') + const session = await Session.fromNextAuth() + const showCreateButton = session.user && omegaQuotesAuth.create.dynamicFields({ + userId: session.user.id + }).auth(session).authorized || false const pageSize: PageSizeOmegaquote = 20 const readQuotes = await readQuotesPageAction({ - page: { - pageSize, - page: 0, - cursor: null, - }, - details: undefined + params: { + paging: { + page: { + pageSize, + page: 0, + cursor: null, + }, + details: undefined + } + } }) if (!readQuotes.success) notFound() const quotes = readQuotes.data diff --git a/src/auth/session/useUser.ts b/src/auth/session/useUser.ts index a09bc86a0..9bf1d4b57 100644 --- a/src/auth/session/useUser.ts +++ b/src/auth/session/useUser.ts @@ -64,8 +64,6 @@ export type ClientAuthStatus = UseUserReturnType['status'] * Wrapper for next-auth's `useSession`. Returns just the user object of the * current session, null otherwise. * -* This function is for client side components. For server side components -* use `getUser``. * @param requiredPermissions - A list of lists that the user must have. If non are given, the user is considered authorized * regardless of their permissions. * @param userRequired - False by default. If true the user will only be unauthorized if they are not logged inn. diff --git a/src/services/actionError.ts b/src/services/actionError.ts index d8bc22767..851a79aa6 100644 --- a/src/services/actionError.ts +++ b/src/services/actionError.ts @@ -1,6 +1,6 @@ import { errorCodes, type ErrorCode, type ErrorMessage } from '@/services/error' import { ParseError, Smorekopp } from '@/services/error' -import type { AuthStatus } from '@/auth/session/getUser' +import type { AuthStatus } from '@/auth/auther/AuthResult' import type { SafeParseError } from 'zod' import type { ActionError, ActionReturn } from './actionTypes' diff --git a/src/services/error.ts b/src/services/error.ts index a4d59fabf..d6daff9b4 100644 --- a/src/services/error.ts +++ b/src/services/error.ts @@ -1,5 +1,5 @@ import type { SafeParseError } from 'zod' -import type { AuthStatus } from '@/auth/session/getUser' +import type { AuthStatus } from '@/auth/auther/AuthResult' export const errorCodes = [ { From bde157880067bd331a91e1f68034e9057067412b Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:17:43 +0100 Subject: [PATCH 5/9] chore: remove all refs to getUser --- .../EditableTextField/EditableTextField.tsx | 3 +- .../[id]/(editComponents)/mailAlias.tsx | 2 +- src/app/admin/mail/[filter]/[id]/page.tsx | 2 +- src/app/admin/mail/[filter]/page.tsx | 2 +- src/app/admin/mail/page.tsx | 2 +- src/app/admin/send-mail/page.tsx | 15 ++--- src/app/admin/stateOfOmega/page.tsx | 17 +++-- src/app/admin/study-programmes/page.tsx | 16 +---- src/app/education/schools/page.tsx | 7 +- src/app/images/page.tsx | 4 +- src/app/lockers/[id]/page.tsx | 8 --- src/app/lockers/page.tsx | 6 -- src/app/ombul/[...yearAndName]/ChangeName.tsx | 8 ++- src/app/ombul/page.tsx | 9 +-- src/auth/auther/RequireEveryPermission.ts | 12 ++++ src/prisma/schema/permission.prisma | 2 +- src/services/education/schools/actions.ts | 33 +--------- src/services/education/schools/auth.ts | 6 ++ src/services/mail/actions.ts | 64 +------------------ src/services/mail/alias/actions.ts | 23 +------ src/services/mail/alias/auth.ts | 9 +++ src/services/mail/auth.ts | 21 ++++++ src/services/mail/list/actions.ts | 18 +----- src/services/mail/list/auth.ts | 9 +++ .../mail/mailAddressExternal/actions.ts | 19 +----- src/services/mail/mailAddressExternal/auth.ts | 9 +++ src/services/notifications/channel/schemas.ts | 1 + src/services/permissions/constants.ts | 2 +- 28 files changed, 108 insertions(+), 221 deletions(-) create mode 100644 src/auth/auther/RequireEveryPermission.ts create mode 100644 src/services/mail/alias/auth.ts create mode 100644 src/services/mail/auth.ts create mode 100644 src/services/mail/list/auth.ts create mode 100644 src/services/mail/mailAddressExternal/auth.ts diff --git a/src/app/_components/EditableTextField/EditableTextField.tsx b/src/app/_components/EditableTextField/EditableTextField.tsx index 5f57d24db..6ccc9b9ae 100644 --- a/src/app/_components/EditableTextField/EditableTextField.tsx +++ b/src/app/_components/EditableTextField/EditableTextField.tsx @@ -35,8 +35,7 @@ export default function EditableTextField({ submitButton, inputName, ...props -}: PropTypes -) { +}: PropTypes) { const [value, setValue] = useState('') const [noChange, setNoChange] = useState(true) const canEdit = useEditing({}) //TODO: auth must be passed diff --git a/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx b/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx index 9fc36fab4..13e98137d 100644 --- a/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx +++ b/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx @@ -42,7 +42,7 @@ export default function EditMailAlias({
} - { permissions.includes('MAILALIAS_DESTORY') &&
+ { permissions.includes('MAILALIAS_DESTROY') &&
push('/admin/mail')} diff --git a/src/app/admin/mail/[filter]/[id]/page.tsx b/src/app/admin/mail/[filter]/[id]/page.tsx index a1b4c284d..48f438559 100644 --- a/src/app/admin/mail/[filter]/[id]/page.tsx +++ b/src/app/admin/mail/[filter]/[id]/page.tsx @@ -1,4 +1,4 @@ -'use server' //why is this use server??? +'use server' //todo: why is this use server??? import MailFlow from './MailFlow' import styles from './page.module.scss' diff --git a/src/app/admin/mail/[filter]/page.tsx b/src/app/admin/mail/[filter]/page.tsx index 586086216..70bab6326 100644 --- a/src/app/admin/mail/[filter]/page.tsx +++ b/src/app/admin/mail/[filter]/page.tsx @@ -1,4 +1,4 @@ -'use server' //why is this use server??? +'use server' //todo: why is this use server??? import { RedirectType, redirect } from 'next/navigation' diff --git a/src/app/admin/mail/page.tsx b/src/app/admin/mail/page.tsx index 8d318e86d..0f455b47b 100644 --- a/src/app/admin/mail/page.tsx +++ b/src/app/admin/mail/page.tsx @@ -1,4 +1,4 @@ -'use server' //why is this use server??? +'use server' //todo: why is this use server??? import styles from './page.module.scss' import CreateMailAlias from './createMailAliasForm' import CreateMailingList from './createMailingListForm' diff --git a/src/app/admin/send-mail/page.tsx b/src/app/admin/send-mail/page.tsx index a705e3e09..197bcdfbf 100644 --- a/src/app/admin/send-mail/page.tsx +++ b/src/app/admin/send-mail/page.tsx @@ -1,17 +1,12 @@ import MailForm from './mailForm' import PageWrapper from '@/components/PageWrapper/PageWrapper' -import { getUser } from '@/auth/session/getUser' -import { notFound } from 'next/navigation' +import { notificationAuth } from '@/services/notifications/auth' +import { Session } from '@/auth/session/Session' export default async function SendMail() { - // TODO: permission checks - const { authorized } = await getUser({ - requiredPermissions: [['MAIL_SEND']], - }) - - if (!authorized) { - notFound() - } + notificationAuth.sendMail.dynamicFields({}).auth( + await Session.fromNextAuth() + ).redirectOnUnauthorized({ returnUrl: '/admin/send-mail' }) return ( diff --git a/src/app/admin/stateOfOmega/page.tsx b/src/app/admin/stateOfOmega/page.tsx index 52c783475..dd5e81fae 100644 --- a/src/app/admin/stateOfOmega/page.tsx +++ b/src/app/admin/stateOfOmega/page.tsx @@ -1,21 +1,20 @@ import styles from './page.module.scss' import CreateOrder from './CreateOrder' -import { getUser } from '@/auth/session/getUser' import { readCurrentOmegaOrderAction } from '@/services/omegaOrder/actions' -import { notFound } from 'next/navigation' +import { omegaOrderAuth } from '@/services/omegaOrder/auth' +import { Session } from '@/auth/session/Session' +import { unwrapActionReturn } from '@/app/redirectToErrorPage' export default async function stateOfOmega() { - const { authorized } = await getUser({ - requiredPermissions: [['OMEGA_ORDER_CREATE']] - }) - if (!authorized) return notFound() //TODO: improve error handling + omegaOrderAuth.create.dynamicFields({}).auth( + await Session.fromNextAuth() + ).redirectOnUnauthorized({ returnUrl: '/admin/state-of-omega' }) - const currentOreder = await readCurrentOmegaOrderAction() - if (!currentOreder.success) return notFound() //TODO: improve error handling + const currentOrder = unwrapActionReturn(await readCurrentOmegaOrderAction()) return (
-

Omega er i orden: { currentOreder.data.order }

+

Omega er i orden: { currentOrder.order }

) diff --git a/src/app/admin/study-programmes/page.tsx b/src/app/admin/study-programmes/page.tsx index 7e7f21715..9aee552f5 100644 --- a/src/app/admin/study-programmes/page.tsx +++ b/src/app/admin/study-programmes/page.tsx @@ -6,21 +6,11 @@ import styles from './page.module.scss' import { readStudyProgrammesAction } from '@/services/groups/studyProgrammes/actions' import { AddHeaderItemPopUp } from '@/components/HeaderItems/HeaderItemPopUp' import PageWrapper from '@/components/PageWrapper/PageWrapper' -import { getUser } from '@/auth/session/getUser' +import { unwrapActionReturn } from '@/app/redirectToErrorPage' export default async function StudyProgrammes() { - const { permissions } = await getUser({ - requiredPermissions: [['STUDY_PROGRAMME_READ']], - shouldRedirect: true, - }) - - const studyprogrammes = await readStudyProgrammesAction() - - if (!studyprogrammes.success) { - console.log(studyprogrammes) - return
Ups, an error occured
- } + const studyprogrammes = unwrapActionReturn(await readStudyProgrammesAction()) const showCreateButton = permissions.includes('STUDY_PROGRAMME_CREATE') const canEdit = permissions.includes('STUDY_PROGRAMME_UPDATE') @@ -48,7 +38,7 @@ export default async function StudyProgrammes() { Del av Omega - +
} diff --git a/src/app/education/schools/page.tsx b/src/app/education/schools/page.tsx index fdee5ad2c..ab016ce96 100644 --- a/src/app/education/schools/page.tsx +++ b/src/app/education/schools/page.tsx @@ -1,17 +1,16 @@ import styles from './page.module.scss' import { readSchoolsPageAction } from '@/services/education/schools/actions' -import { getUser } from '@/auth/session/getUser' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { SchoolPagingProvider } from '@/contexts/paging/SchoolPaging' import SchoolList from '@/components/School/SchoolList' +import { schoolAuth } from '@/services/education/schools/auth' +import { Session } from '@/auth/session/Session' import { schoolListRenderer } from '@/components/School/SchoolListRenderer' import Link from 'next/link' import type { PageSizeSchool } from '@/contexts/paging/SchoolPaging' export default async function Schools() { - const { permissions } = await getUser() - - const isSchoolAdmin = permissions.includes('SCHOOLS_ADMIN') + const isSchoolAdmin = schoolAuth.create.dynamicFields({}).auth(await Session.fromNextAuth()).authorized const pageSizeSchool: PageSizeSchool = 8 const res = await readSchoolsPageAction({ diff --git a/src/app/images/page.tsx b/src/app/images/page.tsx index a2c51e127..e7843f61e 100644 --- a/src/app/images/page.tsx +++ b/src/app/images/page.tsx @@ -3,12 +3,12 @@ import MakeNewCollection from './MakeNewCollection' import ImageCollectionList from '@/components/Image/Collection/ImageCollectionList' import { ImageCollectionPagingProvider } from '@/contexts/paging/ImageCollectionPaging' import CollectionCard from '@/components/Image/Collection/CollectionCard' -import { getUser } from '@/auth/session/getUser' import { readImageCollectionsPageAction } from '@/services/images/collections/actions' +import { Session } from '@/auth/session/Session' import type { PageSizeImageCollection } from '@/contexts/paging/ImageCollectionPaging' export default async function Images() { - const { user } = await getUser() + const { user } = await Session.fromNextAuth() const isAdmin = user?.username === 'harambe' //TODO: temp const pageSize: PageSizeImageCollection = 12 diff --git a/src/app/lockers/[id]/page.tsx b/src/app/lockers/[id]/page.tsx index 63e49ebc0..0a35515d3 100644 --- a/src/app/lockers/[id]/page.tsx +++ b/src/app/lockers/[id]/page.tsx @@ -4,7 +4,6 @@ import CreateLockerReservationForm from './CreateLockerReservationForm' import UpdateLockerReservationForm from './UpdateLockerReservationForm' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readLockerAction } from '@/services/lockers/actions' -import { getUser } from '@/auth/session/getUser' import { checkGroupValidity, groupOperations, inferGroupName } from '@/services/groups/operations' import { notFound } from 'next/navigation' @@ -16,13 +15,6 @@ type PropTypes = { } export default async function Locker({ params }: PropTypes) { - const { user, authorized } = await getUser({ - userRequired: true, - shouldRedirect: true, - requiredPermissions: [['LOCKER_USE']], - }) - if (!authorized) notFound() - const lockerId = parseInt((await params).id, 10) const locker = await readLockerAction({ params: { id: lockerId } }) diff --git a/src/app/lockers/page.tsx b/src/app/lockers/page.tsx index 3f8e6222c..092fa9b47 100644 --- a/src/app/lockers/page.tsx +++ b/src/app/lockers/page.tsx @@ -2,15 +2,9 @@ import LockerIdForm from './LockerIdForm' import LockerList from './LockerList' import QRButton from './QRButton' import PageWrapper from '@/components/PageWrapper/PageWrapper' -import { getUser } from '@/auth/session/getUser' import { LockerPagingProvider } from '@/contexts/paging/LockerPaging' export default async function Lockers() { - await getUser({ - shouldRedirect: true, - requiredPermissions: [['LOCKER_USE']], - }) - return ( diff --git a/src/app/ombul/[...yearAndName]/ChangeName.tsx b/src/app/ombul/[...yearAndName]/ChangeName.tsx index c28c63043..032eea4bf 100644 --- a/src/app/ombul/[...yearAndName]/ChangeName.tsx +++ b/src/app/ombul/[...yearAndName]/ChangeName.tsx @@ -5,6 +5,7 @@ import EditableTextField from '@/components/EditableTextField/EditableTextField' import { updateOmbulAction } from '@/services/ombul/actions' import type { ReactNode } from 'react' import type { ExpandedOmbul } from '@/services/ombul/types' +import { configureAction } from '@/services/configureAction' type PropTypes = { children: ReactNode @@ -21,8 +22,6 @@ type PropTypes = { * @returns The component jsx */ export default function ChangeName({ children, editable, ombulId }: PropTypes) { - const changeName = updateOmbulAction.bind(null, ombulId) - const handleChange = async (data: ExpandedOmbul | undefined) => { const name = data?.name if (!name) return @@ -37,7 +36,10 @@ export default function ChangeName({ children, editable, ombulId }: PropTypes) { , + 'USER_NOT_REQUIERED_FOR_AUTHORIZED' +>(({ session, staticFields }) => ({ + success: staticFields.permissions.every(permission => session.permissions.includes(permission)), + errorMessage: 'Du har ikke en av de nødvendige tillatelsene', + session, +})) diff --git a/src/prisma/schema/permission.prisma b/src/prisma/schema/permission.prisma index 5ab31fbe3..37f313e21 100644 --- a/src/prisma/schema/permission.prisma +++ b/src/prisma/schema/permission.prisma @@ -102,7 +102,7 @@ enum Permission { MAILALIAS_CREATE MAILALIAS_READ MAILALIAS_UPDATE - MAILALIAS_DESTORY + MAILALIAS_DESTROY // Mailing list MAILINGLIST_CREATE diff --git a/src/services/education/schools/actions.ts b/src/services/education/schools/actions.ts index 07cbda3f1..5af2b10b0 100644 --- a/src/services/education/schools/actions.ts +++ b/src/services/education/schools/actions.ts @@ -1,7 +1,6 @@ 'use server' import { schoolOperations } from './operations' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createSchoolValidation, updateSchoolValidation } from '@/education/schools/validation' import { createSchool } from '@/services/education/schools/create' import { destroySchool } from '@/services/education/schools/destroy' @@ -16,11 +15,6 @@ import type { ActionReturn } from '@/services/actionTypes' export async function createSchoolAction( rawdata: FormData | CreateSchoolTypes['Type'] ): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCHOOLS_ADMIN']] - }) - if (!authorized) return createActionError(status) - const parse = createSchoolValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) const data = parse.data @@ -29,31 +23,16 @@ export async function createSchoolAction( } export async function destroySchoolAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCHOOLS_ADMIN']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => destroySchool(id)) } export async function readSchoolsPageAction( pageReadInput: ReadPageInput ): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCHOOLS_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readSchoolsPage(pageReadInput)) } export async function readStandardSchoolsAction(): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCHOOLS_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readStandardSchools()) } @@ -62,11 +41,6 @@ export async function readSchoolsAction({ }: { onlyNonStandard: boolean }): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCHOOLS_READ']] - }) - if (!authorized) return createActionError(status) - return await safeServerCall(() => readSchools({ onlyNonStandard })) } @@ -76,11 +50,6 @@ export async function updateSchoolAction( id: number, rawdata: FormData | UpdateSchoolTypes['Type'] ): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['SCHOOLS_ADMIN']] - }) - if (!authorized) return createActionError(status) - const parse = updateSchoolValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) const data = parse.data diff --git a/src/services/education/schools/auth.ts b/src/services/education/schools/auth.ts index 03b805642..25ae27dec 100644 --- a/src/services/education/schools/auth.ts +++ b/src/services/education/schools/auth.ts @@ -1,6 +1,12 @@ import { RequirePermission } from '@/auth/auther/RequirePermission' export const schoolAuth = { + create: RequirePermission.staticFields({ permission: 'SCHOOLS_ADMIN' }), + destroy: RequirePermission.staticFields({ permission: 'SCHOOLS_ADMIN' }), + readPage: RequirePermission.staticFields({ permission: 'SCHOOLS_READ' }), + readStandard: RequirePermission.staticFields({ permission: 'SCHOOLS_READ' }), + readMany: RequirePermission.staticFields({ permission: 'SCHOOLS_READ' }), + update: RequirePermission.staticFields({ permission: 'SCHOOLS_ADMIN' }), updateCmsParagraph: RequirePermission.staticFields({ permission: 'SCHOOLS_ADMIN' }), read: RequirePermission.staticFields({ permission: 'SCHOOLS_READ' }), updateCmsImage: RequirePermission.staticFields({ permission: 'SCHOOLS_ADMIN' }), diff --git a/src/services/mail/actions.ts b/src/services/mail/actions.ts index a3a7a7432..e88b69292 100644 --- a/src/services/mail/actions.ts +++ b/src/services/mail/actions.ts @@ -1,7 +1,5 @@ 'use server' - -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { readMailAliases } from '@/services/mail/alias/read' import { createAliasMailingListRelation, @@ -35,11 +33,6 @@ import type { MailAliasMailingList, export async function createAliasMailingListRelationAction(formdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_ALIAS_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createAliasMailingListValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -48,11 +41,6 @@ export async function createAliasMailingListRelationAction(formdata: FormData): export async function createMailingListExternalRelationAction(formdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_EXTERNAL_ADDRESS_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createMailingListExternalValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -61,11 +49,6 @@ export async function createMailingListExternalRelationAction(formdata: FormData export async function createMailingListUserRelationAction(formdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_USER_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createMailingListUserValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -74,11 +57,6 @@ export async function createMailingListUserRelationAction(formdata: FormData): export async function createMailingListGroupRelationAction(formdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_GROUP_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createMailingListGroupValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -87,11 +65,6 @@ export async function createMailingListGroupRelationAction(formdata: FormData): export async function destroyAliasMailingListRelationAction(formdata: FormData | CreateAliasMailingListType['Type']): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_ALIAS_DESTROY']] - }) - if (!authorized) return createActionError(status) - const parse = createAliasMailingListValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -100,11 +73,6 @@ export async function destroyAliasMailingListRelationAction(formdata: FormData | export async function destroyMailingListExternalRelationAction(formdata: FormData | CreateMailingListExternalType['Type']): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_EXTERNAL_ADDRESS_DESTROY']] - }) - if (!authorized) return createActionError(status) - const parse = createMailingListExternalValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -113,11 +81,6 @@ export async function destroyMailingListExternalRelationAction(formdata: FormDat export async function destroyMailingListUserRelationAction(formdata: FormData | CreateMailingListUserType['Type']): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_USER_DESTROY']], - }) - if (!authorized) return createActionError(status) - const parse = createMailingListUserValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -126,11 +89,6 @@ export async function destroyMailingListUserRelationAction(formdata: FormData | export async function destroyMailingListGroupRelationAction(formdata: FormData | CreateMailingListGroupType['Type']): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_GROUP_DESTROY']] - }) - if (!authorized) return createActionError(status) - const parse = createMailingListGroupValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) @@ -138,17 +96,6 @@ export async function destroyMailingListGroupRelationAction(formdata: FormData | } export async function readMailFlowAction(filter: MailListTypes, id: number) { - const { authorized, status } = await getUser({ - requiredPermissions: [ - ['MAILINGLIST_READ'], - ['MAILALIAS_READ'], - ['MAILADDRESS_EXTERNAL_READ'], - ['GROUP_READ'], - ], - }) - - if (!authorized) return createActionError(status) - return safeServerCall(() => readMailTraversal({ filter, id, @@ -161,15 +108,6 @@ export async function readMailOptions(): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [ - ['MAILINGLIST_READ'], - ['MAILALIAS_READ'], - ['MAILADDRESS_EXTERNAL_READ'], - ], - }) - if (!authorized) return createActionError(status) - return await safeServerCall(async () => { const results = await Promise.all([ readMailAliases(), diff --git a/src/services/mail/alias/actions.ts b/src/services/mail/alias/actions.ts index a06cc7ff5..e78a86eb3 100644 --- a/src/services/mail/alias/actions.ts +++ b/src/services/mail/alias/actions.ts @@ -1,7 +1,6 @@ 'use server' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createMailAlias } from '@/services/mail/alias/create' import { destroyMailAlias } from '@/services/mail/alias/destroy' import { readMailAliases } from '@/services/mail/alias/read' @@ -16,11 +15,6 @@ import type { MailAlias } from '@prisma/client' export async function createMailAliasAction(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILALIAS_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createMailAliasValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) @@ -28,11 +22,6 @@ export async function createMailAliasAction(rawdata: FormData): } export async function destroyMailAliasAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILALIAS_DESTORY']], - }) - if (!authorized) return createActionError(status) - const parse = destoryMailAliasValidation.typeValidate({ id }) if (!parse.success) return createZodActionError(parse) @@ -40,20 +29,10 @@ export async function destroyMailAliasAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILALIAS_READ']] - }) - if (!authorized) return createActionError(status) - return safeServerCall(() => readMailAliases()) } export async function updateMailAliasAction(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILALIAS_UPDATE']], - }) - if (!authorized) return createActionError(status) - const parsed = updateMailAliasValidation.typeValidate(rawdata) if (!parsed.success) return createZodActionError(parsed) diff --git a/src/services/mail/alias/auth.ts b/src/services/mail/alias/auth.ts new file mode 100644 index 000000000..a27bfe42d --- /dev/null +++ b/src/services/mail/alias/auth.ts @@ -0,0 +1,9 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +//TODO: Use authers when refactoring to operations.ts +export const mailAliasAuth = { + create: RequirePermission.staticFields({ permission: 'MAILALIAS_CREATE' }), + destroy: RequirePermission.staticFields({ permission: 'MAILALIAS_DESTROY' }), + read: RequirePermission.staticFields({ permission: 'MAILALIAS_READ' }), + update: RequirePermission.staticFields({ permission: 'MAILALIAS_UPDATE' }), +} as const diff --git a/src/services/mail/auth.ts b/src/services/mail/auth.ts new file mode 100644 index 000000000..9f81978ee --- /dev/null +++ b/src/services/mail/auth.ts @@ -0,0 +1,21 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' +import { RequireEveryPermission } from '@/auth/auther/RequireEveryPermission' + +export const mailAuth = { + createAliasMailingListRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_ALIAS_CREATE' }), + createMailingListExternalRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_EXTERNAL_ADDRESS_CREATE' }), + createMailingListUserRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_USER_CREATE' }), + createMailingListGroupRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_GROUP_CREATE' }), + destroyAliasMailingListRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_ALIAS_DESTROY' }), + destroyMailingListExternalRelation: RequirePermission.staticFields({ + permission: 'MAILINGLIST_EXTERNAL_ADDRESS_DESTROY' + }), + destroyMailingListUserRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_USER_DESTROY' }), + destroyMailingListGroupRelation: RequirePermission.staticFields({ permission: 'MAILINGLIST_GROUP_DESTROY' }), + readMailFlow: RequireEveryPermission.staticFields({ + permissions: ['MAILINGLIST_READ', 'MAILALIAS_READ', 'MAILADDRESS_EXTERNAL_READ', 'GROUP_READ'] + }), + readMailOptions: RequireEveryPermission.staticFields({ + permissions: ['MAILINGLIST_READ', 'MAILALIAS_READ', 'MAILADDRESS_EXTERNAL_READ'] + }) +} as const diff --git a/src/services/mail/list/actions.ts b/src/services/mail/list/actions.ts index 3a142809a..de47b7428 100644 --- a/src/services/mail/list/actions.ts +++ b/src/services/mail/list/actions.ts @@ -1,7 +1,6 @@ 'use server' -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createMailingList } from '@/services/mail/list/create' import { destroyMailingList } from '@/services/mail/list/destroy' import { updateMailingList } from '@/services/mail/list/update' @@ -15,11 +14,6 @@ import type { MailingList } from '@prisma/client' export async function createMailingListAction(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createMailingListValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) @@ -27,11 +21,6 @@ export async function createMailingListAction(rawdata: FormData): } export async function destroyMailingListAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_DESTROY']], - }) - if (!authorized) return createActionError(status) - const parse = readMailingListValidation.typeValidate({ id }) if (!parse.success) return createZodActionError(parse) @@ -40,11 +29,6 @@ export async function destroyMailingListAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILINGLIST_UPDATE']], - }) - if (!authorized) return createActionError(status) - const parse = updateMailingListValidation.typeValidate(data) if (!parse.success) return createZodActionError(parse) diff --git a/src/services/mail/list/auth.ts b/src/services/mail/list/auth.ts new file mode 100644 index 000000000..f0559905b --- /dev/null +++ b/src/services/mail/list/auth.ts @@ -0,0 +1,9 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +//TODO: Use authers when refactoring to operations.ts +export const mailingListAuth = { + create: RequirePermission.staticFields({ permission: 'MAILINGLIST_CREATE' }), + destroy: RequirePermission.staticFields({ permission: 'MAILINGLIST_DESTROY' }), + read: RequirePermission.staticFields({ permission: 'MAILINGLIST_READ' }), + update: RequirePermission.staticFields({ permission: 'MAILINGLIST_UPDATE' }), +} as const diff --git a/src/services/mail/mailAddressExternal/actions.ts b/src/services/mail/mailAddressExternal/actions.ts index 6a56c49dd..249a6a4ab 100644 --- a/src/services/mail/mailAddressExternal/actions.ts +++ b/src/services/mail/mailAddressExternal/actions.ts @@ -1,7 +1,5 @@ 'use server' - -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { getUser } from '@/auth/session/getUser' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createMailAddressExternal } from '@/services/mail/mailAddressExternal/create' import { destroyMailAddressExternal } from '@/services/mail/mailAddressExternal/destroy' import { updateMailAddressExternal } from '@/services/mail/mailAddressExternal/update' @@ -15,11 +13,6 @@ import type { MailAddressExternal } from '@prisma/client' export async function createMailAddressExternalAction(rawdata: FormData): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILADDRESS_EXTERNAL_CREATE']] - }) - if (!authorized) return createActionError(status) - const parse = createMailAddressExternalValidation.typeValidate(rawdata) if (!parse.success) return createZodActionError(parse) @@ -27,11 +20,6 @@ export async function createMailAddressExternalAction(rawdata: FormData): } export async function destroyMailAddressExternalAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILADDRESS_EXTERNAL_DESTROY']], - }) - if (!authorized) return createActionError(status) - const parse = readMailAddressExternalValidation.typeValidate({ id }) if (!parse.success) return createZodActionError(parse) @@ -40,11 +28,6 @@ export async function destroyMailAddressExternalAction(id: number): Promise> { - const { authorized, status } = await getUser({ - requiredPermissions: [['MAILADDRESS_EXTERNAL_UPDATE']], - }) - if (!authorized) return createActionError(status) - const parse = updateMailAddressExternalValidation.typeValidate(data) if (!parse.success) return createZodActionError(parse) diff --git a/src/services/mail/mailAddressExternal/auth.ts b/src/services/mail/mailAddressExternal/auth.ts new file mode 100644 index 000000000..1f9cb720d --- /dev/null +++ b/src/services/mail/mailAddressExternal/auth.ts @@ -0,0 +1,9 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +//TODO: Use authers when refactoring to operations.ts +export const mailAddressExternalAuth = { + create: RequirePermission.staticFields({ permission: 'MAILADDRESS_EXTERNAL_CREATE' }), + destroy: RequirePermission.staticFields({ permission: 'MAILADDRESS_EXTERNAL_DESTROY' }), + read: RequirePermission.staticFields({ permission: 'MAILADDRESS_EXTERNAL_READ' }), + update: RequirePermission.staticFields({ permission: 'MAILADDRESS_EXTERNAL_UPDATE' }), +} as const diff --git a/src/services/notifications/channel/schemas.ts b/src/services/notifications/channel/schemas.ts index ee178aa1d..a0d06b1f1 100644 --- a/src/services/notifications/channel/schemas.ts +++ b/src/services/notifications/channel/schemas.ts @@ -8,6 +8,7 @@ import type { NotificationMethodTypes } from '@/services/notifications/types' +//TODO: This is unused ?? export function parseMethods(data: FormData, prefix?: NotificationMethodTypes) { return Object.fromEntries( notificationMethodsArray.filter(method => notificationMethodsArray.includes(method)).map(method => { diff --git a/src/services/permissions/constants.ts b/src/services/permissions/constants.ts index 51abb9070..60394e6d5 100644 --- a/src/services/permissions/constants.ts +++ b/src/services/permissions/constants.ts @@ -302,7 +302,7 @@ export const permissionConfig = { description: 'kan lage epostalias', category: 'mail', }, - MAILALIAS_DESTORY: { + MAILALIAS_DESTROY: { name: 'Slette epostalias', description: 'kan slette epostalias', category: 'mail', From c254efcb44a2d9527f6686b5e9e0c250699667ee Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:54:10 +0100 Subject: [PATCH 6/9] feat: useUser -> useSession useEditing -> useEditMode --- src/app/(auth)/register-email/page.tsx | 4 +- src/app/(auth)/verify-email/page.tsx | 2 +- .../_components/Cms/Article/AddSection.tsx | 6 +- .../_components/Cms/Article/SectionMover.tsx | 8 +- .../AddPartToArticleSection.tsx | 9 +- .../Cms/ArticleSection/ImageControls.tsx | 8 +- .../Cms/ArticleSection/RemovePart.tsx | 8 +- .../Cms/CmsImage/CmsImageEditor.tsx | 7 +- .../_components/Cms/CmsLink/CmsLinkEditor.tsx | 8 +- .../Cms/CmsParagraph/CmsParagraphEditor.tsx | 12 +- src/app/_components/Company/CompanyList.tsx | 12 +- .../EditableTextField/EditableTextField.tsx | 8 +- .../OmegaId/identification/OmegaIdElement.tsx | 8 +- src/app/admin/dots/CreateDotForm.tsx | 7 +- .../[filter]/[id]/(editComponents)/group.tsx | 7 +- .../(editComponents)/mailAddressExternal.tsx | 6 +- .../[id]/(editComponents)/mailAlias.tsx | 9 +- .../[id]/(editComponents)/mailingList.tsx | 7 +- .../[filter]/[id]/(editComponents)/user.tsx | 8 +- src/app/admin/mail/[filter]/[id]/MailFlow.tsx | 6 +- src/app/admin/study-programmes/page.tsx | 10 +- src/app/cabin/book/stateWrapper.tsx | 56 ++++--- .../career/jobads/[nameAndId]/EditJobAd.tsx | 8 +- .../collections/[id]/CollectionAdmin.tsx | 8 +- src/app/layout.tsx | 2 +- src/app/lockers/[id]/page.tsx | 10 +- src/app/news/[nameAndId]/EditNews.tsx | 7 +- src/app/ombul/[...yearAndName]/ChangeName.tsx | 2 +- src/app/ombul/[...yearAndName]/OmbulAdmin.tsx | 27 +++- src/app/ombul/[...yearAndName]/page.tsx | 4 +- src/auth/auther/Auther.ts | 4 +- src/auth/session/Session.ts | 4 + src/auth/session/useSession.ts | 27 ++++ src/auth/session/useUser.ts | 152 ------------------ src/contexts/DefaultPermissions.tsx | 2 +- src/hooks/useAuther.ts | 33 ++++ src/hooks/useEditing.ts | 59 ------- src/hooks/useEditmode.ts | 34 ++++ src/lib/checkMatrix.ts | 14 -- src/services/groups/committees/operations.ts | 4 +- src/services/groups/studyProgrammes/auth.ts | 9 ++ src/services/notifications/operations.ts | 4 +- src/services/ombul/operations.ts | 4 +- src/services/omegaid/actions.ts | 2 +- src/services/omegaquotes/operations.ts | 8 +- src/services/serviceOperation.ts | 4 +- 46 files changed, 299 insertions(+), 349 deletions(-) create mode 100644 src/auth/session/useSession.ts delete mode 100644 src/auth/session/useUser.ts create mode 100644 src/hooks/useAuther.ts delete mode 100644 src/hooks/useEditing.ts create mode 100644 src/hooks/useEditmode.ts delete mode 100644 src/lib/checkMatrix.ts create mode 100644 src/services/groups/studyProgrammes/auth.ts diff --git a/src/app/(auth)/register-email/page.tsx b/src/app/(auth)/register-email/page.tsx index 69e17f192..d2877ca5f 100644 --- a/src/app/(auth)/register-email/page.tsx +++ b/src/app/(auth)/register-email/page.tsx @@ -1,8 +1,8 @@ -import { RequireUser } from '@/auth/auther/RequireUser' import EmailRegistrationForm from './EmailregistrationForm' +import { RequireUser } from '@/auth/auther/RequireUser' import { readUserAction } from '@/services/users/actions' -import { notFound, redirect } from 'next/navigation' import { Session } from '@/auth/session/Session' +import { notFound, redirect } from 'next/navigation' export default async function Registeremail() { const { authorized, session } = RequireUser.staticFields({}).dynamicFields({}).auth(await Session.fromNextAuth()) diff --git a/src/app/(auth)/verify-email/page.tsx b/src/app/(auth)/verify-email/page.tsx index d0334f0a6..06d1e1e38 100644 --- a/src/app/(auth)/verify-email/page.tsx +++ b/src/app/(auth)/verify-email/page.tsx @@ -2,10 +2,10 @@ import { EmailVerifiedWrapper } from './EmailVerifiedWrapper' import { QueryParams } from '@/lib/queryParams/queryParams' import { unwrapActionReturn } from '@/app/redirectToErrorPage' import { verifyEmailAction } from '@/services/auth/actions' +import { Session } from '@/auth/session/Session' import { notFound, redirect } from 'next/navigation' import Link from 'next/link' import type { SearchParamsServerSide } from '@/lib/queryParams/types' -import { Session } from '@/auth/session/Session' type PropTypes = SearchParamsServerSide diff --git a/src/app/_components/Cms/Article/AddSection.tsx b/src/app/_components/Cms/Article/AddSection.tsx index 10bd011b4..42f0b39dd 100644 --- a/src/app/_components/Cms/Article/AddSection.tsx +++ b/src/app/_components/Cms/Article/AddSection.tsx @@ -3,7 +3,8 @@ import styles from './AddSection.module.scss' import AddParts from '@/cms/AddParts' import { maxSections } from '@/cms/articles/constants' -import useEditing from '@/hooks/useEditing' +import useEditMode from '@/hooks/useEditmode' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { useRouter } from 'next/navigation' import type { ArticleSectionPart } from '@/cms/articleSections/types' import type { AddSectionToArticleAction } from '@/cms/articles/types' @@ -19,7 +20,8 @@ export default function AddSection({ addSectionToArticleAction }: PropTypes) { const { refresh } = useRouter() - const canEdit = useEditing({}) //TODO: check visibility of article for user and pass it to useEditing + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ auther: RequireNothing.staticFields({}).dynamicFields({}) }) const handleAdd = async (includePart: ArticleSectionPart) => { await addSectionToArticleAction({ diff --git a/src/app/_components/Cms/Article/SectionMover.tsx b/src/app/_components/Cms/Article/SectionMover.tsx index 639a5f3ab..c67d0f937 100644 --- a/src/app/_components/Cms/Article/SectionMover.tsx +++ b/src/app/_components/Cms/Article/SectionMover.tsx @@ -1,6 +1,7 @@ 'use client' import styles from './SectionMover.module.scss' -import useEditing from '@/hooks/useEditing' +import useEditMode from '@/hooks/useEditmode' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons' import { useCallback } from 'react' @@ -21,7 +22,10 @@ export default function SectionMover({ showDown, reorderArticleSectionsAction }: PropTypes) { - const canEdit = useEditing({}) //TODO: check visibility of section for user and pass it to useEditing + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const { refresh } = useRouter() const handleMove = useCallback(async (direction: 'UP' | 'DOWN') => { await reorderArticleSectionsAction({ data: { direction } }) diff --git a/src/app/_components/Cms/ArticleSection/AddPartToArticleSection.tsx b/src/app/_components/Cms/ArticleSection/AddPartToArticleSection.tsx index 26529b267..7264c4464 100644 --- a/src/app/_components/Cms/ArticleSection/AddPartToArticleSection.tsx +++ b/src/app/_components/Cms/ArticleSection/AddPartToArticleSection.tsx @@ -1,7 +1,8 @@ 'use client' import styles from './AddPartToArticleSection.module.scss' import AddParts from '@/cms/AddParts' -import useEditing from '@/hooks/useEditing' +import useEditMode from '@/hooks/useEditmode' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { useCallback } from 'react' import { useRouter } from 'next/navigation' import type { PropTypes as AddPartsPropTypes } from '@/cms/AddParts' @@ -23,8 +24,10 @@ export default function AddPartToArticleSection({ ...props }: PropTypes) { const { refresh } = useRouter() - const canEdit = useEditing({}) //TODO: check visibility of article for user and pass it to useEditing - + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const handleAdd = useCallback(async (part: ArticleSectionPart) => { await addPartToArticleSectionAction({ data: { part } }) refresh() diff --git a/src/app/_components/Cms/ArticleSection/ImageControls.tsx b/src/app/_components/Cms/ArticleSection/ImageControls.tsx index 46543095a..09e9b8f4e 100644 --- a/src/app/_components/Cms/ArticleSection/ImageControls.tsx +++ b/src/app/_components/Cms/ArticleSection/ImageControls.tsx @@ -1,7 +1,8 @@ 'use client' import styles from './ImageControls.module.scss' +import useEditMode from '@/hooks/useEditmode' import { imageSizeIncrement, maxImageSize, minImageSize } from '@/cms/articleSections/constants' -import useEditing from '@/hooks/useEditing' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronLeft, @@ -24,7 +25,10 @@ type PropTypes = { * i.e move it left or right and size it */ export default function ImageControls({ articleSection, className, updateArticleSectionAction }: PropTypes) { - const canEdit = useEditing({}) //TODO: check visibility of article for user and pass it to useEditing + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const { refresh } = useRouter() if (!canEdit) return null diff --git a/src/app/_components/Cms/ArticleSection/RemovePart.tsx b/src/app/_components/Cms/ArticleSection/RemovePart.tsx index d2198be3c..e58ac7cbe 100644 --- a/src/app/_components/Cms/ArticleSection/RemovePart.tsx +++ b/src/app/_components/Cms/ArticleSection/RemovePart.tsx @@ -1,8 +1,9 @@ 'use client' import styles from './RemovePart.module.scss' import Form from '@/components/Form/Form' +import useEditMode from '@/hooks/useEditmode' import useClickOutsideRef from '@/hooks/useClickOutsideRef' -import useEditing from '@/hooks/useEditing' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { useRouter } from 'next/navigation' import { useState } from 'react' import { faX } from '@fortawesome/free-solid-svg-icons' @@ -17,7 +18,10 @@ type PropTypes = { export default function RemovePart({ part, removePartFromArticleSectionAction }: PropTypes) { const { refresh } = useRouter() - const canEdit = useEditing({}) + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const [confirmOpen, setConfirmOpen] = useState(false) const confirmRef = useClickOutsideRef(() => setConfirmOpen(false)) if (!canEdit) return null diff --git a/src/app/_components/Cms/CmsImage/CmsImageEditor.tsx b/src/app/_components/Cms/CmsImage/CmsImageEditor.tsx index 22191ddd4..9227e736d 100644 --- a/src/app/_components/Cms/CmsImage/CmsImageEditor.tsx +++ b/src/app/_components/Cms/CmsImage/CmsImageEditor.tsx @@ -8,10 +8,11 @@ import EndlessScroll from '@/components/PagingWrappers/EndlessScroll' import CollectionCard from '@/components/Image/Collection/CollectionCard' import ImageList from '@/components/Image/ImageList/ImageList' import { ImageCollectionPagingProvider, ImageCollectionPagingContext } from '@/contexts/paging/ImageCollectionPaging' +import useEditMode from '@/hooks/useEditmode' import { ImagePagingProvider } from '@/contexts/paging/ImagePaging' import PopUpProvider from '@/contexts/PopUp' +import { RequireNothing } from '@/auth/auther/RequireNothing' import ImageSelectionProvider from '@/contexts/ImageSelection' -import useEditing from '@/hooks/useEditing' import { useState } from 'react' import Link from 'next/link' import type { CmsImage, Image as ImageT } from '@prisma/client' @@ -30,7 +31,9 @@ type PropTypes = { * @returns */ export default function CmsImageEditor({ cmsImage, updateCmsImageAction }: PropTypes) { - const canEdit = useEditing({}) + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const [currentCollectionId, setCurrentCollectionId] = useState(cmsImage.image.collectionId) const isCollectionActive = (collection: { id: number }) => ( diff --git a/src/app/_components/Cms/CmsLink/CmsLinkEditor.tsx b/src/app/_components/Cms/CmsLink/CmsLinkEditor.tsx index 5a5abba9e..af4c27359 100644 --- a/src/app/_components/Cms/CmsLink/CmsLinkEditor.tsx +++ b/src/app/_components/Cms/CmsLink/CmsLinkEditor.tsx @@ -2,10 +2,11 @@ import styles from './CmsLinkEditor.module.scss' import TextInput from '@/components/UI/TextInput' import EditOverlay from '@/cms/EditOverlay' +import useEditMode from '@/hooks/useEditmode' import Form from '@/components/Form/Form' import PopUp from '@/components/PopUp/PopUp' -import useEditing from '@/hooks/useEditing' import { configureAction } from '@/services/configureAction' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { useRouter } from 'next/navigation' import type { CmsLink } from '@prisma/client' import type { UpdateCmsLinkAction } from '@/cms/links/types' @@ -16,7 +17,10 @@ type PropTypes = { } export default function CmsLinkEditor({ cmsLink, updateCmsLinkAction }: PropTypes) { - const canEdit = useEditing({}) //TODO: check visibility of cmsLink for user and pass it to useEditing + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const { refresh } = useRouter() if (!canEdit) return null diff --git a/src/app/_components/Cms/CmsParagraph/CmsParagraphEditor.tsx b/src/app/_components/Cms/CmsParagraph/CmsParagraphEditor.tsx index ba7e42399..87989ccc6 100644 --- a/src/app/_components/Cms/CmsParagraph/CmsParagraphEditor.tsx +++ b/src/app/_components/Cms/CmsParagraph/CmsParagraphEditor.tsx @@ -1,14 +1,15 @@ 'use client' +import 'easymde/dist/easymde.min.css' +import './CustomEditorClasses.scss' import styles from './CmsParagraphEditor.module.scss' import EditOverlay from '@/components/Cms/EditOverlay' import Form from '@/components/Form/Form' import PopUp from '@/components/PopUp/PopUp' -import useEditing from '@/hooks/useEditing' +import { RequireNothing } from '@/auth/auther/RequireNothing' import { configureAction } from '@/services/configureAction' +import useEditMode from '@/hooks/useEditmode' import { useState } from 'react' import { useRouter } from 'next/navigation' -import 'easymde/dist/easymde.min.css' -import './CustomEditorClasses.scss' import dynamic from 'next/dynamic' import type { CmsParagraph } from '@prisma/client' import type { UpdateCmsParagraphAction } from '@/cms/paragraphs/types' @@ -29,7 +30,10 @@ type PropTypes = { } export default function CmsParagraphEditor({ cmsParagraph, editorClassName, updateCmsParagraphAction }: PropTypes) { - const canEdit = useEditing({}) //TODO: pass visibility / permissions to useEditing + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const { refresh } = useRouter() const [content, setContent] = useState(cmsParagraph.contentMd) diff --git a/src/app/_components/Company/CompanyList.tsx b/src/app/_components/Company/CompanyList.tsx index 58d47bac4..1b560aa6d 100644 --- a/src/app/_components/Company/CompanyList.tsx +++ b/src/app/_components/Company/CompanyList.tsx @@ -3,7 +3,7 @@ import { companyListRenderer } from './CompanyListRenderer' import styles from './CompanyList.module.scss' import { CompanyPagingContext } from '@/contexts/paging/CompanyPaging' import EndlessScroll from '@/components/PagingWrappers/EndlessScroll' -import { useUser } from '@/auth/session/useUser' +import { useSession } from '@/auth/session/useSession' import type { ReactNode } from 'react' type PropTypes = { @@ -12,18 +12,16 @@ type PropTypes = { } export default function CompanyList({ serverRenderedData, disableEditing }: PropTypes) { - const ses = useUser() - const session = { - user: ses.user ?? null, - permissions: ses.permissions ?? [], - memberships: ses.memberships ?? [], + const session = useSession() + if (session.loading) { + return <>laster session... } return (
{serverRenderedData} companyListRenderer({ asClient: true, session, disableEditing })(data)} + renderer={data => companyListRenderer({ asClient: true, session: session.session, disableEditing })(data)} />
) diff --git a/src/app/_components/EditableTextField/EditableTextField.tsx b/src/app/_components/EditableTextField/EditableTextField.tsx index 6ccc9b9ae..416d86973 100644 --- a/src/app/_components/EditableTextField/EditableTextField.tsx +++ b/src/app/_components/EditableTextField/EditableTextField.tsx @@ -1,8 +1,9 @@ 'use client' import styles from './EditableTextField.module.scss' import Form from '@/components/Form/Form' +import useEditMode from '@/hooks/useEditmode' +import { RequireNothing } from '@/auth/auther/RequireNothing' import useKeyPress from '@/hooks/useKeyPress' -import useEditing from '@/hooks/useEditing' import React, { useEffect, useState, useRef } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPencil } from '@fortawesome/free-solid-svg-icons' @@ -38,7 +39,10 @@ export default function EditableTextField({ }: PropTypes) { const [value, setValue] = useState('') const [noChange, setNoChange] = useState(true) - const canEdit = useEditing({}) //TODO: auth must be passed + //TODO: Auther must be passed in.... + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const ref = useRef(null) const submitRef = useRef(null) useKeyPress('Enter', () => { diff --git a/src/app/_components/OmegaId/identification/OmegaIdElement.tsx b/src/app/_components/OmegaId/identification/OmegaIdElement.tsx index e5a72ef1e..33c772cdd 100644 --- a/src/app/_components/OmegaId/identification/OmegaIdElement.tsx +++ b/src/app/_components/OmegaId/identification/OmegaIdElement.tsx @@ -1,9 +1,9 @@ 'use client' import styles from './OmegaIdElement.module.scss' -import { useUser } from '@/auth/session/useUser' import { generateOmegaIdAction } from '@/services/omegaid/actions' import { readJWTPayload } from '@/jwt/jwtReadUnsecure' import { compressOmegaId } from '@/services/omegaid/compress' +import { useSession } from '@/auth/session/useSession' import { useQRCode } from 'next-qrcode' import { useEffect, useState } from 'react' @@ -14,7 +14,7 @@ export default function OmegaIdElement({ token }: { }) { const [tokenState, setTokenState] = useState(token) - const { user } = useUser() + const session = useSession() const { SVG } = useQRCode() @@ -42,7 +42,9 @@ export default function OmegaIdElement({ token }: { return () => clearInterval(interval) }) - if (!user) return

Could not load OmegaID, since the user is not loggedin.

+ if (session.loading) return

Loading...

+ if (!session.session.user) return

Could not load OmegaID, since the user is not loggedin.

+ const user = session.session.user return

Omega ID

diff --git a/src/app/admin/dots/CreateDotForm.tsx b/src/app/admin/dots/CreateDotForm.tsx index b12757d06..6bce055d1 100644 --- a/src/app/admin/dots/CreateDotForm.tsx +++ b/src/app/admin/dots/CreateDotForm.tsx @@ -6,19 +6,20 @@ import PopUp from '@/components/PopUp/PopUp' import NumberInput from '@/components/UI/NumberInput' import TextInput from '@/components/UI/TextInput' import UserList from '@/components/User/UserList/UserList' -import { useUser } from '@/auth/session/useUser' import { PopUpContext } from '@/contexts/PopUp' import { UserSelectionContext } from '@/contexts/UserSelection' import { configureAction } from '@/services/configureAction' import { useContext } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPlus } from '@fortawesome/free-solid-svg-icons' +import { useSession } from 'next-auth/react' export default function CreateDotForm() { - const session = useUser() + const session = useSession() const userSelectionContext = useContext(UserSelectionContext) const popUpContext = useContext(PopUpContext) - if (!session.user) return <> + if (session.loading) return <>loading... + if (!session.session.user) return <> if (!userSelectionContext) return <> userSelectionContext.onSelection(() => { diff --git a/src/app/admin/mail/[filter]/[id]/(editComponents)/group.tsx b/src/app/admin/mail/[filter]/[id]/(editComponents)/group.tsx index cb1b3f3c7..38e3baf65 100644 --- a/src/app/admin/mail/[filter]/[id]/(editComponents)/group.tsx +++ b/src/app/admin/mail/[filter]/[id]/(editComponents)/group.tsx @@ -1,8 +1,8 @@ 'use client' +import { useSession } from '@/auth/session/useSession' import Form from '@/components/Form/Form' import { SelectNumber } from '@/components/UI/Select' import { createMailingListGroupRelationAction } from '@/services/mail/actions' -import { useUser } from '@/auth/session/useUser' import type { MailFlowObject } from '@/services/mail/types' import type { MailingList } from '@prisma/client' @@ -19,9 +19,8 @@ export default function EditGroup({ if (!focusedGroup) { throw Error('Could not find group') } - - const uResults = useUser() - const permissions = uResults.permissions ?? [] + const session = useSession() + const permissions = !session.loading ? session.session.permissions : [] return

{focusedGroup.id}

diff --git a/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAddressExternal.tsx b/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAddressExternal.tsx index fad80f65d..4e75dfefe 100644 --- a/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAddressExternal.tsx +++ b/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAddressExternal.tsx @@ -4,11 +4,11 @@ import TextInput from '@/components/UI/TextInput' import Form from '@/components/Form/Form' import { SelectNumber } from '@/components/UI/Select' import { createMailingListExternalRelationAction } from '@/services/mail/actions' -import { useUser } from '@/auth/session/useUser' import { updateMailAddressExternalAction, destroyMailAddressExternalAction } from '@/services/mail/mailAddressExternal/actions' +import { useSession } from '@/auth/session/useSession' import { useRouter } from 'next/navigation' import type { MailingList } from '@prisma/client' import type { MailFlowObject } from '@/services/mail/types' @@ -29,8 +29,8 @@ export default function EditMailAddressExternal({ throw Error('Could not find alias') } - const uResults = useUser() - const permissions = uResults.permissions ?? [] + const session = useSession() + const permissions = !session.loading ? session.session.permissions : [] return <>

{focusedAddress.address}

diff --git a/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx b/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx index 13e98137d..2b8ce8eaa 100644 --- a/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx +++ b/src/app/admin/mail/[filter]/[id]/(editComponents)/mailAlias.tsx @@ -1,16 +1,14 @@ 'use client' - import TextInput from '@/components/UI/TextInput' import Form from '@/components/Form/Form' import { SelectNumber } from '@/components/UI/Select' import { createAliasMailingListRelationAction } from '@/services/mail/actions' -import { useUser } from '@/auth/session/useUser' import { updateMailAliasAction, destroyMailAliasAction } from '@/services/mail/alias/actions' +import { useSession } from '@/auth/session/useSession' import { useRouter } from 'next/navigation' import type { MailFlowObject } from '@/services/mail/types' import type { MailingList } from '@prisma/client' - export default function EditMailAlias({ data, mailingLists @@ -26,11 +24,12 @@ export default function EditMailAlias({ throw Error('Could not find alias') } - const uResults = useUser() - const permissions = uResults.permissions ?? [] + const session = useSession() + const permissions = !session.loading ? session.session.permissions : [] return <>

{focusedAlias.address}

+ { /** TODO: Call auther */ } { permissions.includes('MAILALIAS_UPDATE') &&

{focusedMailingList.name}

+ {/** TODO: Call author */} { permissions.includes('MAILINGLIST_UPDATE') &&

{`${focusedUser.firstname} ${focusedUser.lastname}`}

+ {/** TODO: Call author */} { permissions.includes('MAILINGLIST_USER_CREATE') && @@ -38,7 +40,7 @@ export default async function StudyProgrammes() { Del av Omega - + } diff --git a/src/app/cabin/book/stateWrapper.tsx b/src/app/cabin/book/stateWrapper.tsx index bea9db687..ead708744 100644 --- a/src/app/cabin/book/stateWrapper.tsx +++ b/src/app/cabin/book/stateWrapper.tsx @@ -1,14 +1,12 @@ 'use client' - import CabinCalendar from './CabinCalendar' import CabinPriceCalculator from './CabinPriceCalculator' import SelectBedProducts from './SelectBedProduct' -import RadioLarge from '@/app/_components/UI/RadioLarge' -import Form from '@/app/_components/Form/Form' -import TextInput from '@/app/_components/UI/TextInput' -import NumberInput from '@/app/_components/UI/NumberInput' -import Checkbox from '@/app/_components/UI/Checkbox' -import { useUser } from '@/auth/session/useUser' +import RadioLarge from '@/components/UI/RadioLarge' +import Form from '@/components/Form/Form' +import TextInput from '@/components/UI/TextInput' +import NumberInput from '@/components/UI/NumberInput' +import Checkbox from '@/components/UI/Checkbox' import { createBedBookingNoUserAction, createBedBookingUserAttachedAction, @@ -17,6 +15,7 @@ import { } from '@/services/cabin/actions' import { getZodDateString } from '@/lib/dates/formatting' import { configureAction } from '@/services/configureAction' +import { useSession } from '@/auth/session/useSession' import { useMemo, useState } from 'react' import type { CabinProductExtended } from '@/services/cabin/product/constants' import type { BookingFiltered } from '@/services/cabin/booking/types' @@ -58,7 +57,7 @@ export default function StateWrapper({ const [numberOfMembers, setNumberOfMembers] = useState(0) const [numberOfNonMembers, setNumberOfNonMembers] = useState(0) - const user = useUser() + const session = useSession() const calendar = useMemo(() => ( Du kan ikke booke hytta. } + if (session.loading) { + return <>Laster session... + } + const canChangeBookingType = canBookCabin && canBookBed + const user = session.session.user + function submitFormAction() { + if (session.loading) { + throw new Error('Session is still loading') + } if (!cabinProduct) { throw new Error('Could not find the cabin product.') } - if (!user.user) { + if (!user) { if (bookingType === 'CABIN') { return configureAction(createCabinBookingNoUserAction, { params: { @@ -118,7 +126,7 @@ export default function StateWrapper({ if (bookingType === 'CABIN') { return configureAction(createCabinBookingUserAttachedAction, { params: { - userId: user.user?.id ?? -1, + userId: user?.id ?? -1, bookingProducts: [{ cabinProductId: cabinProduct.id, quantity: 1, @@ -129,7 +137,7 @@ export default function StateWrapper({ return configureAction(createBedBookingUserAttachedAction, { params: { - userId: user.user?.id ?? -1, + userId: user?.id ?? -1, bookingProducts: bedProducts.map((product, index) => ({ cabinProductId: product.id, quantity: bedAmounts[index], @@ -175,7 +183,7 @@ export default function StateWrapper({ } - {(bookingType === 'CABIN' && user.user) ? <> + {(bookingType === 'CABIN' && user) ? <> diff --git a/src/app/career/jobads/[nameAndId]/EditJobAd.tsx b/src/app/career/jobads/[nameAndId]/EditJobAd.tsx index 6f8162f92..390af9922 100644 --- a/src/app/career/jobads/[nameAndId]/EditJobAd.tsx +++ b/src/app/career/jobads/[nameAndId]/EditJobAd.tsx @@ -4,7 +4,6 @@ import SelectedCompany from '@/career/jobads/SelectedCompany' import Form from '@/components/Form/Form' import TextInput from '@/components/UI/TextInput' import Textarea from '@/components/UI/Textarea' -import useEditing from '@/hooks/useEditing' import { SelectString } from '@/components/UI/Select' import DateInput from '@/components/UI/DateInput' import Slider from '@/app/_components/UI/Slider' @@ -12,6 +11,8 @@ import { CompanyPagingContext } from '@/contexts/paging/CompanyPaging' import CompanyChooser from '@/app/career/jobads/CompanyChooser' import { destroyJobAdAction, updateJobAdAction } from '@/career/jobAds/actions' import { jobAdOptions } from '@/services/career/jobAds/constants' +import useEditMode from '@/hooks/useEditmode' +import { jobAdAuth } from '@/services/career/jobAds/auth' import { configureAction } from '@/services/configureAction' import { formatVevenUri } from '@/lib/urlEncoding' import { v4 as uuid } from 'uuid' @@ -30,8 +31,9 @@ type PropTypes = { * @param children - children to render if editmode is off */ export default function EditJobAd({ jobAd, children }: PropTypes) { - //TODO: chack visibility - const canEdit = useEditing({}) + const canEdit = useEditMode({ + auther: jobAdAuth.update.dynamicFields({}) + }) const companyPagingCtx = useContext(CompanyPagingContext) if (!canEdit) return children if (!companyPagingCtx) { diff --git a/src/app/images/collections/[id]/CollectionAdmin.tsx b/src/app/images/collections/[id]/CollectionAdmin.tsx index 5aa408c33..3accd3fd5 100644 --- a/src/app/images/collections/[id]/CollectionAdmin.tsx +++ b/src/app/images/collections/[id]/CollectionAdmin.tsx @@ -6,8 +6,9 @@ import Form from '@/components/Form/Form' import TextInput from '@/components/UI/TextInput' import { ImagePagingContext } from '@/contexts/paging/ImagePaging' import ImageUploader from '@/components/Image/ImageUploader' -import useEditing from '@/hooks/useEditing' import PopUp from '@/components/PopUp/PopUp' +import useEditMode from '@/hooks/useEditmode' +import { RequireNothing } from '@/auth/auther/RequireNothing' import Button from '@/components/UI/Button' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCog, faEye, faUpload } from '@fortawesome/free-solid-svg-icons' @@ -27,7 +28,10 @@ export default function CollectionAdmin({ collection, visibilityAdmin, visibilit const { id: collectionId } = collection const router = useRouter() const pagingContext = useContext(ImagePagingContext) - const canEdit = useEditing({}) //TODO: pass in auther + //TODO: Use correct auther. + const canEdit = useEditMode({ + auther: RequireNothing.staticFields({}).dynamicFields({}) + }) const [uploadOption, setUploadOption] = useState<'MANY' | 'ONE'>('MANY') if (!canEdit) return null diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 81a8b0078..bfd4a7f49 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,5 @@ import styles from './layout.module.scss' -import { SessionProvider } from '@/auth/session/useUser' +import { SessionProvider } from '@/auth/session/useSession' import MobileNavBar from '@/components/NavBar/MobileNavBar' import NavBar from '@/components/NavBar/NavBar' import Footer from '@/components/Footer/Footer' diff --git a/src/app/lockers/[id]/page.tsx b/src/app/lockers/[id]/page.tsx index 0a35515d3..33520f902 100644 --- a/src/app/lockers/[id]/page.tsx +++ b/src/app/lockers/[id]/page.tsx @@ -5,8 +5,8 @@ import UpdateLockerReservationForm from './UpdateLockerReservationForm' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readLockerAction } from '@/services/lockers/actions' import { checkGroupValidity, groupOperations, inferGroupName } from '@/services/groups/operations' -import { notFound } from 'next/navigation' - +import { Session } from '@/auth/session/Session' +import { RequireUser } from '@/auth/auther/RequireUser' type PropTypes = { params: Promise<{ @@ -26,6 +26,12 @@ export default async function Locker({ params }: PropTypes) { const reservation = locker.data.LockerReservation[0] const groupName = (isReserved && reservation.group) ? inferGroupName(checkGroupValidity(reservation.group)) : '' + const user = RequireUser.staticFields({}).dynamicFields({}).auth( + await Session.fromNextAuth() + ).redirectOnUnauthorized({ + returnUrl: `/lockers/${lockerId}` + }).session.user + const groups = await groupOperations.readGroupsOfUser({ bypassAuth: true, params: { diff --git a/src/app/news/[nameAndId]/EditNews.tsx b/src/app/news/[nameAndId]/EditNews.tsx index 2c96ffda5..29098831d 100644 --- a/src/app/news/[nameAndId]/EditNews.tsx +++ b/src/app/news/[nameAndId]/EditNews.tsx @@ -2,11 +2,12 @@ import styles from './EditNews.module.scss' import Form from '@/components/Form/Form' import TextInput from '@/components/UI/TextInput' +import useEditMode from '@/hooks/useEditmode' import Textarea from '@/components/UI/Textarea' import DateInput from '@/components/UI/DateInput' -import useEditing from '@/hooks/useEditing' import { destroyNewsAction, updateNewsAction } from '@/services/news/actions' import { formatVevenUri } from '@/lib/urlEncoding' +import { newsAuth } from '@/services/news/auth' import { configureAction } from '@/services/configureAction' import { useRouter } from 'next/navigation' import type { ExpandedNewsArticle } from '@/services/news/types' @@ -24,7 +25,9 @@ type PropTypes = { export default function EditNews({ news, children }: PropTypes) { const { refresh, push } = useRouter() //TODO: chack visibility - const canEdit = useEditing({}) + const canEdit = useEditMode({ + auther: newsAuth.update.dynamicFields({}) + }) if (!canEdit) return children // TODO: VISINILITY ADMIN diff --git a/src/app/ombul/[...yearAndName]/ChangeName.tsx b/src/app/ombul/[...yearAndName]/ChangeName.tsx index 032eea4bf..4a065f0a1 100644 --- a/src/app/ombul/[...yearAndName]/ChangeName.tsx +++ b/src/app/ombul/[...yearAndName]/ChangeName.tsx @@ -3,9 +3,9 @@ import styles from './ChangeName.module.scss' import EditableTextField from '@/components/EditableTextField/EditableTextField' import { updateOmbulAction } from '@/services/ombul/actions' +import { configureAction } from '@/services/configureAction' import type { ReactNode } from 'react' import type { ExpandedOmbul } from '@/services/ombul/types' -import { configureAction } from '@/services/configureAction' type PropTypes = { children: ReactNode diff --git a/src/app/ombul/[...yearAndName]/OmbulAdmin.tsx b/src/app/ombul/[...yearAndName]/OmbulAdmin.tsx index 04634117f..f4d28062b 100644 --- a/src/app/ombul/[...yearAndName]/OmbulAdmin.tsx +++ b/src/app/ombul/[...yearAndName]/OmbulAdmin.tsx @@ -5,7 +5,9 @@ import Form from '@/components/Form/Form' import { updateOmbulAction, updateOmbulFileAction, destroyOmbulAction } from '@/services/ombul/actions' import NumberInput from '@/components/UI/NumberInput' import FileInput from '@/components/UI/FileInput' -import useEditing from '@/hooks/useEditing' +import useEditMode from '@/hooks/useEditmode' +import { ombulAuth } from '@/services/ombul/auth' +import { configureAction } from '@/services/configureAction' import { useRouter } from 'next/navigation' import type { ReactNode } from 'react' import type { ExpandedOmbul } from '@/services/ombul/types' @@ -27,15 +29,21 @@ export default function OmbulAdmin({ children, }: PropTypes) { const { push, refresh } = useRouter() - const canUpdate = useEditing({ - requiredPermissions: [['OMBUL_UPDATE']] + const canUpdate = useEditMode({ + auther: ombulAuth.update.dynamicFields({}) }) - const canDestroy = useEditing({ - requiredPermissions: [['OMBUL_DESTROY']] + const canDestroy = useEditMode({ + auther: ombulAuth.destroy.dynamicFields({}) }) - const updateOmbulActionBind = updateOmbulAction.bind(null, ombul.id) - const updateOmbulFileActionBind = updateOmbulFileAction.bind(null, ombul.id) + const updateOmbulActionBind = configureAction( + updateOmbulAction, + { params: { id: ombul.id } } + ) + const updateOmbulFileActionBind = configureAction( + updateOmbulFileAction, + { params: { id: ombul.id } } + ) const handleChange = async (newOmbul: ExpandedOmbul | undefined) => { if (!newOmbul) return @@ -85,7 +93,10 @@ export default function OmbulAdmin({ { canDestroy && ( = { auth: (session: SessionMaybeUser) => UserRequieredOut extends 'USER_REQUIERED_FOR_AUTHORIZED' @@ -15,7 +15,7 @@ export type AutherStaticFieldsBound< DynamicFields extends object, UserRequieredOut extends UserRequieredOutOpt = 'USER_NOT_REQUIERED_FOR_AUTHORIZED' | 'USER_REQUIERED_FOR_AUTHORIZED', > = { - dynamicFields: (dynamicFields: DynamicFields) => AutherResult, + dynamicFields: (dynamicFields: DynamicFields) => AutherDynamicFieldsBound, } export type Auther< diff --git a/src/auth/session/Session.ts b/src/auth/session/Session.ts index ad85ab956..6f038b1e0 100644 --- a/src/auth/session/Session.ts +++ b/src/auth/session/Session.ts @@ -124,4 +124,8 @@ export class Session { apiKeyId: id, }) } + + public static fromDefaultPermissions(defaultPermissions: Permission[]) { + return new Session({ permissions: defaultPermissions, memberships: [], user: null }) + } } diff --git a/src/auth/session/useSession.ts b/src/auth/session/useSession.ts new file mode 100644 index 000000000..fd035e14b --- /dev/null +++ b/src/auth/session/useSession.ts @@ -0,0 +1,27 @@ +'use client' +import { Session } from './Session' +import { DefaultPermissionsContext } from '@/contexts/DefaultPermissions' +import { useSession as useSessionNextAuth } from 'next-auth/react' +import { useContext } from 'react' + +export { SessionProvider } from 'next-auth/react' + +type useSessionReturn = { loading: true } | { loading: false, session: Session<'NO_USER'> | Session<'HAS_USER'> } + +export function useSession(): useSessionReturn { + const defaultPermissionsContext = useContext(DefaultPermissionsContext) + const defaultPermissions = defaultPermissionsContext ? defaultPermissionsContext.defaultPermissions : [] + + const { data: session, status: nextAuthStatus } = useSessionNextAuth() + + switch (nextAuthStatus) { + case 'loading': + return { loading: true } + case 'unauthenticated': + return { loading: false, session: Session.fromDefaultPermissions(defaultPermissions) } + case 'authenticated': + return { loading: false, session: Session.fromJsObject(session) } + default: + return { loading: false, session: Session.fromDefaultPermissions(defaultPermissions) } + } +} diff --git a/src/auth/session/useUser.ts b/src/auth/session/useUser.ts deleted file mode 100644 index 9bf1d4b57..000000000 --- a/src/auth/session/useUser.ts +++ /dev/null @@ -1,152 +0,0 @@ -'use client' - -import { DefaultPermissionsContext } from '@/contexts/DefaultPermissions' -import checkMatrix from '@/lib/checkMatrix' -import { useSession } from 'next-auth/react' -import { usePathname, useRouter } from 'next/navigation' -import { useContext, useEffect, useState } from 'react' -import type { Permission } from '@prisma/client' -import type { UserFiltered } from '@/services/users/types' -import type { Matrix } from '@/lib/checkMatrix' -import type { MembershipFiltered } from '@/services/groups/memberships/types' - -// SessionProvider needs to be exported from a 'use client' file so that it can -// be used in a server side file. -export { SessionProvider } from 'next-auth/react' - -type UseUserArgsType = { - requiredPermissions?: Matrix, - userRequired?: UserRequired, - shouldRedirect?: ShouldRedirect, - redirectUrl?: string, - redirectToLogin?: boolean, -} - -type AuthorizedUseUserReturnType = ({ - user: UserFiltered, - authorized: true, - status: 'AUTHORIZED', -} | ( - UserRequired extends true ? never : { - user: null, - authorized: true, - status: 'AUTHORIZED_NO_USER', - } -)) & { - permissions: Permission[], - memberships: MembershipFiltered[], -} | { - user: undefined, - authorized: undefined, - status: 'LOADING', - permissions: undefined, - memberships: undefined, -} - -type UseUserReturnType = ( - AuthorizedUseUserReturnType -) | ({ - user: null, - authorized: false, - status: 'UNAUTHENTICATED', -} | { - user: UserFiltered, - authorized: false, - status: 'UNAUTHORIZED', -}) & { - permissions: Permission[], - memberships: MembershipFiltered[], -} - -export type ClientAuthStatus = UseUserReturnType['status'] - -/** - * Wrapper for next-auth's `useSession`. Returns just the user object of the - * current session, null otherwise. -* -* @param requiredPermissions - A list of lists that the user must have. If non are given, the user is considered authorized -* regardless of their permissions. -* @param userRequired - False by default. If true the user will only be unauthorized if they are not logged inn. -* @param shouldRedirect - True by default. If true the user will be redirected when not authorized. Where the user will be -* redirected to will depend on the arguments below. -* @param redirectUrl - The homepage by default, if the user is not authorized they will be redirected here if -* redirect to login is disabled. -* @param redirectToLogin - True by default. If the user is not logged in they will be sent to the login page. -* -* @returns The user (always returned if `shouldRedirect` and `userRequired` is true). -* The auth status, and if the user is authorized (i.e. if the user fufilles the given requirements) -* -* @deprecated - Deprecated as the new service mehtod system handles this. -* For getting the user in the app router a utility function will be developed. -*/ -// Overloading is required here to get correct typehinting base on if required is true or false in options. -export function useUser( - args?: UseUserArgsType -): UseUserReturnType -export function useUser( - args?: UseUserArgsType -): AuthorizedUseUserReturnType -export function useUser({ - requiredPermissions = [], - userRequired = false, - shouldRedirect = false, - redirectUrl = '/', // TODO: Should be unauthorized page by default - redirectToLogin = true, -}: UseUserArgsType = {}): UseUserReturnType { - const { push } = useRouter() - const pathName = usePathname() - const defaultPermissionsCtx = useContext(DefaultPermissionsContext) - const defaultPermissions = defaultPermissionsCtx ? defaultPermissionsCtx.defaultPermissions : [] - const [useUserReturn, setUseuserReturn] = useState>({ - status: 'LOADING', - authorized: undefined, - user: undefined, - memberships: undefined, - permissions: undefined, - }) - - const { data: session, status: nextAuthStatus } = useSession({ - required: shouldRedirect && redirectToLogin, - }) - - useEffect(() => { - if (nextAuthStatus === 'loading') return - - const { - user = null, - permissions = defaultPermissions, - memberships = [], - } = session ?? {} - - // Authorized is true if both these conditions are true - // 1. The user is logged inn or (the user is not logged inn and the user session is not required) - // 2. The user has all the required permissions - if ((user || !userRequired) && checkMatrix(permissions, requiredPermissions)) { - setUseuserReturn(user - ? { user, authorized: true, status: 'AUTHORIZED', permissions, memberships } - : { user, authorized: true, status: 'AUTHORIZED_NO_USER', permissions, memberships } - ) - return - } - - if (shouldRedirect) { - // The next auth useSession function will redirect the user to the login page if there - // is no user session at all. However, since we can have authorized users without a - // session we can only use this feature if userRequired is true. Therefore we need to - // handle the case where userRequired is false and the deafult permissions aren't - // enough our selves. - if (!user && redirectToLogin) { - push(`/login?callbackUrl=${encodeURI(pathName)}`) - } - - push(redirectUrl) - } - - setUseuserReturn(user - ? { user, authorized: false, status: 'UNAUTHORIZED', permissions, memberships } - : { user, authorized: false, status: 'UNAUTHENTICATED', permissions, memberships } - ) - }, [session, nextAuthStatus]) - - return useUserReturn -} diff --git a/src/contexts/DefaultPermissions.tsx b/src/contexts/DefaultPermissions.tsx index e1c0f8973..c3cfa1abc 100644 --- a/src/contexts/DefaultPermissions.tsx +++ b/src/contexts/DefaultPermissions.tsx @@ -10,7 +10,7 @@ type PropTypes = { /** * This is a context thatsaves the default permissions for all users on the client - * It is manly used for the useUser hook, when no user is logged in. + * It is manly used for the useSession hook to provide permissions for unauthenticated users */ export const DefaultPermissionsContext = createContext<{ defaultPermissions: Permission[], diff --git a/src/hooks/useAuther.ts b/src/hooks/useAuther.ts new file mode 100644 index 000000000..43b0f120d --- /dev/null +++ b/src/hooks/useAuther.ts @@ -0,0 +1,33 @@ +import { useSession } from '@/auth/session/useSession' +import { AuthResult } from '@/auth/auther/AuthResult' +import { Session, type UserGuaranteeOption } from '@/auth/session/Session' +import type { AutherDynamicFieldsBound } from '@/auth/auther/Auther' + +/** + * This function applies an auther to the current client side session stored. While + * this session is still loading, it will return an AuthResult with authorized set to false. + * @param param0 + */ +function useAuther({ + auther +}: { + auther: AutherDynamicFieldsBound<'USER_NOT_REQUIERED_FOR_AUTHORIZED'> +}): AuthResult +function useAuther({ + auther +}: { + auther: AutherDynamicFieldsBound<'USER_REQUIERED_FOR_AUTHORIZED'> +}): AuthResult | AuthResult<'HAS_USER', true> +function useAuther({ + auther +}: { + auther: AutherDynamicFieldsBound +}): AuthResult { + const session = useSession() + if (session.loading) { + return new AuthResult(Session.empty(), false) + } + return auther.auth(session.session) +} + +export default useAuther diff --git a/src/hooks/useEditing.ts b/src/hooks/useEditing.ts deleted file mode 100644 index 8fc1f5553..000000000 --- a/src/hooks/useEditing.ts +++ /dev/null @@ -1,59 +0,0 @@ -'use client' -import { useUser } from '@/auth/session/useUser' -import { EditModeContext } from '@/contexts/EditMode' -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' - -//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 - * so EditModeContext will signal editModeSwitches to show. - * @param requiredPermissions - The permissions required to edit the content. If non are provided, it is - * assumed that the user is allowed to edit the content. - * @param requiredVisibility - The visibility required to edit the content. If non is provided, it is - * assumed that the user is allowed to edit the content. - * @param operation - The operation to perform on the permissions and visibility. AND or OR, OR by default - * @param level - The level of visibility required to pass visibility test. ADMIN by default - * @returns - A bool indicating: - * - IF the bool is true editMode is on, and the user has the required permissions and/or visibility - * - IF the bool is false editMode is off or the user does not have the required permissions and/or visibility - */ -export default function useEditing({ - requiredPermissions, -}: { - requiredPermissions?: Matrix, -}): boolean { - const editModeCtx = useContext(EditModeContext) - const { authorized: permissionAuthorized, permissions, memberships } = useUser({ - requiredPermissions, - shouldRedirect: false, - }) - const [authorized, setAuthorized] = useState(false) - //Editable if ctx is on and user has the required permissions and/or visibility - const [editable, setEditable] = useState(false) - - const uniqueKey = useRef(uuid()).current - useEffect(() => { - const authorized_ = permissionAuthorized === true - if (editModeCtx) { - if (authorized_) editModeCtx.addEditableContent(uniqueKey) - if (!authorized_) editModeCtx.removeEditableContent(uniqueKey) - setAuthorized(authorized_) - } - return () => { - if (editModeCtx) editModeCtx.removeEditableContent(uniqueKey) - } - }, [permissionAuthorized, permissions, memberships]) - - useEffect(() => { - setEditable(editModeCtx ? Boolean(authorized) && editModeCtx.editMode : false) - }, [authorized, editModeCtx?.editMode]) - - return editable -} diff --git a/src/hooks/useEditmode.ts b/src/hooks/useEditmode.ts new file mode 100644 index 000000000..afd4e6bda --- /dev/null +++ b/src/hooks/useEditmode.ts @@ -0,0 +1,34 @@ +'use client' +import useAuther from './useAuther' +import { EditModeContext } from '@/contexts/EditMode' +import { useContext, useEffect, useRef } from 'react' +import { v4 as uuid } from 'uuid' +import type { AutherDynamicFieldsBound } from '@/auth/auther/Auther' + +/** + * This hook does the following: + * - If the user is authorized, see the useAuther hook, it will register the component in the edit mode context + * signaling that there is a editable component on the page. This causes the context to display the edit mode pencil. + * - If the user is not authorized, it will not register the component in the edit mode context. + * @param param0 + * @returns If the component should open editMode, i.e. if the user is authorized and edit mode is enabled. + */ +export default function useEditMode({ + auther +}: { + auther: AutherDynamicFieldsBound +}): boolean { + const editModeCtx = useContext(EditModeContext) + const uniqueKey = useRef(uuid()).current + const authResult = useAuther({ auther }) + useEffect(() => { + if (editModeCtx) { + if (authResult.authorized) editModeCtx.addEditableContent(uniqueKey) + if (!authResult.authorized) editModeCtx.removeEditableContent(uniqueKey) + } + return () => { + if (editModeCtx) editModeCtx.removeEditableContent(uniqueKey) + } + }, [editModeCtx, auther, authResult, uniqueKey]) + return authResult.authorized && editModeCtx?.editMode === true +} diff --git a/src/lib/checkMatrix.ts b/src/lib/checkMatrix.ts deleted file mode 100644 index 61142d460..000000000 --- a/src/lib/checkMatrix.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type Matrix = T[][] - -/** - * Utility function for checking if an array of a given type fulfills the a requirement matrix. - * The given array must have at least one item for each array (row) in the matrix. - * [[A, B], [C, D]] means the given array must have (either A or B) and (either C or D). - * - * @param given - An array of a given type. - * @param required - The requirements matrix. - * @returns - true if the given array fulfills the required matrix, false otherwise. - */ -export default function checkMatrix(given: T[], required: Matrix): boolean { - return required.every((row) => row.some((item) => given.includes(item))) -} diff --git a/src/services/groups/committees/operations.ts b/src/services/groups/committees/operations.ts index fe8eb13c3..b43703dc5 100644 --- a/src/services/groups/committees/operations.ts +++ b/src/services/groups/committees/operations.ts @@ -1,5 +1,6 @@ import { committeeAuth } from './auth' import { committeeExpandedIncluder, committeeLogoIncluder, membershipIncluder } from './constants' +import { committeeSchemas } from './validation' import { ServerOnlyAuther } from '@/auth/auther/RequireServer' import { cmsImageOperations } from '@/cms/images/operations' import { cmsParagraphOperations } from '@/cms/paragraphs/operations' @@ -8,9 +9,8 @@ import { defineOperation } from '@/services/serviceOperation' import { articleRealtionsIncluder } from '@/cms/articles/constants' import { implementUpdateArticleOperations } from '@/cms/articles/implement' import { articleOperations } from '@/cms/articles/operations' -import { z } from 'zod' -import { committeeSchemas } from './validation' import { readCurrentOmegaOrder } from '@/services/omegaOrder/read' +import { z } from 'zod' import { GroupType } from '@prisma/client' diff --git a/src/services/groups/studyProgrammes/auth.ts b/src/services/groups/studyProgrammes/auth.ts new file mode 100644 index 000000000..977693661 --- /dev/null +++ b/src/services/groups/studyProgrammes/auth.ts @@ -0,0 +1,9 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + + +export const studyProgrammeAuth = { + create: RequirePermission.staticFields({ permission: 'STUDY_PROGRAMME_CREATE' }), + read: RequirePermission.staticFields({ permission: 'STUDY_PROGRAMME_READ' }), + update: RequirePermission.staticFields({ permission: 'STUDY_PROGRAMME_UPDATE' }), + destroy: RequirePermission.staticFields({ permission: 'STUDY_PROGRAMME_DESTROY' }), +} as const diff --git a/src/services/notifications/operations.ts b/src/services/notifications/operations.ts index 807286f43..43eeeb438 100644 --- a/src/services/notifications/operations.ts +++ b/src/services/notifications/operations.ts @@ -4,6 +4,8 @@ import { notificationAuth } from './auth' import { notificationSchemas } from './schemas' import { allNotificationMethodsOn, notificationMethodsArray } from './constants' import { availableNotificationMethodIncluder } from './channel/constants' +import { sendMail } from './email/send' +import { emailSchemas } from './email/schemas' import { userFilterSelection } from '@/services/users/constants' import { defineOperation } from '@/services/serviceOperation' import { ServerOnly } from '@/auth/auther/ServerOnly' @@ -12,8 +14,6 @@ import { SpecialNotificationChannel } from '@prisma/client' import type { Notification } from '@prisma/client' import type { ExpandedNotificationChannel, NotificationResult } from './types' import type { UserFiltered } from '@/services/users/types' -import { sendMail } from './email/send' -import { emailSchemas } from './email/schemas' const dispathMethod = { email: dispatchEmailNotifications, diff --git a/src/services/ombul/operations.ts b/src/services/ombul/operations.ts index b64c3c14b..81801ccdb 100644 --- a/src/services/ombul/operations.ts +++ b/src/services/ombul/operations.ts @@ -1,15 +1,15 @@ import '@pn-server-only' -import { z } from 'zod' import { ombulAuth } from './auth' +import { ombulSchemas } from './schemas' import { defineOperation } from '@/services/serviceOperation' import { cmsImageOperations } from '@/cms/images/operations' import { ServerError } from '@/services/error' import { destroyFile } from '@/services/store/destroyFile' -import { ombulSchemas } from './schemas' import { createFile } from '@/services/store/createFile' import { readSpecialImageCollection } from '@/services/images/collections/read' import { imageOperations } from '@/services/images/operations' import { notificationOperations } from '@/services/notifications/operations' +import { z } from 'zod' const read = defineOperation({ authorizer: () => ombulAuth.read.dynamicFields({}), diff --git a/src/services/omegaid/actions.ts b/src/services/omegaid/actions.ts index d6afdbe8e..5ca08a025 100644 --- a/src/services/omegaid/actions.ts +++ b/src/services/omegaid/actions.ts @@ -2,8 +2,8 @@ import { createActionError } from '@/services/actionError' import { ServerError } from '@/services/error' import { generateOmegaId } from '@/services/omegaid/generate' -import type { ActionReturn } from '@/services/actionTypes' import { Session } from '@/auth/session/Session' +import type { ActionReturn } from '@/services/actionTypes' export async function generateOmegaIdAction(): Promise> { //TODO: when changed to makeAction + operation it should take in a params: userId and diff --git a/src/services/omegaquotes/operations.ts b/src/services/omegaquotes/operations.ts index 8cb9fb9a9..1dbe925d4 100644 --- a/src/services/omegaquotes/operations.ts +++ b/src/services/omegaquotes/operations.ts @@ -1,11 +1,11 @@ -import { defineOperation } from '@/services/serviceOperation' import { omegaQuotesAuth } from './auth' import { omegaquoteSchemas } from './schemas' -import { notificationOperations } from '@/services/notifications/operations' -import { z } from 'zod' -import { readPageInputSchemaObject } from '@/lib/paging/schema' import { omegaQuoteFilterSelection } from './constants' +import { readPageInputSchemaObject } from '@/lib/paging/schema' +import { notificationOperations } from '@/services/notifications/operations' +import { defineOperation } from '@/services/serviceOperation' import { cursorPageingSelection } from '@/lib/paging/cursorPageingSelection' +import { z } from 'zod' export const omegaquoteOperations = { create: defineOperation({ diff --git a/src/services/serviceOperation.ts b/src/services/serviceOperation.ts index 62d9a77fe..ac0f6d024 100644 --- a/src/services/serviceOperation.ts +++ b/src/services/serviceOperation.ts @@ -6,7 +6,7 @@ import { RequireNothing } from '@/auth/auther/RequireNothing' import { Session } from '@/auth/session/Session' import { zfd } from 'zod-form-data' import { AsyncLocalStorage } from 'async_hooks' -import type { AutherResult } from '@/auth/auther/Auther' +import type { AutherDynamicFieldsBound } from '@/auth/auther/Auther' import type { z } from 'zod' import type { SessionMaybeUser } from '@/auth/session/Session' import type { Prisma, PrismaClient } from '@prisma/client' @@ -100,7 +100,7 @@ export type AutherGetter< ImplementationParamsSchema extends z.ZodTypeAny | undefined > = ( args: ArgsAuthGetterAndOwnershipCheck -) => AutherResult | Promise +) => AutherDynamicFieldsBound | Promise export type OwnershipCheck< OpensTransaction extends boolean, From b5104060f3456dee17bfdc33547f4da20494dd9b Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:41:21 +0100 Subject: [PATCH 7/9] refactor: make a seperate ServerSession to Session so that we do not expose server side constructors to client --- src/app/(auth)/register-email/page.tsx | 6 +- src/app/(auth)/register/page.tsx | 4 +- src/app/(auth)/verify-email/page.tsx | 4 +- src/app/(frontpage)/page.tsx | 4 +- src/app/admin/dots/CreateDotForm.tsx | 4 +- src/app/admin/mail/page.tsx | 4 +- src/app/admin/send-mail/page.tsx | 4 +- src/app/admin/stateOfOmega/page.tsx | 4 +- src/app/admin/study-programmes/page.tsx | 6 +- src/app/api/apiHandler.ts | 4 +- src/app/applications/[periodName]/page.tsx | 4 +- src/app/cabin/book/page.tsx | 4 +- src/app/career/companies/page.tsx | 4 +- src/app/career/jobads/[nameAndId]/page.tsx | 4 +- src/app/career/page.tsx | 4 +- src/app/education/schools/page.tsx | 4 +- src/app/events/archive/page.tsx | 4 +- src/app/events/page.tsx | 4 +- src/app/images/page.tsx | 4 +- src/app/interest-groups/page.tsx | 4 +- src/app/lockers/[id]/page.tsx | 3 +- src/app/ombul/[...yearAndName]/page.tsx | 4 +- src/app/ombul/page.tsx | 4 +- src/app/omegaquotes/page.tsx | 4 +- .../(user-admin)/getProfileForAdmin.ts | 4 +- .../users/[username]/(user-admin)/layout.tsx | 4 +- src/app/users/[username]/omegaid/page.tsx | 4 +- src/app/users/[username]/page.tsx | 4 +- src/auth/session/ServerSession.ts | 67 +++++++++++++++++++ src/auth/session/Session.ts | 66 +----------------- src/services/omegaid/actions.ts | 6 +- src/services/screens/actions.ts | 26 +------ src/services/screens/auth.ts | 10 +++ src/services/serverAction.ts | 4 +- 34 files changed, 143 insertions(+), 151 deletions(-) create mode 100644 src/auth/session/ServerSession.ts create mode 100644 src/services/screens/auth.ts diff --git a/src/app/(auth)/register-email/page.tsx b/src/app/(auth)/register-email/page.tsx index d2877ca5f..b6f7c6d2a 100644 --- a/src/app/(auth)/register-email/page.tsx +++ b/src/app/(auth)/register-email/page.tsx @@ -1,11 +1,13 @@ import EmailRegistrationForm from './EmailregistrationForm' +import { ServerSession } from '@/auth/session/ServerSession' import { RequireUser } from '@/auth/auther/RequireUser' import { readUserAction } from '@/services/users/actions' -import { Session } from '@/auth/session/Session' import { notFound, redirect } from 'next/navigation' export default async function Registeremail() { - const { authorized, session } = RequireUser.staticFields({}).dynamicFields({}).auth(await Session.fromNextAuth()) + const { authorized, session } = RequireUser.staticFields({}).dynamicFields({}).auth( + await ServerSession.fromNextAuth() + ) if (!authorized) notFound() diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index e62430aee..2540224f5 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,15 +1,15 @@ import RegistrationForm from './RegistrationForm' import { QueryParams } from '@/lib/queryParams/queryParams' import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { ServerSession } from '@/auth/session/ServerSession' import { readUserAction } from '@/services/users/actions' -import { Session } from '@/auth/session/Session' import { notFound, redirect } from 'next/navigation' import type { SearchParamsServerSide } from '@/lib/queryParams/types' type PropTypes = SearchParamsServerSide export default async function Register({ searchParams }: PropTypes) { - const user = (await Session.fromNextAuth()).user + const user = (await ServerSession.fromNextAuth()).user const callbackUrl = QueryParams.callbackUrl.decode(await searchParams) if (!user) { return notFound() diff --git a/src/app/(auth)/verify-email/page.tsx b/src/app/(auth)/verify-email/page.tsx index 06d1e1e38..5efc98f59 100644 --- a/src/app/(auth)/verify-email/page.tsx +++ b/src/app/(auth)/verify-email/page.tsx @@ -1,8 +1,8 @@ import { EmailVerifiedWrapper } from './EmailVerifiedWrapper' import { QueryParams } from '@/lib/queryParams/queryParams' import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { ServerSession } from '@/auth/session/ServerSession' import { verifyEmailAction } from '@/services/auth/actions' -import { Session } from '@/auth/session/Session' import { notFound, redirect } from 'next/navigation' import Link from 'next/link' import type { SearchParamsServerSide } from '@/lib/queryParams/types' @@ -15,7 +15,7 @@ export default async function Register({ searchParams }: PropTypes) { notFound() } - const user = (await Session.fromNextAuth()).user + const user = (await ServerSession.fromNextAuth()).user const userId = user?.id const updatedUser = unwrapActionReturn(await verifyEmailAction({ params: { token } })) diff --git a/src/app/(frontpage)/page.tsx b/src/app/(frontpage)/page.tsx index 9f794f4bb..705828b52 100644 --- a/src/app/(frontpage)/page.tsx +++ b/src/app/(frontpage)/page.tsx @@ -1,8 +1,8 @@ import LoggedInLandingPage from './LoggedIn' import LoggedOutLandingPage from './LoggedOut' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' export default async function Home() { - const { user } = await Session.fromNextAuth() + const { user } = await ServerSession.fromNextAuth() return user ? : } diff --git a/src/app/admin/dots/CreateDotForm.tsx b/src/app/admin/dots/CreateDotForm.tsx index 6bce055d1..e294a03d3 100644 --- a/src/app/admin/dots/CreateDotForm.tsx +++ b/src/app/admin/dots/CreateDotForm.tsx @@ -6,13 +6,13 @@ import PopUp from '@/components/PopUp/PopUp' import NumberInput from '@/components/UI/NumberInput' import TextInput from '@/components/UI/TextInput' import UserList from '@/components/User/UserList/UserList' +import { useSession } from '@/auth/session/useSession' import { PopUpContext } from '@/contexts/PopUp' import { UserSelectionContext } from '@/contexts/UserSelection' import { configureAction } from '@/services/configureAction' import { useContext } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPlus } from '@fortawesome/free-solid-svg-icons' -import { useSession } from 'next-auth/react' export default function CreateDotForm() { const session = useSession() @@ -47,7 +47,7 @@ export default function CreateDotForm() {
diff --git a/src/app/admin/mail/page.tsx b/src/app/admin/mail/page.tsx index 0f455b47b..052615b7e 100644 --- a/src/app/admin/mail/page.tsx +++ b/src/app/admin/mail/page.tsx @@ -8,10 +8,10 @@ import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readMailAliases } from '@/services/mail/alias/read' import { readMailingLists } from '@/services/mail/list/read' import { readMailAddressExternal } from '@/services/mail/mailAddressExternal/read' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' export default async function MailSettings() { - const { permissions } = await Session.fromNextAuth() + const { permissions } = await ServerSession.fromNextAuth() const createMailAlias = permissions.includes('MAILALIAS_CREATE') const createMailingList = permissions.includes('MAILINGLIST_CREATE') diff --git a/src/app/admin/send-mail/page.tsx b/src/app/admin/send-mail/page.tsx index 197bcdfbf..282e244a8 100644 --- a/src/app/admin/send-mail/page.tsx +++ b/src/app/admin/send-mail/page.tsx @@ -1,11 +1,11 @@ import MailForm from './mailForm' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { notificationAuth } from '@/services/notifications/auth' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' export default async function SendMail() { notificationAuth.sendMail.dynamicFields({}).auth( - await Session.fromNextAuth() + await ServerSession.fromNextAuth() ).redirectOnUnauthorized({ returnUrl: '/admin/send-mail' }) return ( diff --git a/src/app/admin/stateOfOmega/page.tsx b/src/app/admin/stateOfOmega/page.tsx index dd5e81fae..d5212977f 100644 --- a/src/app/admin/stateOfOmega/page.tsx +++ b/src/app/admin/stateOfOmega/page.tsx @@ -2,12 +2,12 @@ import styles from './page.module.scss' import CreateOrder from './CreateOrder' import { readCurrentOmegaOrderAction } from '@/services/omegaOrder/actions' import { omegaOrderAuth } from '@/services/omegaOrder/auth' -import { Session } from '@/auth/session/Session' import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { ServerSession } from '@/auth/session/ServerSession' export default async function stateOfOmega() { omegaOrderAuth.create.dynamicFields({}).auth( - await Session.fromNextAuth() + await ServerSession.fromNextAuth() ).redirectOnUnauthorized({ returnUrl: '/admin/state-of-omega' }) const currentOrder = unwrapActionReturn(await readCurrentOmegaOrderAction()) diff --git a/src/app/admin/study-programmes/page.tsx b/src/app/admin/study-programmes/page.tsx index aff3cf42e..6bf8c0674 100644 --- a/src/app/admin/study-programmes/page.tsx +++ b/src/app/admin/study-programmes/page.tsx @@ -8,14 +8,14 @@ import { AddHeaderItemPopUp } from '@/components/HeaderItems/HeaderItemPopUp' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { unwrapActionReturn } from '@/app/redirectToErrorPage' import { studyProgrammeAuth } from '@/services/groups/studyProgrammes/auth' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' export default async function StudyProgrammes() { const studyprogrammes = unwrapActionReturn(await readStudyProgrammesAction()) - const showCreateButton = studyProgrammeAuth.create.dynamicFields({}).auth(await Session.fromNextAuth()) - const canEdit = studyProgrammeAuth.update.dynamicFields({}).auth(await Session.fromNextAuth()) + const showCreateButton = studyProgrammeAuth.create.dynamicFields({}).auth(await ServerSession.fromNextAuth()) + const canEdit = studyProgrammeAuth.update.dynamicFields({}).auth(await ServerSession.fromNextAuth()) return (req: Request, handle: (session: SessionNoUser) => Promise) { try { const authorization = req.headers.get('authorization') - const session = await Session.fromApiKey(authorization) + const session = await ServerSession.fromApiKey(authorization) const result = await handle(session) return createApiResponse(result) } catch (error: unknown) { diff --git a/src/app/applications/[periodName]/page.tsx b/src/app/applications/[periodName]/page.tsx index 5813a4af2..dc51cf5c2 100644 --- a/src/app/applications/[periodName]/page.tsx +++ b/src/app/applications/[periodName]/page.tsx @@ -7,7 +7,6 @@ import CountDown from '@/components/countDown/CountDown' import BackdropImage from '@/components/BackdropImage/BackdropImage' import CmsParagraph from '@/components/Cms/CmsParagraph/CmsParagraph' import PopUp from '@/components/PopUp/PopUp' -import { Session } from '@/auth/session/Session' import Textarea from '@/components/UI/Textarea' import Form from '@/components/Form/Form' import { SettingsHeaderItemPopUp } from '@/components/HeaderItems/HeaderItemPopUp' @@ -25,6 +24,7 @@ import { readApplicationPeriodAction } from '@/services/applications/periods/actions' import { readSpecialImageAction } from '@/services/images/actions' +import { ServerSession } from '@/auth/session/ServerSession' import { configureAction } from '@/services/configureAction' import { faVideo } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -37,7 +37,7 @@ export type PropTypes = { } export default async function ApplicationPeriod({ params }: PropTypes) { - const userId = (await Session.fromNextAuth()).user?.id + const userId = (await ServerSession.fromNextAuth()).user?.id const period = unwrapActionReturn( await readApplicationPeriodAction({ params: { name: (await params).periodName } }) ) diff --git a/src/app/cabin/book/page.tsx b/src/app/cabin/book/page.tsx index 96a2fc06e..c9e2433e6 100644 --- a/src/app/cabin/book/page.tsx +++ b/src/app/cabin/book/page.tsx @@ -11,8 +11,8 @@ import { readSpecialCmsParagraphCabinContractAction, updateSpecialCmsParagraphCabinContractAction } from '@/services/cabin/actions' +import { ServerSession } from '@/auth/session/ServerSession' import { displayDate } from '@/lib/dates/displayDate' -import { Session } from '@/auth/session/Session' import { cabinBookingAuth } from '@/services/cabin/booking/auth' import type { ReleasePeriod } from '@prisma/client' @@ -43,7 +43,7 @@ export default async function CabinBooking() { const nextReleasePeriod = findNextReleasePeriod(releasePeriods) const pricePeriods = unwrapActionReturn(await readPublicPricePeriodsAction()) const cabinProducts = unwrapActionReturn(await readCabinProductsActiveAction()) - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const canBookCabin = cabinBookingAuth.createCabinBookingNoUser.dynamicFields({}).auth(session) const canBookBed = cabinBookingAuth.createBedBookingNoUser.dynamicFields({}).auth(session) diff --git a/src/app/career/companies/page.tsx b/src/app/career/companies/page.tsx index 8bbc19ac1..c16521ff0 100644 --- a/src/app/career/companies/page.tsx +++ b/src/app/career/companies/page.tsx @@ -8,7 +8,7 @@ import CompanyList from '@/components/Company/CompanyList' import { companyListRenderer } from '@/components/Company/CompanyListRenderer' import { QueryParams } from '@/lib/queryParams/queryParams' import CompanyListFilter from '@/app/_components/Company/CompanyListFilter' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' import { configureAction } from '@/services/configureAction' import type { SearchParamsServerSide } from '@/lib/queryParams/types' import type { PageSizeCompany } from '@/contexts/paging/CompanyPaging' @@ -19,7 +19,7 @@ export default async function page({ searchParams }: PropTypes) { const pageSize = 10 satisfies PageSizeCompany const name = QueryParams.companyName.decode(await searchParams) ?? undefined - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const res = await configureAction(readCompanyPageAction, { params: { paging: { diff --git a/src/app/career/jobads/[nameAndId]/page.tsx b/src/app/career/jobads/[nameAndId]/page.tsx index 5c32122a2..294c1bb53 100644 --- a/src/app/career/jobads/[nameAndId]/page.tsx +++ b/src/app/career/jobads/[nameAndId]/page.tsx @@ -5,7 +5,6 @@ import CompanySelectionProvider from '@/contexts/CompanySelection' import { CompanyPagingProvider } from '@/contexts/paging/CompanyPaging' import Company from '@/components/Company/Company' import Date from '@/components/Date/Date' -import { Session } from '@/auth/session/Session' import { readJobAdAction, updateJobAdArticleAction, @@ -21,6 +20,7 @@ import { } from '@/services/career/jobAds/actions' import { jobAdType } from '@/services/career/jobAds/constants' import { decodeVevenUriHandleError } from '@/lib/urlEncoding' +import { ServerSession } from '@/auth/session/ServerSession' import { configureAction } from '@/services/configureAction' import { notFound } from 'next/navigation' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -42,7 +42,7 @@ type PropTypes = { export default async function JobAd({ params }: PropTypes) { const nameAndId = (await params).nameAndId - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const jobAdRes = await readJobAdAction({ params: { id: decodeVevenUriHandleError(nameAndId) } }) if (!jobAdRes.success) { //TODO: Handle error in idiomatic way diff --git a/src/app/career/page.tsx b/src/app/career/page.tsx index f989f67c9..675a10511 100644 --- a/src/app/career/page.tsx +++ b/src/app/career/page.tsx @@ -1,7 +1,7 @@ import styles from './page.module.scss' import SpecialCmsParagraph from '@/components/Cms/CmsParagraph/SpecialCmsParagraph' import PageWrapper from '@/components/PageWrapper/PageWrapper' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' import Image from '@/components/Image/Image' import CmsLink from '@/components/Cms/CmsLink/CmsLink' import { QueryParams } from '@/lib/queryParams/queryParams' @@ -12,7 +12,7 @@ import { readCompanySpecialCmsLinkAction, updateCompanySpecialCmsLinkAction } fr import Link from 'next/link' export default async function CareerLandingPage() { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const jobAdImageRes = await readSpecialImageAction({ params: { special: 'MACHINE' } }) const eventImageRes = await readSpecialImageAction({ params: { special: 'FAIR' } }) const comanyImageRes = await readSpecialImageAction({ params: { special: 'REALFAGSBYGGET' } }) diff --git a/src/app/education/schools/page.tsx b/src/app/education/schools/page.tsx index ab016ce96..434d5fa8d 100644 --- a/src/app/education/schools/page.tsx +++ b/src/app/education/schools/page.tsx @@ -3,14 +3,14 @@ import { readSchoolsPageAction } from '@/services/education/schools/actions' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { SchoolPagingProvider } from '@/contexts/paging/SchoolPaging' import SchoolList from '@/components/School/SchoolList' +import { ServerSession } from '@/auth/session/ServerSession' import { schoolAuth } from '@/services/education/schools/auth' -import { Session } from '@/auth/session/Session' import { schoolListRenderer } from '@/components/School/SchoolListRenderer' import Link from 'next/link' import type { PageSizeSchool } from '@/contexts/paging/SchoolPaging' export default async function Schools() { - const isSchoolAdmin = schoolAuth.create.dynamicFields({}).auth(await Session.fromNextAuth()).authorized + const isSchoolAdmin = schoolAuth.create.dynamicFields({}).auth(await ServerSession.fromNextAuth()).authorized const pageSizeSchool: PageSizeSchool = 8 const res = await readSchoolsPageAction({ diff --git a/src/app/events/archive/page.tsx b/src/app/events/archive/page.tsx index 5e6cc3dd6..cee56fddd 100644 --- a/src/app/events/archive/page.tsx +++ b/src/app/events/archive/page.tsx @@ -3,9 +3,9 @@ import TagHeaderItem from '@/app/events/TagHeaderItem' import { readEventTagsAction } from '@/services/events/tags/actions' import EventsLandingLayout from '@/app/events/EventsLandingLayout' import { EventArchivePagingProvider } from '@/contexts/paging/EventArchivePaging' +import { ServerSession } from '@/auth/session/ServerSession' import { QueryParams } from '@/lib/queryParams/queryParams' import { eventTagAuth } from '@/services/events/tags/auth' -import { Session } from '@/auth/session/Session' import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' import type { SearchParamsServerSide } from '@/lib/queryParams/types' @@ -22,7 +22,7 @@ export default async function EventArchive({ const { data: eventTags } = eventTagsResponse const selectedTags = selectedTagNames ? eventTags.filter(tag => selectedTagNames.includes(tag.name)) : [] - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const canUpdate = eventTagAuth.update.dynamicFields({}).auth(session) const canCreate = eventTagAuth.create.dynamicFields({}).auth(session) diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx index c2ff33232..626c640d1 100644 --- a/src/app/events/page.tsx +++ b/src/app/events/page.tsx @@ -8,7 +8,7 @@ import EventCard from '@/components/Event/EventCard' import { readEventTagsAction } from '@/services/events/tags/actions' import { eventTagAuth } from '@/services/events/tags/auth' import { QueryParams } from '@/lib/queryParams/queryParams' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' import { faArchive } from '@fortawesome/free-solid-svg-icons' import type { SearchParamsServerSide } from '@/lib/queryParams/types' @@ -33,7 +33,7 @@ export default async function Events({ const currentTags = tagNames ? eventTags.filter(tag => tagNames.includes(tag.name)) : [] - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const canUpdate = eventTagAuth.update.dynamicFields({}).auth(session) const canCreate = eventTagAuth.create.dynamicFields({}).auth(session) diff --git a/src/app/images/page.tsx b/src/app/images/page.tsx index e7843f61e..faa571a78 100644 --- a/src/app/images/page.tsx +++ b/src/app/images/page.tsx @@ -3,12 +3,12 @@ import MakeNewCollection from './MakeNewCollection' import ImageCollectionList from '@/components/Image/Collection/ImageCollectionList' import { ImageCollectionPagingProvider } from '@/contexts/paging/ImageCollectionPaging' import CollectionCard from '@/components/Image/Collection/CollectionCard' +import { ServerSession } from '@/auth/session/ServerSession' import { readImageCollectionsPageAction } from '@/services/images/collections/actions' -import { Session } from '@/auth/session/Session' import type { PageSizeImageCollection } from '@/contexts/paging/ImageCollectionPaging' export default async function Images() { - const { user } = await Session.fromNextAuth() + const { user } = await ServerSession.fromNextAuth() const isAdmin = user?.username === 'harambe' //TODO: temp const pageSize: PageSizeImageCollection = 12 diff --git a/src/app/interest-groups/page.tsx b/src/app/interest-groups/page.tsx index 5b9edbd89..cfda60e32 100644 --- a/src/app/interest-groups/page.tsx +++ b/src/app/interest-groups/page.tsx @@ -3,16 +3,16 @@ import InterestGroup from './InterestGroup' import SpecialCmsParagraph from '@/cms/CmsParagraph/SpecialCmsParagraph' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { AddHeaderItemPopUp } from '@/components/HeaderItems/HeaderItemPopUp' -import { Session } from '@/auth/session/Session' import { interestGroupAuth } from '@/services/groups/interestGroups/auth' import { readInterestGroupsAction, readSpecialCmsParagraphGeneralInfoAction, updateSpecialCmsParagraphContentGeneralInfoAction } from '@/services/groups/interestGroups/actions' +import { ServerSession } from '@/auth/session/ServerSession' export default async function InterestGroups() { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const interestGroupsRes = await readInterestGroupsAction() if (!interestGroupsRes.success) return
Failed to load interest groups
//TODO: Change to unwrap? const interestGroups = interestGroupsRes.data diff --git a/src/app/lockers/[id]/page.tsx b/src/app/lockers/[id]/page.tsx index 33520f902..22627cac2 100644 --- a/src/app/lockers/[id]/page.tsx +++ b/src/app/lockers/[id]/page.tsx @@ -7,6 +7,7 @@ import { readLockerAction } from '@/services/lockers/actions' import { checkGroupValidity, groupOperations, inferGroupName } from '@/services/groups/operations' import { Session } from '@/auth/session/Session' import { RequireUser } from '@/auth/auther/RequireUser' +import { ServerSession } from '@/auth/session/ServerSession' type PropTypes = { params: Promise<{ @@ -27,7 +28,7 @@ export default async function Locker({ params }: PropTypes) { const groupName = (isReserved && reservation.group) ? inferGroupName(checkGroupValidity(reservation.group)) : '' const user = RequireUser.staticFields({}).dynamicFields({}).auth( - await Session.fromNextAuth() + await ServerSession.fromNextAuth() ).redirectOnUnauthorized({ returnUrl: `/lockers/${lockerId}` }).session.user diff --git a/src/app/ombul/[...yearAndName]/page.tsx b/src/app/ombul/[...yearAndName]/page.tsx index 3aa63a55f..41d84cc28 100644 --- a/src/app/ombul/[...yearAndName]/page.tsx +++ b/src/app/ombul/[...yearAndName]/page.tsx @@ -6,9 +6,9 @@ import PdfDocument from '@/components/PdfDocument/PdfDocument' import SlideInOnView from '@/components/SlideInOnView/SlideInOnView' import EditableTextField from '@/components/EditableTextField/EditableTextField' import CmsImage from '@/components/Cms/CmsImage/CmsImage' +import { ServerSession } from '@/auth/session/ServerSession' import { configureAction } from '@/services/configureAction' import { ombulAuth } from '@/services/ombul/auth' -import { Session } from '@/auth/session/Session' import Link from 'next/link' import { notFound } from 'next/navigation' @@ -33,7 +33,7 @@ export default async function Ombul({ params }: PropTypes) { const path = `/store/ombul/${ombul.fsLocation}` - const canUpdate = ombulAuth.update.dynamicFields({}).auth(await Session.fromNextAuth()).authorized + const canUpdate = ombulAuth.update.dynamicFields({}).auth(await ServerSession.fromNextAuth()).authorized const changeDescription = configureAction( updateOmbulAction, diff --git a/src/app/ombul/page.tsx b/src/app/ombul/page.tsx index 371080ed5..2207a7c2d 100644 --- a/src/app/ombul/page.tsx +++ b/src/app/ombul/page.tsx @@ -3,13 +3,13 @@ import CreateOmbul from './CreateOmbul' import OmbulCover from './OmbulCover' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { AddHeaderItemPopUp } from '@/components/HeaderItems/HeaderItemPopUp' +import { ServerSession } from '@/auth/session/ServerSession' import { readLatestOmbulAction, readOmbulsAction } from '@/services/ombul/actions' import { ombulAuth } from '@/services/ombul/auth' -import { Session } from '@/auth/session/Session' import type { ExpandedOmbul } from '@/services/ombul/types' export default async function Ombuls() { - const showCreateButton = ombulAuth.create.dynamicFields({}).auth(await Session.fromNextAuth()).authorized + const showCreateButton = ombulAuth.create.dynamicFields({}).auth(await ServerSession.fromNextAuth()).authorized const latestOmbulRes = await readLatestOmbulAction() const latestOmbul = latestOmbulRes.success ? latestOmbulRes.data : null diff --git a/src/app/omegaquotes/page.tsx b/src/app/omegaquotes/page.tsx index 6ee1852c8..18f4d6c88 100644 --- a/src/app/omegaquotes/page.tsx +++ b/src/app/omegaquotes/page.tsx @@ -4,14 +4,14 @@ import CreateOmegaquoteForm from './CreateOmegaquoteForm' import { OmegaquotePagingProvider } from '@/contexts/paging/OmegaquotesPaging' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readQuotesPageAction } from '@/services/omegaquotes/actions' +import { ServerSession } from '@/auth/session/ServerSession' import { omegaQuotesAuth } from '@/services/omegaquotes/auth' -import { Session } from '@/auth/session/Session' import { notFound } from 'next/navigation' import { v4 as uuid } from 'uuid' import type { PageSizeOmegaquote } from '@/contexts/paging/OmegaquotesPaging' export default async function OmegaQuotes() { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const showCreateButton = session.user && omegaQuotesAuth.create.dynamicFields({ userId: session.user.id }).auth(session).authorized || false diff --git a/src/app/users/[username]/(user-admin)/getProfileForAdmin.ts b/src/app/users/[username]/(user-admin)/getProfileForAdmin.ts index cf4a518c4..81da230a1 100644 --- a/src/app/users/[username]/(user-admin)/getProfileForAdmin.ts +++ b/src/app/users/[username]/(user-admin)/getProfileForAdmin.ts @@ -1,5 +1,5 @@ +import { ServerSession } from '@/auth/session/ServerSession' import { readUserProfileAction } from '@/services/users/actions' -import { Session } from '@/auth/session/Session' import { userAuth } from '@/services/users/auth' import { notFound, redirect } from 'next/navigation' @@ -15,7 +15,7 @@ type Params = { * @returns - The profile being seen and the session of the current user. */ export async function getProfileForAdmin({ username }: Params, adminPage: string) { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() if (username === 'me') { if (!session.user) return notFound() redirect(`/users/${session.user.username}/${adminPage}`) //This throws. diff --git a/src/app/users/[username]/(user-admin)/layout.tsx b/src/app/users/[username]/(user-admin)/layout.tsx index 0e39e9b07..129f02dbe 100644 --- a/src/app/users/[username]/(user-admin)/layout.tsx +++ b/src/app/users/[username]/(user-admin)/layout.tsx @@ -1,8 +1,8 @@ import styles from './layout.module.scss' import Nav from './Nav' -import { Session } from '@/auth/session/Session' import { readUserProfileAction } from '@/services/users/actions' import { unwrapActionReturn } from '@/app/redirectToErrorPage' +import { ServerSession } from '@/auth/session/ServerSession' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { notFound } from 'next/navigation' import Link from 'next/link' @@ -10,7 +10,7 @@ import type { ReactNode } from 'react' import type { PropTypes } from '@/app/users/[username]/page' export default async function UserAdmin({ children, params }: PropTypes & { children: ReactNode }) { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() let username = (await params).username if (username === 'me') { if (!session.user) return notFound() diff --git a/src/app/users/[username]/omegaid/page.tsx b/src/app/users/[username]/omegaid/page.tsx index 281050b7c..fbf575824 100644 --- a/src/app/users/[username]/omegaid/page.tsx +++ b/src/app/users/[username]/omegaid/page.tsx @@ -1,13 +1,13 @@ 'use server' import styles from './page.module.scss' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' import OmegaId from '@/components/OmegaId/identification/OmegaId' import { forbidden, notFound, redirect } from 'next/navigation' import type { PropTypes } from '@/app/users/[username]/page' export default async function OmegaIdPage({ params }: PropTypes) { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() const username = (await params).username if (!session.user) return notFound() diff --git a/src/app/users/[username]/page.tsx b/src/app/users/[username]/page.tsx index 98e49439d..ffe1d7fbf 100644 --- a/src/app/users/[username]/page.tsx +++ b/src/app/users/[username]/page.tsx @@ -1,11 +1,11 @@ import styles from './page.module.scss' import BorderButton from '@/components/UI/BorderButton' -import { Session } from '@/auth/session/Session' import { userAuth } from '@/services/users/auth' import ProfilePicture from '@/components/User/ProfilePicture' import UserDisplayName from '@/components/User/UserDisplayName' import { readUserProfileAction } from '@/services/users/actions' import { readSpecialImageAction } from '@/services/images/actions' +import { ServerSession } from '@/auth/session/ServerSession' import { sexConfig } from '@/services/users/constants' import Link from 'next/link' import { notFound, redirect } from 'next/navigation' @@ -18,7 +18,7 @@ export type PropTypes = { } export default async function User({ params }: PropTypes) { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() if ((await params).username === 'me') { if (!session.user) redirect('/login') redirect(`/users/${session.user.username}`) //This throws. diff --git a/src/auth/session/ServerSession.ts b/src/auth/session/ServerSession.ts new file mode 100644 index 000000000..13fe4ae99 --- /dev/null +++ b/src/auth/session/ServerSession.ts @@ -0,0 +1,67 @@ +import { Session, type UserGuaranteeOption } from './Session' +import { authOptions } from '@/auth/nextAuth/authOptions' +import { apiKeyOperations } from '@/services/apiKeys/operations' +import { apiKeyDecryptAndCompare } from '@/services/apiKeys/hashEncryptKey' +import { decodeApiKey } from '@/services/apiKeys/apiKeyEncoder' +import { ServerError } from '@/services/error' +import { permissionOperations } from '@/services/permissions/operations' +import { getServerSession as getSessionNextAuth } from 'next-auth' + +export class ServerSession extends Session { + public static async fromNextAuth(): Promise | Session<'HAS_USER'>> { + const { + user = null, + permissions = await permissionOperations.readDefaultPermissions({ + bypassAuth: true, + }), + memberships = [], + } = await getSessionNextAuth(authOptions) ?? {} + return new Session({ user, permissions, memberships }) + } + + /** + * This function generates a Session from an api-key. If the key is bad + * an error will be thrown. + * @param key - The key provided by client in the `id=keyId&key=key` format + * If the key is null, the session will be cratedwith only default permissios + */ + public static async fromApiKey(keyAndIdEncoded: string | null): Promise> { + const defaultPermissions = await permissionOperations.readDefaultPermissions({ + bypassAuth: true, + }) + if (!keyAndIdEncoded) return new Session<'NO_USER'>({ user: null, permissions: defaultPermissions, memberships: [] }) + const { id, key } = decodeApiKey(keyAndIdEncoded) + + const INVALID_API_KEY_MESSAGE = 'Api nøkkelen er ikke valid' + + let apiKeyFetch + + try { + apiKeyFetch = await apiKeyOperations.readWithHash({ + bypassAuth: true, + params: { id } + }) + } catch (e) { + if (e instanceof ServerError && e.errorCode === 'NOT FOUND') { + throw new ServerError('INVALID API KEY', INVALID_API_KEY_MESSAGE) + } + throw e + } + + const { keyHashEncrypted, active, permissions } = apiKeyFetch + + if (!active) throw new ServerError('INVALID API KEY', 'Api nøkkelen har utløpt') + + const success = await apiKeyDecryptAndCompare(key, keyHashEncrypted) + if (!success) throw new ServerError('INVALID API KEY', INVALID_API_KEY_MESSAGE) + + return new Session<'NO_USER'>({ + user: null, + permissions: [...defaultPermissions, ...permissions].filter( + (permission, i, ps) => ps.indexOf(permission) === i + ), + memberships: [], + apiKeyId: id, + }) + } +} diff --git a/src/auth/session/Session.ts b/src/auth/session/Session.ts index 6f038b1e0..614697cca 100644 --- a/src/auth/session/Session.ts +++ b/src/auth/session/Session.ts @@ -1,10 +1,3 @@ -import { authOptions } from '@/auth/nextAuth/authOptions' -import { apiKeyOperations } from '@/services/apiKeys/operations' -import { apiKeyDecryptAndCompare } from '@/services/apiKeys/hashEncryptKey' -import { decodeApiKey } from '@/services/apiKeys/apiKeyEncoder' -import { ServerError } from '@/services/error' -import { permissionOperations } from '@/services/permissions/operations' -import { getServerSession as getSessionNextAuth } from 'next-auth' import type { Permission } from '@prisma/client' import type { UserFiltered } from '@/services/users/types' import type { MembershipFiltered } from '@/services/groups/memberships/types' @@ -27,7 +20,7 @@ export type SessionMaybeUser = SessionType<'HAS_USER'> | SessionType<'NO_USER'> export class Session { private session: SessionType - private constructor(session: SessionType) { + protected constructor(session: SessionType) { this.session = session } @@ -68,63 +61,6 @@ export class Session { return new Session(jsObject) } - public static async fromNextAuth(): Promise | Session<'HAS_USER'>> { - const { - user = null, - permissions = await permissionOperations.readDefaultPermissions({ - bypassAuth: true, - }), - memberships = [], - } = await getSessionNextAuth(authOptions) ?? {} - return new Session({ user, permissions, memberships }) - } - - /** - * This function generates a Session from an api-key. If the key is bad - * an error will be thrown. - * @param key - The key provided by client in the `id=keyId&key=key` format - * If the key is null, the session will be cratedwith only default permissios - */ - public static async fromApiKey(keyAndIdEncoded: string | null): Promise> { - const defaultPermissions = await permissionOperations.readDefaultPermissions({ - bypassAuth: true, - }) - if (!keyAndIdEncoded) return new Session<'NO_USER'>({ user: null, permissions: defaultPermissions, memberships: [] }) - const { id, key } = decodeApiKey(keyAndIdEncoded) - - const INVALID_API_KEY_MESSAGE = 'Api nøkkelen er ikke valid' - - let apiKeyFetch - - try { - apiKeyFetch = await apiKeyOperations.readWithHash({ - bypassAuth: true, - params: { id } - }) - } catch (e) { - if (e instanceof ServerError && e.errorCode === 'NOT FOUND') { - throw new ServerError('INVALID API KEY', INVALID_API_KEY_MESSAGE) - } - throw e - } - - const { keyHashEncrypted, active, permissions } = apiKeyFetch - - if (!active) throw new ServerError('INVALID API KEY', 'Api nøkkelen har utløpt') - - const success = await apiKeyDecryptAndCompare(key, keyHashEncrypted) - if (!success) throw new ServerError('INVALID API KEY', INVALID_API_KEY_MESSAGE) - - return new Session<'NO_USER'>({ - user: null, - permissions: [...defaultPermissions, ...permissions].filter( - (permission, i, ps) => ps.indexOf(permission) === i - ), - memberships: [], - apiKeyId: id, - }) - } - public static fromDefaultPermissions(defaultPermissions: Permission[]) { return new Session({ permissions: defaultPermissions, memberships: [], user: null }) } diff --git a/src/services/omegaid/actions.ts b/src/services/omegaid/actions.ts index 5ca08a025..6dd2ad507 100644 --- a/src/services/omegaid/actions.ts +++ b/src/services/omegaid/actions.ts @@ -1,15 +1,15 @@ 'use server' +import { ServerSession } from '@/auth/session/ServerSession' import { createActionError } from '@/services/actionError' import { ServerError } from '@/services/error' import { generateOmegaId } from '@/services/omegaid/generate' -import { Session } from '@/auth/session/Session' import type { ActionReturn } from '@/services/actionTypes' export async function generateOmegaIdAction(): Promise> { //TODO: when changed to makeAction + operation it should take in a params: userId and //then auth on userId using the RequireUserId auther. - const user = (await Session.fromNextAuth()).user - if (!user) return createActionError('User not found') + const user = (await ServerSession.fromNextAuth()).user + if (!user) return createActionError('NOT FOUND', 'User not found') const token = generateOmegaId(user) diff --git a/src/services/screens/actions.ts b/src/services/screens/actions.ts index d345de45e..ee60f6928 100644 --- a/src/services/screens/actions.ts +++ b/src/services/screens/actions.ts @@ -1,8 +1,5 @@ 'use server' - -import { createActionError, createZodActionError, safeServerCall } from '@/services/actionError' -import { RequirePermission } from '@/auth/auther/RequirePermission' -import { Session } from '@/auth/session/Session' +import { createZodActionError, safeServerCall } from '@/services/actionError' import { createScreen } from '@/services/screens/create' import { destroyScreen } from '@/services/screens/destroy' import { readScreen, readScreens } from '@/services/screens/read' @@ -13,13 +10,7 @@ import type { ActionReturn } from '@/services/actionTypes' import type { CreateScreenTypes, UpdateScreenTypes } from '@/services/screens/validation' import type { Screen } from '@prisma/client' -export const adminScreenAuther = RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }) -export const readScreenAuther = RequirePermission.staticFields({ permission: 'SCREEN_READ' }) - export async function createScreenAction(formdata: CreateScreenTypes['Type']): Promise> { - const authRes = adminScreenAuther.dynamicFields({}).auth(await Session.fromNextAuth()) - if (!authRes.authorized) return createActionError(authRes.status) - const parse = createScreenValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) const data = parse.data @@ -28,30 +19,18 @@ export async function createScreenAction(formdata: CreateScreenTypes['Type']): P } export async function destroyScreenAction(id: number): Promise> { - const authRes = adminScreenAuther.dynamicFields({}).auth(await Session.fromNextAuth()) - if (!authRes.authorized) return createActionError(authRes.status) - return await safeServerCall(() => destroyScreen(id)) } export async function readScreenAction(id: number): Promise> { - const authRes = readScreenAuther.dynamicFields({}).auth(await Session.fromNextAuth()) - if (!authRes.authorized) return createActionError(authRes.status) - return await safeServerCall(() => readScreen(id)) } export async function readScreensAction(): Promise> { - const authRes = readScreenAuther.dynamicFields({}).auth(await Session.fromNextAuth()) - if (!authRes.authorized) return createActionError(authRes.status) - return await safeServerCall(() => readScreens()) } export async function updateScreenAction(id: number, formdata: UpdateScreenTypes['Type']): Promise> { - const authRes = adminScreenAuther.dynamicFields({}).auth(await Session.fromNextAuth()) - if (!authRes.authorized) return createActionError(authRes.status) - const parse = updateScreenValidation.typeValidate(formdata) if (!parse.success) return createZodActionError(parse) const data = parse.data @@ -63,8 +42,5 @@ export async function movePageInScreenAction( id: {screen: number, page: number}, direction: ScreenPageMoveDirection ): Promise> { - const authRes = adminScreenAuther.dynamicFields({}).auth(await Session.fromNextAuth()) - if (!authRes.authorized) return createActionError(authRes.status) - return await safeServerCall(() => movePageInScreen(id, direction)) } diff --git a/src/services/screens/auth.ts b/src/services/screens/auth.ts new file mode 100644 index 000000000..7672684a2 --- /dev/null +++ b/src/services/screens/auth.ts @@ -0,0 +1,10 @@ +import { RequirePermission } from '@/auth/auther/RequirePermission' + +export const screenAuth = { + create: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), + destroy: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), + read: RequirePermission.staticFields({ permission: 'SCREEN_READ' }), + readAll: RequirePermission.staticFields({ permission: 'SCREEN_READ' }), + update: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), + movePage: RequirePermission.staticFields({ permission: 'SCREEN_ADMIN' }), +} as const diff --git a/src/services/serverAction.ts b/src/services/serverAction.ts index 836809a25..efa86d92f 100644 --- a/src/services/serverAction.ts +++ b/src/services/serverAction.ts @@ -1,6 +1,6 @@ import '@pn-server-only' import { safeServerCall } from './actionError' -import { Session } from '@/auth/session/Session' +import { ServerSession } from '@/auth/session/ServerSession' import type { Action, ActionReturn } from './actionTypes' import type { ServiceOperation } from '@/services/serviceOperation' import type { z } from 'zod' @@ -93,7 +93,7 @@ export function makeAction< params: { params: unknown }, data: { data: unknown } | FormData, ): Promise> => { - const session = await Session.fromNextAuth() + const session = await ServerSession.fromNextAuth() let processedData: unknown if (data instanceof FormData) { From 68c67ab13b34ed7e7ed6b7238e8774ab4cbb417b Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:42:04 +0100 Subject: [PATCH 8/9] style: lint --- src/app/lockers/[id]/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/lockers/[id]/page.tsx b/src/app/lockers/[id]/page.tsx index 22627cac2..deeaeb72b 100644 --- a/src/app/lockers/[id]/page.tsx +++ b/src/app/lockers/[id]/page.tsx @@ -5,7 +5,6 @@ import UpdateLockerReservationForm from './UpdateLockerReservationForm' import PageWrapper from '@/components/PageWrapper/PageWrapper' import { readLockerAction } from '@/services/lockers/actions' import { checkGroupValidity, groupOperations, inferGroupName } from '@/services/groups/operations' -import { Session } from '@/auth/session/Session' import { RequireUser } from '@/auth/auther/RequireUser' import { ServerSession } from '@/auth/session/ServerSession' From 4cfc273006797566d4529b18fee8336f66da54ca Mon Sep 17 00:00:00 2001 From: JohanHjelsethStorstad <82723971+JohanHjelsethStorstad@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:49:32 +0100 Subject: [PATCH 9/9] fix: few old reference to changed names and interfaces --- src/app/omegaquotes/CreateOmegaquoteForm.tsx | 9 ++++++++- src/contexts/paging/OmegaquotesPaging.tsx | 2 +- src/services/cms/articleSections/implement.ts | 4 ++-- src/services/cms/articles/implement.ts | 4 ++-- src/services/omegaquotes/types.ts | 2 +- src/services/visibility/implement.ts | 6 +++--- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/app/omegaquotes/CreateOmegaquoteForm.tsx b/src/app/omegaquotes/CreateOmegaquoteForm.tsx index 21c8e6715..958b20233 100644 --- a/src/app/omegaquotes/CreateOmegaquoteForm.tsx +++ b/src/app/omegaquotes/CreateOmegaquoteForm.tsx @@ -7,9 +7,13 @@ import { createQuoteAction } from '@/services/omegaquotes/actions' import TextInput from '@/components/UI/TextInput' import Textarea from '@/components/UI/Textarea' import { useRouter } from 'next/navigation' +import { configureAction } from '@/services/configureAction' +import { useSession } from '@/auth/session/useSession' export default function CreateOmegaquoteForm() { const { refresh } = useRouter() + const session = useSession() + if (session.loading || !session.session.user) return null return ( diff --git a/src/contexts/paging/OmegaquotesPaging.tsx b/src/contexts/paging/OmegaquotesPaging.tsx index 3330e0f48..d617f3f79 100644 --- a/src/contexts/paging/OmegaquotesPaging.tsx +++ b/src/contexts/paging/OmegaquotesPaging.tsx @@ -10,6 +10,6 @@ export const [OmegaquotePagingContext, OmegaquotePagingProvider] = generatePagin OmegaquoteCursor, PageSizeOmegaquote >({ - fetcher: async ({ paging }) => await readQuotesPageAction(paging), + fetcher: async ({ paging }) => await readQuotesPageAction({ params: { paging } }), getCursor: ({ lastElement }) => ({ id: lastElement.id }), }) diff --git a/src/services/cms/articleSections/implement.ts b/src/services/cms/articleSections/implement.ts index 5717c6406..d341f3609 100644 --- a/src/services/cms/articleSections/implement.ts +++ b/src/services/cms/articleSections/implement.ts @@ -4,7 +4,7 @@ import { cmsParagraphOperations } from '@/cms/paragraphs/operations' import { cmsImageOperations } from '@/cms/images/operations' import { cmsLinkOperations } from '@/cms/links/operations' import type { articleSectionSchemas } from './schemas' -import type { AutherResult } from '@/auth/auther/Auther' +import type { AutherDynamicFieldsBound } from '@/auth/auther/Auther' import type { Prisma } from '@prisma/client' import type { z } from 'zod' import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation' @@ -35,7 +35,7 @@ export function implementUpdateArticleSectionOperations< prisma: PrismaPossibleTransaction, implementationParams: z.infer } - ) => AutherResult | Promise, + ) => AutherDynamicFieldsBound | Promise, ownedArticleSections: ( args: { prisma: PrismaPossibleTransaction, diff --git a/src/services/cms/articles/implement.ts b/src/services/cms/articles/implement.ts index 41875a227..77c13683b 100644 --- a/src/services/cms/articles/implement.ts +++ b/src/services/cms/articles/implement.ts @@ -2,7 +2,7 @@ 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 { AutherResult } from '@/auth/auther/Auther' +import type { AutherDynamicFieldsBound } from '@/auth/auther/Auther' import type { Prisma } from '@prisma/client' import type { articleSchemas } from './schemas' import type { z } from 'zod' @@ -37,7 +37,7 @@ export function implementUpdateArticleOperations< prisma: PrismaPossibleTransaction, implementationParams: z.infer } - ) => AutherResult | Promise, + ) => AutherDynamicFieldsBound | Promise, ownedArticles: ( args: { prisma: PrismaPossibleTransaction, diff --git a/src/services/omegaquotes/types.ts b/src/services/omegaquotes/types.ts index d864f103d..9e385c6ea 100644 --- a/src/services/omegaquotes/types.ts +++ b/src/services/omegaquotes/types.ts @@ -1,4 +1,4 @@ -import type { omegaQuoteFieldsToExpose } from './CofigVars' +import type { omegaQuoteFieldsToExpose } from './constants' import type { OmegaQuote } from '@prisma/client' export type OmegaquoteFiltered = Pick diff --git a/src/services/visibility/implement.ts b/src/services/visibility/implement.ts index 5bd231d03..2e4d7e1bd 100644 --- a/src/services/visibility/implement.ts +++ b/src/services/visibility/implement.ts @@ -1,6 +1,6 @@ import { visibilityOperations } from './operations' import type { ArgsAuthGetterAndOwnershipCheck, PrismaPossibleTransaction } from '@/services/serviceOperation' -import type { AutherResult } from '@/auth/auther/Auther' +import type { AutherDynamicFieldsBound } from '@/auth/auther/Auther' import type { Prisma } from '@prisma/client' import type { visibilitySchemas } from './schemas' import type { z } from 'zod' @@ -35,13 +35,13 @@ export function implementVisibilityOperations< prisma: PrismaPossibleTransaction, implementationParams: z.infer } - ) => AutherResult | Promise + ) => AutherDynamicFieldsBound | Promise read: ( args: { prisma: PrismaPossibleTransaction, implementationParams: z.infer } - ) => AutherResult | Promise + ) => AutherDynamicFieldsBound | Promise }, ownedVisibility: ( args: {