diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index 0ede1380d..14eb62854 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -2,6 +2,9 @@ import { getProviders } from '@/app/api/auth/[...nextauth]/auth-options'; import { getCurrentUser } from '@/components/auth'; import { redirect } from 'next/navigation'; import SignIn from './signin'; +import { generateGuestReferenceToken } from '@/lib/reference-guest-user-token'; + +const dayInMS = 1000 * 60 * 60 * 24; // take in search query const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: string } }) => { @@ -13,6 +16,11 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin redirect(callbackUrl); } + // NOTE: expiration should be the same as the expiration for sign in mails + const guestReferenceToken = isGuest + ? generateGuestReferenceToken({ guestId: session.user.id }, new Date(Date.now() + dayInMS)) + : undefined; + let providers = getProviders(); providers = providers.filter((provider) => !isGuest || 'development-users' !== provider.id); @@ -37,7 +45,9 @@ const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: strin if (!session) userType = 'none' as const; else userType = isGuest ? ('guest' as const) : ('user' as const); - return ; + return ( + + ); }; export default SignInPage; diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index af30cc870..73f2c759f 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -58,9 +58,13 @@ const signInTitle = ( const SignIn: FC<{ providers: ExtractedProvider[]; userType: 'guest' | 'user' | 'none'; -}> = ({ providers, userType }) => { + guestReferenceToken?: string; +}> = ({ providers, userType, guestReferenceToken }) => { const searchParams = useSearchParams(); const callbackUrl = searchParams.get('callbackUrl') ?? undefined; + const callbackUrlWithGuestRef = guestReferenceToken + ? `/transfer-processes?referenceToken=${guestReferenceToken}&callbackUrl=${callbackUrl}` + : callbackUrl; const authError = searchParams.get('error'); const oauthProviders = providers.filter((provider) => provider.type === 'oauth'); @@ -150,7 +154,9 @@ const SignIn: FC<{ if (provider.type === 'credentials') { return (
signIn(provider.id, { ...values, callbackUrl })} + onFinish={(values) => + signIn(provider.id, { ...values, callbackUrl: callbackUrlWithGuestRef }) + } key={provider.id} layout="vertical" > @@ -168,7 +174,9 @@ const SignIn: FC<{ return ( <> signIn(provider.id, { ...values, callbackUrl })} + onFinish={(values) => + signIn(provider.id, { ...values, callbackUrl: callbackUrlWithGuestRef }) + } key={provider.id} layout="vertical" > @@ -211,7 +219,7 @@ const SignIn: FC<{ style={{ width: '1.5rem', height: 'auto' }} /> } - onClick={() => signIn(provider.id, { callbackUrl })} + onClick={() => signIn(provider.id, { callbackUrl: callbackUrlWithGuestRef })} /> ); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx index bfe3d8e02..6f12bb616 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[processId]/page.tsx @@ -1,4 +1,4 @@ -import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; +import { getCurrentEnvironment } from '@/components/auth'; import Wrapper from './wrapper'; import styles from './page.module.scss'; import Modeler from './modeler'; @@ -18,11 +18,10 @@ const Process = async ({ params: { processId, environmentId }, searchParams }: P //console.log('processId', processId); //console.log('query', searchParams); const selectedVersionId = searchParams.version ? searchParams.version : undefined; - const { ability } = await getCurrentEnvironment(environmentId); - const { userId } = await getCurrentUser(); + const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); // Only load bpmn if no version selected. const process = await getProcess(processId, !selectedVersionId); - const processes = await getProcesses(userId, ability, false); + const processes = await getProcesses(activeEnvironment.spaceId, ability, false); if (!ability.can('view', toCaslResource('Process', process))) { throw new Error('Forbidden.'); @@ -35,7 +34,7 @@ const Process = async ({ params: { processId, environmentId }, searchParams }: P ? process.versions.find((version) => version.id === selectedVersionId) : undefined; - // Since the user is able to minimize and close the page, everyting is in a + // Since the user is able to minimize and close the page, everything is in a // client component from here. return ( <> diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts b/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts index 05e82bd8e..72b0db5c5 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/adapter.ts @@ -29,7 +29,7 @@ const Adapter = { return getUserById(id); }, updateUser: async (user: AuthenticatedUser) => { - return updateUser(user.id, user); + return updateUser(user.id, { ...user, isGuest: false }); }, getUserByEmail: async (email: string) => { return getUserByEmail(email) ?? null; diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts index c4e43a363..35d78cbe6 100644 --- a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts +++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts @@ -63,16 +63,25 @@ const nextAuthOptions: AuthOptions = { return session; }, - signIn: async ({ account, user: _user }) => { + signIn: async ({ account, user: _user, email }) => { const session = await getServerSession(nextAuthOptions); const sessionUser = session?.user; - if (sessionUser?.isGuest && account?.provider !== 'guest-loguin') { + // Guest account signs in with proper auth + if ( + sessionUser?.isGuest && + account?.provider !== 'guest-signin' && + !email?.verificationRequest + ) { + // Check if the user's cookie is correct + const sessionUserInDb = await getUserById(sessionUser.id); + if (!sessionUserInDb || !sessionUserInDb.isGuest) throw new Error('Something went wrong'); + const user = _user as Partial; - const guestUser = await getUserById(sessionUser.id); + const userSigningIn = await getUserById(_user.id); - if (guestUser?.isGuest) { - updateUser(guestUser.id, { + if (!userSigningIn) { + await updateUser(sessionUser.id, { firstName: user.firstName ?? undefined, lastName: user.lastName ?? undefined, username: user.username ?? undefined, diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index 5d5a4f3f1..d96ddfbc5 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -54,7 +54,7 @@ const getProcessInfo = async ( const { ability, activeEnvironment } = await getCurrentEnvironment(session?.user.id); ({ spaceId } = activeEnvironment); // get all the processes the user has access to - const ownedProcesses = await getProcesses(userId, ability); + const ownedProcesses = await getProcesses(spaceId, ability); // check if the current user is the owner of the process(/has access to the process) => if yes give access regardless of sharing status isOwner = ownedProcesses.some((process) => process.id === definitionId); } diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx new file mode 100644 index 000000000..073b072be --- /dev/null +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -0,0 +1,71 @@ +import { getCurrentUser } from '@/components/auth'; +import Content from '@/components/content'; +import { getProcesses, getUserById } from '@/lib/data/DTOs'; +import { Card, Result } from 'antd'; +import { redirect } from 'next/navigation'; +import ProcessTransferButtons from './transfer-processes-confirmation-buttons'; +import { getGuestReference } from '@/lib/reference-guest-user-token'; + +export default async function TransferProcessesPage({ + searchParams, +}: { + searchParams: { + callbackUrl?: string; + referenceToken?: string; + }; +}) { + const { userId, session } = await getCurrentUser(); + if (!session) redirect('api/auth/signin'); + if (session.user.isGuest) redirect('/'); + + const callbackUrl = decodeURIComponent(searchParams.callbackUrl || '/'); + + const token = decodeURIComponent(searchParams.referenceToken || ''); + const referenceToken = getGuestReference(token); + if ('error' in referenceToken) { + let message = 'Invalid link'; + if (referenceToken.error === 'TokenExpiredError') message = 'Link expired'; + + return ( + + + + ); + } + const guestId = referenceToken.guestId; + + // guestId === userId if the user signed in with a non existing account, and the guest user was + // turned into an authenticated user + if (!guestId || guestId === userId) redirect(callbackUrl); + + const possibleGuest = await getUserById(guestId); + // possibleGuest might be a normal user, this would happen if the user signed in with an existing + // accocunt, generating the token above, and before using it, he signed in with a new account. + // We only go further then this redirect, if the user signed in with an account that was + // already linked to an existing user + if (!possibleGuest || !possibleGuest.isGuest) redirect(callbackUrl); + + // NOTE: this ignores folders + const guestProcesses = await getProcesses(guestId); + + // If the guest has no processes -> nothing to do + if (guestProcesses.length === 0) redirect(callbackUrl); + + return ( + + + Your guest account had {guestProcesses.length} process{guestProcesses.length !== 1 && 'es'}. +
+ Would you like to transfer them to your account? + +
+
+ ); +} diff --git a/src/management-system-v2/app/transfer-processes/server-actions.ts b/src/management-system-v2/app/transfer-processes/server-actions.ts new file mode 100644 index 000000000..50519b208 --- /dev/null +++ b/src/management-system-v2/app/transfer-processes/server-actions.ts @@ -0,0 +1,88 @@ +'use server'; + +import { getCurrentUser } from '@/components/auth'; +import { Folder } from '@/lib/data/folder-schema'; +import { + getProcesses, + getFolders, + getRootFolder, + moveFolder, + updateFolderMetaData, + updateProcess, + getUserById, + deleteUser, +} from '@/lib/data/DTOs'; +import { Process } from '@/lib/data/process-schema'; +import { getGuestReference } from '@/lib/reference-guest-user-token'; +import { UserErrorType, userError } from '@/lib/user-error'; +import { redirect } from 'next/navigation'; + +export async function transferProcesses(referenceToken: string, callbackUrl: string = '/') { + const { session } = await getCurrentUser(); + if (!session) return userError("You're not signed in", UserErrorType.PermissionError); + if (session.user.isGuest) + return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); + + const reference = getGuestReference(referenceToken); + if ('error' in reference) return userError(reference.error); + const guestId = reference.guestId; + + if (guestId === session.user.id) redirect(callbackUrl); + + const possibleGuest = await getUserById(guestId); + if (!possibleGuest || !possibleGuest.isGuest) + return userError('Invalid guest id', UserErrorType.PermissionError); + + // Processes and folders under root folder of guest space guet their folderId changed to the + // root folder of the new owner space, for the rest we just update the environmentId + const userRootFolderId = (await getRootFolder(session.user.id)).id; + const guestRootFolderId = (await getRootFolder(guestId)).id; + + // no ability check necessary, owners of personal spaces can do anything + const guestProcesses = await getProcesses(guestId); + for (const process of guestProcesses) { + const processUpdate: Partial = { + environmentId: session.user.id, + creatorId: session.user.id, + }; + if (process.folderId === guestRootFolderId) processUpdate.folderId = userRootFolderId; + await updateProcess(process.id, processUpdate); + } + + const guestFolders = await getFolders(guestId); + for (const folder of guestFolders) { + if (folder.id === guestRootFolderId) continue; + + const folderData: Partial = { createdBy: session.user.id }; + + if (folder.parentId === guestRootFolderId) moveFolder(folder.id, userRootFolderId); + else folderData.environmentId = session.user.id; + + updateFolderMetaData(folder.id, folderData); + } + + deleteUser(guestId); + + redirect(callbackUrl); +} + +export async function discardProcesses(referenceToken: string, redirectUrl: string = '/') { + const { session } = await getCurrentUser(); + if (!session) return userError("You're not signed in", UserErrorType.PermissionError); + if (session.user.isGuest) + return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); + + const reference = getGuestReference(referenceToken); + if ('error' in reference) return userError(reference.error); + const guestId = reference.guestId; + + if (guestId === session.user.id) redirect(redirectUrl); + + const possibleGuest = await getUserById(guestId); + if (!possibleGuest || !possibleGuest.isGuest) + return userError('Invalid guest id', UserErrorType.PermissionError); + + deleteUser(guestId); + + redirect(redirectUrl); +} diff --git a/src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx b/src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx new file mode 100644 index 000000000..174c57a54 --- /dev/null +++ b/src/management-system-v2/app/transfer-processes/transfer-processes-confirmation-buttons.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { Space, Button } from 'antd'; +import { ReactNode, useTransition } from 'react'; +import { + transferProcesses as serverTransferProcesses, + discardProcesses as serverDiscardProcesses, +} from './server-actions'; + +export default function ProcessTransferButtons({ + referenceToken, + callbackUrl, +}: { + referenceToken: string; + callbackUrl?: string; + children?: ReactNode; +}) { + const [discardingProcesses, startDiscardingProcesses] = useTransition(); + function discardProcesses() { + startDiscardingProcesses(async () => { + await serverDiscardProcesses(referenceToken, callbackUrl); + }); + } + + const [transferring, startTransfer] = useTransition(); + function transferProcesses() { + startTransfer(async () => { + await serverTransferProcesses(referenceToken, callbackUrl); + }); + } + + return ( + + + + + ); +} diff --git a/src/management-system-v2/lib/change-email/server-actions.ts b/src/management-system-v2/lib/change-email/server-actions.ts index b8efc3ba0..2ec4d8e7f 100644 --- a/src/management-system-v2/lib/change-email/server-actions.ts +++ b/src/management-system-v2/lib/change-email/server-actions.ts @@ -67,7 +67,7 @@ export async function changeEmail(token: string, identifier: string, cancel: boo ) return userError('Invalid token'); - if (!cancel) updateUser(userId, { email: verificationToken.identifier }); + if (!cancel) updateUser(userId, { email: verificationToken.identifier, isGuest: false }); deleteVerificationToken(tokenParams); } diff --git a/src/management-system-v2/lib/data/DTOs.ts b/src/management-system-v2/lib/data/DTOs.ts index ff1a73f6d..d6ab2cc0f 100644 --- a/src/management-system-v2/lib/data/DTOs.ts +++ b/src/management-system-v2/lib/data/DTOs.ts @@ -28,10 +28,10 @@ export async function getProcess(processDefinitionsId: string, includeBPMN = fal : await processModuleLegacy.getProcess(processDefinitionsId, includeBPMN); } -export async function getProcesses(userId: string, ability: Ability, includeBPMN = false) { +export async function getProcesses(environmentId: string, ability?: Ability, includeBPMN = false) { return enableUseDB - ? await processModuleDB.getProcesses(userId, ability, includeBPMN) - : await processModuleLegacy.getProcesses(userId, ability, includeBPMN); + ? await processModuleDB.getProcesses(environmentId, ability, includeBPMN) + : await processModuleLegacy.getProcesses(environmentId, ability, includeBPMN); } export async function getProcessBpmn(processDefinitionsId: string) { @@ -45,6 +45,12 @@ export async function getProcessVersionBpmn(processDefinitionsId: string, versio : await processModuleLegacy.getProcessVersionBpmn(processDefinitionsId, version); } +export async function updateProcess(...args: Parameters) { + return enableUseDB + ? await processModuleDB.updateProcess(...args) + : await processModuleLegacy.updateProcess(...args); +} + export async function getFolderContents(folderId: string, ability?: Ability) { return enableUseDB ? await folderModuleDB.getFolderContents(folderId, ability) @@ -63,6 +69,20 @@ export async function getFolderById(folderId: string, ability?: Ability) { : await folderModuleLegacy.getFolderById(folderId, ability); } +export async function moveFolder(folderId: string, newParentId: string, ability?: Ability) { + return enableUseDB + ? await folderModuleDB.moveFolder(folderId, newParentId, ability) + : await folderModuleLegacy.moveFolder(folderId, newParentId, ability); +} + +export async function updateFolderMetaData( + ...args: Parameters +) { + return enableUseDB + ? await folderModuleDB.updateFolderMetaData(...args) + : await folderModuleLegacy.updateFolderMetaData(...args); +} + export async function deleteEnvironment(environmentId: string, ability?: Ability) { return enableUseDB ? await environmentModuleDB.deleteEnvironment(environmentId, ability) diff --git a/src/management-system-v2/lib/data/db/process.ts b/src/management-system-v2/lib/data/db/process.ts index fbacb36c2..479a6de0c 100644 --- a/src/management-system-v2/lib/data/db/process.ts +++ b/src/management-system-v2/lib/data/db/process.ts @@ -21,11 +21,14 @@ import { asyncMap } from '@/lib/helpers/javascriptHelpers'; import { copyFile } from '../file-manager/file-manager'; import { generateProcessFilePath } from '@/lib/helpers/fileManagerHelpers'; -/** Returns all processes for a user */ -export async function getProcesses(userId: string, ability: Ability, includeBPMN = false) { - const userProcesses = await db.process.findMany({ +/** + * Returns all processes in an environment + * If you want the processes for a specific user, you have to provide his ability + * */ +export async function getProcesses(environmentId: string, ability?: Ability, includeBPMN = false) { + const spaceProcesses = await db.process.findMany({ where: { - creatorId: userId, + environmentId, }, select: { id: true, @@ -50,10 +53,9 @@ export async function getProcesses(userId: string, ability: Ability, includeBPMN }, }); - //TODO: ability check ? is it really necessary in this case? //TODO: add pagination - return userProcesses; + return ability ? ability.filter('view', 'Process', spaceProcesses) : spaceProcesses; } export async function getProcess(processDefinitionsId: string, includeBPMN = false) { diff --git a/src/management-system-v2/lib/data/legacy/_process.ts b/src/management-system-v2/lib/data/legacy/_process.ts index ee3fc6d12..36ff7b3b1 100644 --- a/src/management-system-v2/lib/data/legacy/_process.ts +++ b/src/management-system-v2/lib/data/legacy/_process.ts @@ -58,18 +58,15 @@ export function getProcessMetaObjects() { } /** Returns all processes for a user */ -export async function getProcesses(userId: string, ability: Ability, includeBPMN = false) { - const processes = Object.values(processMetaObjects); - - const userProcesses = await Promise.all( - ability - .filter('view', 'Process', processes) - .map(async (process) => - !includeBPMN ? process : { ...process, bpmn: getProcessBpmn(process.id) }, - ), +export async function getProcesses(environmentId: string, ability?: Ability, includeBPMN = false) { + const spaceProcesses = Object.values(processMetaObjects).filter( + (process) => process.environmentId === environmentId, ); - return userProcesses; + const processes = ability ? ability.filter('view', 'Process', spaceProcesses) : spaceProcesses; + + if (!includeBPMN) return processes; + return processes.map((process) => ({ ...process, bpmn: getProcessBpmn(process.id) })); } export async function getProcess(processDefinitionsId: string, includeBPMN = false) { @@ -213,7 +210,7 @@ export function moveProcess({ if (!dontUpdateOldFolder) { const oldFolder = foldersMetaObject.folders[process.folderId]; - if (!oldFolder) throw new Error("Consistensy Error: Process' folder not found"); + if (!oldFolder) throw new Error("Consistency Error: Process' folder not found"); const processOldFolderIdx = oldFolder.children.findIndex( (item) => 'type' in item && item.type === 'process' && item.id === processDefinitionsId, ); @@ -316,7 +313,7 @@ export async function addProcessVersion(processDefinitionsId: string, bpmn: stri await saveProcessVersion(processDefinitionsId, versionCreatedOn, bpmn); - // add information about the new version to the meta information and inform others about its existance + // add information about the new version to the meta information and inform others about its existence const newVersions = existingProcess.versions ? [...existingProcess.versions] : []; newVersions.push({ diff --git a/src/management-system-v2/lib/data/legacy/folders.ts b/src/management-system-v2/lib/data/legacy/folders.ts index 6c65c4851..a982cf24a 100644 --- a/src/management-system-v2/lib/data/legacy/folders.ts +++ b/src/management-system-v2/lib/data/legacy/folders.ts @@ -109,13 +109,19 @@ export async function getFolderById(folderId: string, ability?: Ability) { return folderData.folder as Folder; } -export function getFolders(spaceId?: string) { - const folders = Object.values(foldersMetaObject.folders); - const selection = spaceId - ? folders.filter((folder) => folder?.folder.environmentId === spaceId) - : folders; +export function getFolders(environmentId?: string, ability?: Ability) { + const _folders = environmentId + ? Object.values(foldersMetaObject.folders).filter( + (folder) => folder?.folder.environmentId === environmentId, + ) + : Object.values(foldersMetaObject.folders); - return selection.map((folder) => folder!.folder); + const folders = _folders.map((f) => f!.folder); + + if (ability) + return folders.filter((folder) => ability.can('view', toCaslResource('Folder', folder))); + + return folders; } export async function getFolderChildren(folderId: string, ability?: Ability) { @@ -243,7 +249,7 @@ function _deleteFolder( export async function updateFolderMetaData( folderId: string, - newMetaDataInput: Partial, + newMetaDataInput: Partial>, ability?: Ability, ) { const folderData = foldersMetaObject.folders[folderId]; @@ -252,12 +258,15 @@ export async function updateFolderMetaData( if (ability && !ability.can('update', toCaslResource('Folder', folderData.folder))) throw new UnauthorizedError(); - const newMetaData = FolderUserInputSchema.partial().parse(newMetaDataInput); - if ( - newMetaDataInput.environmentId && - newMetaDataInput.environmentId != folderData.folder.environmentId - ) - throw new Error('environmentId cannot be changed'); + const newMetaData = FolderSchema.omit({ + parentId: true, + id: true, + // if there is an ability, we interpret this as a user updating the folder + environmentId: ability ? true : undefined, + createdBy: ability ? true : undefined, + }) + .partial() + .parse(newMetaDataInput); const newFolder: Folder = { ...folderData.folder, @@ -295,7 +304,8 @@ export async function moveFolder(folderId: string, newParentId: string, ability? const newParentData = foldersMetaObject.folders[newParentId]; if (!newParentData) throw new Error('New parent folder not found'); - if (newParentData.folder.environmentId !== folderData.folder.environmentId) + // only perform this check when an ability is present (it means that a user is moving the folder) + if (ability && newParentData.folder.environmentId !== folderData.folder.environmentId) throw new Error('Cannot move folder to a different environment'); const oldParentData = foldersMetaObject.folders[folderData.folder.parentId]; @@ -326,6 +336,7 @@ export async function moveFolder(folderId: string, newParentId: string, ability? store.update('folders', oldParentData.folder.id, oldParentData.folder); folderData.folder.parentId = newParentId; + folderData.folder.environmentId = newParentData.folder.environmentId; newParentData.children.push({ type: 'folder', id: folderData.folder.id }); newParentData.folder.lastEditedOn = new Date(); store.update('folders', newParentData.folder.id, newParentData.folder); diff --git a/src/management-system-v2/lib/data/legacy/iam/environments.ts b/src/management-system-v2/lib/data/legacy/iam/environments.ts index 09493259d..c2cc1ba41 100644 --- a/src/management-system-v2/lib/data/legacy/iam/environments.ts +++ b/src/management-system-v2/lib/data/legacy/iam/environments.ts @@ -13,7 +13,7 @@ import { environmentSchema, } from '../../environment-schema'; import { getProcessMetaObjects, removeProcess } from '../_process'; -import { createFolder } from '../folders'; +import { createFolder, deleteFolder, getRootFolder } from '../folders'; import { deleteLogo, getLogo, hasLogo, saveLogo } from '../fileHandling.js'; import { toCaslResource } from '@/lib/ability/caslAbility'; import { env } from '@/lib/env-vars.js'; @@ -125,28 +125,27 @@ export async function deleteEnvironment(environmentId: string, ability?: Ability if (ability && !ability.can('delete', 'Environment')) throw new UnauthorizedError(); - const roles = Object.values(roleMetaObjects); - for (const role of roles) { - if (role.environmentId === environmentId) { - deleteRole(role.id); // also deletes role mappings - } - } - - const processes = Object.values(getProcessMetaObjects()); - for (const process of processes) { - if (process.environmentId === environmentId) { - removeProcess(process.id); - } - } + // NOTE: when using a db I think it would be faster to just delete processes and folders where de + // environmentId matches + const rootFolder = await getRootFolder(environmentId); + if (!rootFolder) throw new Error('Root folder not found'); + await deleteFolder(rootFolder.id); if (environment.isOrganization) { const environmentMemberships = membershipMetaObject[environmentId]; if (environmentMemberships) { for (const { userId } of environmentMemberships) { - removeMember(environmentId, userId); + await removeMember(environmentId, userId); } delete membershipMetaObject[environmentId]; } + + const roles = Object.values(roleMetaObjects); + for (const role of roles) { + if (role.environmentId === environmentId) { + await deleteRole(role.id); // also deletes role mappings + } + } } delete environmentsMetaObject[environmentId]; diff --git a/src/management-system-v2/lib/data/legacy/iam/users.ts b/src/management-system-v2/lib/data/legacy/iam/users.ts index 7f944a879..5e2d38f28 100644 --- a/src/management-system-v2/lib/data/legacy/iam/users.ts +++ b/src/management-system-v2/lib/data/legacy/iam/users.ts @@ -7,6 +7,8 @@ import { OauthAccount, AuthenticatedUser, AuthenticatedUserSchema, + GuestUser, + GuestUserSchema, } from '../../user-schema'; import { addEnvironment, deleteEnvironment } from './environments'; import { OptionalKeys } from '@/lib/typescript-utils.js'; diff --git a/src/management-system-v2/lib/data/legacy/verification-tokens.ts b/src/management-system-v2/lib/data/legacy/verification-tokens.ts index 3c274926c..0babcf854 100644 --- a/src/management-system-v2/lib/data/legacy/verification-tokens.ts +++ b/src/management-system-v2/lib/data/legacy/verification-tokens.ts @@ -1,17 +1,17 @@ import store from './store.js'; import { z } from 'zod'; -const baseTokenSchema = z.object({ - token: z.string(), - identifier: z.string(), - expires: z.date(), -}); - const verificationTokenSchema = z.union([ - baseTokenSchema.extend({ + z.object({ + token: z.string(), + identifier: z.string(), + expires: z.date(), updateEmail: z.literal(false).optional(), }), - baseTokenSchema.extend({ + z.object({ + token: z.string(), + identifier: z.string(), + expires: z.date(), updateEmail: z.literal(true), userId: z.string(), }), diff --git a/src/management-system-v2/lib/data/users.tsx b/src/management-system-v2/lib/data/users.tsx index 6cf30ee54..5a054a3d9 100644 --- a/src/management-system-v2/lib/data/users.tsx +++ b/src/management-system-v2/lib/data/users.tsx @@ -53,7 +53,12 @@ export async function deleteUser() {
    {conflictingOrgsNames.map((name, idx) => (
  • - {name}: manage roles here + {name}:{' '} + + manage roles here +
  • ))}
@@ -73,10 +78,15 @@ export async function updateUser(newUserDataInput: AuthenticatedUserData) { try { const { userId } = await getCurrentUser(); + const user = await getUserById(userId); + + if (user?.isGuest) { + return userError('Guest users cannot be updated'); + } const newUserData = AuthenticatedUserDataSchema.parse(newUserDataInput); - _updateUser(userId, newUserData); + _updateUser(userId, { ...newUserData }); } catch (_) { return userError('Error updating user'); } diff --git a/src/management-system-v2/lib/env-vars.ts b/src/management-system-v2/lib/env-vars.ts index 62e4e517a..86e49c839 100644 --- a/src/management-system-v2/lib/env-vars.ts +++ b/src/management-system-v2/lib/env-vars.ts @@ -57,12 +57,15 @@ const environmentVariables = { TWITTER_CLIENT_SECRET: z.string(), SHARING_ENCRYPTION_SECRET: z.string(), + + GUEST_REFERENCE_SECRET: z.string(), }, development: { SHARING_ENCRYPTION_SECRET: z.string().default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), INVITATION_ENCRYPTION_SECRET: z .string() .default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), + GUEST_REFERENCE_SECRET: z.string().default('T8VB/r1dw0kJAXjanUvGXpDb+VRr4dV5y59BT9TBqiQ='), }, test: {}, } satisfies EnvironmentVariables; diff --git a/src/management-system-v2/lib/reference-guest-user-token.ts b/src/management-system-v2/lib/reference-guest-user-token.ts new file mode 100644 index 000000000..19d6e45b1 --- /dev/null +++ b/src/management-system-v2/lib/reference-guest-user-token.ts @@ -0,0 +1,32 @@ +/** + * When a user authenticates himself, and it is detected, that he was signed in as a guest + * a link with this token is set as the redirect url, after he signs he will be asked + * if he wants to transfer the guest processes. + * The token is necessary, because otherwise you could write any guest user id + * this is a small attack surface, but it is better to be safe. + * * */ +import { z } from 'zod'; +import { env } from './env-vars'; +import jwt from 'jsonwebtoken'; + +const referenceSchema = z.object({ guestId: z.string() }); +type Reference = z.infer; + +export function generateGuestReferenceToken(invitation: Reference, expiration: Date) { + // in seconds + const expiresIn = (expiration.getTime() - Date.now()) / 1000; + + return jwt.sign(invitation, env.GUEST_REFERENCE_SECRET, { expiresIn }); +} + +export function getGuestReference(token: string) { + try { + const payload = jwt.verify(token, env.GUEST_REFERENCE_SECRET); + return referenceSchema.parse(payload); + } catch (error) { + if (error instanceof jwt.JsonWebTokenError && error.name === 'TokenExpiredError') { + return { error: 'TokenExpiredError' as const }; + } + return { error: 'error' as const }; + } +}