diff --git a/src/management-system-v2/app/admin/ms-config/ms-config-form.tsx b/src/management-system-v2/app/admin/ms-config/ms-config-form.tsx index c2056f224..1c060f77b 100644 --- a/src/management-system-v2/app/admin/ms-config/ms-config-form.tsx +++ b/src/management-system-v2/app/admin/ms-config/ms-config-form.tsx @@ -107,7 +107,7 @@ export default function MSConfigForm({ // Get context for the setting const overridden = overwrittenByEnv.includes(configKey); const envOnly = mSConfigEnvironmentOnlyKeys.includes(configKey as any); - const disabled = pathParts.find((part) => disabledGroups.get(part)); + const disabledKey = pathParts.find((part) => disabledGroups.get(part)); // This is needed, for when a key that can disable a group, is part of a group that is // disabled @@ -127,7 +127,7 @@ export default function MSConfigForm({ !envOnly && !overridden && !parentGroupDisabled && - (!disabled || disablers.length > 0) + (!disabledKey || disablers.length > 0) ) return; @@ -150,7 +150,7 @@ export default function MSConfigForm({ tooltipMessage = 'This config was overridden by an environment variable'; else if (envOnly) tooltipMessage = 'This setting can only be changed through environment variables'; - else if (disabled) tooltipMessage = `Disabled by ${disabled}`; + else if (disabledKey) tooltipMessage = `Disabled by ${disabledGroups.get(disabledKey)}`; return { input: {input}, diff --git a/src/management-system-v2/app/admin/ms-config/page.tsx b/src/management-system-v2/app/admin/ms-config/page.tsx index b7173673c..517db07f6 100644 --- a/src/management-system-v2/app/admin/ms-config/page.tsx +++ b/src/management-system-v2/app/admin/ms-config/page.tsx @@ -7,11 +7,17 @@ import { getMSConfig, updateMSConfig, writeDefaultMSConfig } from '@/lib/ms-conf import MSConfigForm from './ms-config-form'; import { userError } from '@/lib/user-error'; import { SettingGroup } from '@/app/(dashboard)/[environmentId]/settings/type-util'; +import { ConfigurableMSConfig } from '@/lib/ms-config/config-schema'; +import { restartInternalSchedulerWithCurrentConfigs } from '@/lib/scheduler'; -async function saveConfig(newConfig: Record) { +async function saveConfig(changedValues: Record) { 'use server'; try { - await updateMSConfig(newConfig); + await updateMSConfig(changedValues); + + if ('SCHEDULER_INTERNAL_ACTIVE' in changedValues || 'SCHEDULER_INTERVAL' in changedValues) { + await restartInternalSchedulerWithCurrentConfigs(); + } } catch (e) { return userError('Error saving config'); } @@ -289,15 +295,47 @@ async function ConfigPage() { }, { type: 'boolean', - name: 'SCHEDULER_JOB_DELETE_INACTIVE_GUESTS', - key: 'SCHEDULER_JOB_DELETE_INACTIVE_GUESTS', - value: msConfig.SCHEDULER_JOB_DELETE_INACTIVE_GUESTS, + name: 'SCHEDULER_INTERNAL_ACTIVE', + key: 'SCHEDULER_INTERNAL_ACTIVE', + value: msConfig.SCHEDULER_INTERNAL_ACTIVE, }, { - type: 'boolean', - name: 'SCHEDULER_JOB_DELETE_OLD_ARTIFACTS', - key: 'SCHEDULER_JOB_DELETE_OLD_ARTIFACTS', - value: msConfig.SCHEDULER_JOB_DELETE_OLD_ARTIFACTS, + type: 'number', + name: 'SCHEDULER_TASK_DELETE_INACTIVE_GUESTS', + key: 'SCHEDULER_TASK_DELETE_INACTIVE_GUESTS', + value: msConfig.SCHEDULER_TASK_DELETE_INACTIVE_GUESTS, + }, + { + type: 'number', + name: 'SCHEDULER_TASK_DELETE_OLD_ARTIFACTS', + key: 'SCHEDULER_TASK_DELETE_OLD_ARTIFACTS', + value: msConfig.SCHEDULER_TASK_DELETE_OLD_ARTIFACTS, + }, + { + type: 'number', + name: 'SCHEDULER_TASK_DELETE_INACTIVE_SPACES', + key: 'SCHEDULER_TASK_DELETE_INACTIVE_SPACES', + value: msConfig.SCHEDULER_TASK_DELETE_INACTIVE_SPACES, + }, + { + type: 'number', + name: 'SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_REGISTRATION_TOKENS', + key: 'SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_REGISTRATION_TOKENS', + value: msConfig.SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_REGISTRATION_TOKENS, + }, + { + type: 'number', + name: 'SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_CHANGE_TOKENS', + key: 'SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_CHANGE_TOKENS', + value: msConfig.SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_CHANGE_TOKENS, + }, + { + type: 'number', + name: 'SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_VERIFICATION_TOKENS', + key: 'SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_VERIFICATION_TOKENS', + value: msConfig.SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_VERIFICATION_TOKENS, + // NOTE: eventually remove this description + description: 'This value will only take effect after restarting the system', }, ], }, diff --git a/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts b/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts deleted file mode 100644 index 3bc7cf608..000000000 --- a/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { deleteInactiveGuestUsers } from '@/lib/data/db/iam/users'; - -// TODO: get this time from the database -const GUESET_INACTIVE_TIME = 1000 * 60 * 60 * 24 * 30; // 30 days - -export async function POST(request: NextRequest) { - try { - // Extract and validate the Bearer token - const authHeader = request.headers.get('Authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Unauthorized: Missing Bearer token' }, { status: 401 }); - } - - const token = authHeader.replace('Bearer ', '').trim(); - // TODO: use different token - if (token !== process.env.SWEEPER_TRIGGER_TOKEN) { - return NextResponse.json({ error: 'Unauthorized: Invalid Bearer token' }, { status: 403 }); - } - - const { count } = await deleteInactiveGuestUsers(GUESET_INACTIVE_TIME); - - return NextResponse.json( - { - message: `${count} guest users deleted`, - }, - { status: 200 }, - ); - } catch (error) { - console.error('Error deleting inactive guest users:', error); - return NextResponse.json({ error: 'Failed to delete inactive guest users' }, { status: 500 }); - } -} diff --git a/src/management-system-v2/app/api/private/file-manager/run-sweeper/route.ts b/src/management-system-v2/app/api/private/file-manager/run-sweeper/route.ts deleted file mode 100644 index f0cf418bb..000000000 --- a/src/management-system-v2/app/api/private/file-manager/run-sweeper/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import db from '@/lib/data/db'; -import { deleteProcessArtifact } from '@/lib/data/file-manager-facade'; - -export async function POST(request: NextRequest) { - try { - // Extract and validate the Bearer token - const authHeader = request.headers.get('Authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Unauthorized: Missing Bearer token' }, { status: 401 }); - } - - const token = authHeader.replace('Bearer ', '').trim(); - if (token !== process.env.SWEEPER_TRIGGER_TOKEN) { - return NextResponse.json({ error: 'Unauthorized: Invalid Bearer token' }, { status: 403 }); - } - - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - - const deletableFiles = await db.artifact.findMany({ - where: { - deletable: true, - deletedOn: { lte: oneWeekAgo }, - }, - select: { filePath: true }, - }); - - if (deletableFiles.length > 0) { - await Promise.all(deletableFiles.map((file) => deleteProcessArtifact(file.filePath, true))); - } - - return NextResponse.json( - { - message: `${deletableFiles.length} files deleted successfully`, - }, - { status: 200 }, - ); - } catch (error) { - console.error('Error deleting files:', error); - return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 }); - } -} diff --git a/src/management-system-v2/app/api/scheduler/tasks/[task]/route.ts b/src/management-system-v2/app/api/scheduler/tasks/[task]/route.ts new file mode 100644 index 000000000..895df88c4 --- /dev/null +++ b/src/management-system-v2/app/api/scheduler/tasks/[task]/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getMSConfig } from '@/lib/ms-config/ms-config'; +import { allSchedulerTasks } from '@/lib/scheduler'; + +export async function POST(request: NextRequest, { params }: { params: { task: string } }) { + if (params.task !== 'all') { + return new Response('Not implemented', { + status: 501, + }); + } + + try { + const msConfig = await getMSConfig(); + + // Extract and validate the Bearer token + const authHeader = request.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return NextResponse.json({ error: 'Unauthorized: Missing Bearer token' }, { status: 401 }); + } + + const token = authHeader.replace('Bearer ', '').trim(); + if (token !== msConfig.SCHEDULER_TOKEN) { + return NextResponse.json({ error: 'Unauthorized: Invalid Bearer token' }, { status: 403 }); + } + + const message = allSchedulerTasks(); + + return NextResponse.json({ message }, { status: 200 }); + } catch (error) { + console.error('Error cleaning up DB:', error); + return NextResponse.json({ error: 'Failed to clean up DB' }, { status: 500 }); + } +} diff --git a/src/management-system-v2/instrumentation.ts b/src/management-system-v2/instrumentation.ts index 7db70a4c0..b2ea23652 100644 --- a/src/management-system-v2/instrumentation.ts +++ b/src/management-system-v2/instrumentation.ts @@ -1,5 +1,7 @@ export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { + // Importing env and using it here will ensure that env variables (msconfig) are verified at + // startup const { env } = await import('./lib/ms-config/env-vars'); const { getMSConfigDBValuesAndEnsureDefaults } = await import('./lib/ms-config/ms-config'); @@ -53,5 +55,9 @@ export async function register() { process.exit(1); } } + + // Start scheduler if necessary + const { restartInternalSchedulerWithCurrentConfigs } = await import('./lib/scheduler'); + await restartInternalSchedulerWithCurrentConfigs(); } } diff --git a/src/management-system-v2/lib/auth.ts b/src/management-system-v2/lib/auth.ts index 660452a49..f55aeb1dc 100644 --- a/src/management-system-v2/lib/auth.ts +++ b/src/management-system-v2/lib/auth.ts @@ -29,421 +29,443 @@ import db from './data/db'; import { createUserRegistrationToken } from './email-verification-tokens/utils'; import { saveEmailVerificationToken } from './data/db/iam/verification-tokens'; import { NextAuthEmailTakenError, NextAuthUsernameTakenError } from './authjs-error-message'; +import { getMSConfig } from './ms-config/ms-config'; -const nextAuthOptions: NextAuthConfig = { - secret: env.NEXTAUTH_SECRET, - adapter: Adapter, - session: { - strategy: 'jwt', - }, - cookies: { - csrfToken: { - name: 'proceed.csrf-token', - }, - callbackUrl: { - name: 'proceed.callback-url', +let nextAuthOptions: NextAuthConfig; +async function getNextAuthOptions() { + if (nextAuthOptions) return nextAuthOptions; + + const options: NextAuthConfig = { + secret: env.NEXTAUTH_SECRET, + adapter: Adapter, + session: { + strategy: 'jwt', }, - sessionToken: { - name: 'proceed.session-token', + cookies: { + csrfToken: { + name: 'proceed.csrf-token', + }, + callbackUrl: { + name: 'proceed.callback-url', + }, + sessionToken: { + name: 'proceed.session-token', + }, }, - }, - trustHost: true, - providers: [], - callbacks: { - async jwt({ token, user: _user, trigger }) { - if (!env.PROCEED_PUBLIC_IAM_ACTIVE) { - token.user = noIamUser.user; - return token; - } + trustHost: true, + providers: [], + callbacks: { + async jwt({ token, user: _user, trigger }) { + if (!env.PROCEED_PUBLIC_IAM_ACTIVE) { + token.user = noIamUser.user; + return token; + } - let user = _user as User | undefined; + let user = _user as User | undefined; - if (trigger === 'update') user = (await getUserById(token.user.id)) as User; + if (trigger === 'update') user = (await getUserById(token.user.id)) as User; - if (user) token.user = user; + if (user) token.user = user; - return token; - }, - session({ session, token }) { - if (token.user) (session.user as User) = token.user; + return token; + }, + session({ session, token }) { + if (token.user) (session.user as User) = token.user; - return session; - }, - signIn: async (params) => { - const { account, user: _user, email } = params; - if (account?.provider === 'register-as-new-user' && (_user as any).notARealUser === true) { - return `/signin?error=${encodeURIComponent('$success Check your email: we sent you a link to sign in with your new user.')}`; - } - - const session = await auth(); - const sessionUser = session?.user; - - // 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 userSigningIn = _user.id ? await getUserById(_user.id) : null; - - if (!userSigningIn) { - const user = _user as Partial; - await updateUser(sessionUser.id, { - firstName: user.firstName ?? undefined, - lastName: user.lastName ?? undefined, - username: user.username ?? undefined, - profileImage: user.image ?? undefined, - email: user.email ?? undefined, - isGuest: false, - }); + return session; + }, + signIn: async (params) => { + const { account, user: _user, email } = params; + if (account?.provider === 'register-as-new-user' && (_user as any).notARealUser === true) { + return `/signin?error=${encodeURIComponent('$success Check your email: we sent you a link to sign in with your new user.')}`; } - } - return true; - }, - }, - events: { - async signOut(message) { - // since we use jwt message contains a token - const token = (message as { token: JWT }).token; - - if (!token.user.isGuest) return; - - const user = await getUserById(token.user.id); - if (user) { - if (!user.isGuest) { - console.warn('User with invalid session'); - return; + const session = await auth(); + const sessionUser = session?.user; + + // 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 userSigningIn = _user.id ? await getUserById(_user.id) : null; + + if (!userSigningIn) { + const user = _user as Partial; + await updateUser(sessionUser.id, { + firstName: user.firstName ?? undefined, + lastName: user.lastName ?? undefined, + username: user.username ?? undefined, + profileImage: user.image ?? undefined, + email: user.email ?? undefined, + isGuest: false, + }); + } } - await deleteUser(user.id); - } + return true; + }, }, - async session({ session }) { - // TODO: this causes many db calls, we should debounce this with a significant delay - if (session.user.isGuest) { - await updateGuestUserLastSigninTime(session.user.id, new Date()); - } + events: { + async signOut(message) { + // since we use jwt message contains a token + const token = (message as { token: JWT }).token; + + if (!token.user.isGuest) return; + + const user = await getUserById(token.user.id); + if (user) { + if (!user.isGuest) { + console.warn('User with invalid session'); + return; + } + + await deleteUser(user.id); + } + }, + async session({ session }) { + // TODO: this causes many db calls, we should debounce this with a significant delay + if (session.user.isGuest) { + await updateGuestUserLastSigninTime(session.user.id, new Date()); + } + }, }, - }, - pages: { - signIn: '/signin', - error: '/signin', - }, - logger: { - error(error) { - if (error instanceof AuthError) { - if (['AdapterError', 'CallbackRouteError', 'OAuthProfileParseError'].includes(error.type)) { - console.error(error); + pages: { + signIn: '/signin', + error: '/signin', + }, + logger: { + error(error) { + if (error instanceof AuthError) { + if ( + ['AdapterError', 'CallbackRouteError', 'OAuthProfileParseError'].includes(error.type) + ) { + console.error(error); + } else { + console.error('NextAuth error:', error.type); + } } else { - console.error('NextAuth error:', error.type); + console.error(error); } - } else { - console.error(error); - } - }, - }, -}; - -if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { - nextAuthOptions.providers.push( - EmailProvider({ - id: 'email', - name: 'Sign in with E-mail', - server: {}, - sendVerificationRequest(params) { - const signinMail = renderSigninLinkEmail({ - signInLink: params.url, - expires: params.expires, - }); - - sendEmail({ - to: params.identifier, - subject: 'Sign in to PROCEED', - html: signinMail.html, - text: signinMail.text, - }); }, - maxAge: 24 * 60 * 60, // one day - }), - ); -} - -if (env.PROCEED_PUBLIC_IAM_LOGIN_OAUTH_GOOGLE_ACTIVE) { - nextAuthOptions.providers.push( - GoogleProvider({ - id: 'google', - clientId: env.IAM_LOGIN_OAUTH_GOOGLE_CLIENT_ID, - clientSecret: env.IAM_LOGIN_OAUTH_GOOGLE_CLIENT_SECRET, - profile(profile) { - return { - id: profile.sub, - name: profile.name, - firstName: profile.given_name, - lastName: profile.family_name, - email: profile.email, - image: profile.picture, - }; - }, - }), - ); -} + }, + }; -if (env.PROCEED_PUBLIC_IAM_LOGIN_OAUTH_X_ACTIVE) { - nextAuthOptions.providers.push( - TwitterProvider({ - id: 'twitter', - clientId: env.IAM_LOGIN_OAUTH_X_CLIENT_ID, - clientSecret: env.IAM_LOGIN_OAUTH_X_CLIENT_SECRET, - profile({ data, email }) { - const nameParts = data.name.split(' '); - const fistName = nameParts[0]; - const lastName = nameParts.slice(1).join(' '); - - return { - email: typeof email === 'string' ? email : undefined, - username: data.username, - id: data.id, - image: data.profile_image_url, - firstName: fistName.length > 0 ? fistName : undefined, - lastName: lastName.length > 0 ? lastName : undefined, - }; - }, - }), - ); -} + if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { + options.providers.push( + EmailProvider({ + id: 'email', + name: 'Sign in with E-mail', + server: {}, + sendVerificationRequest(params) { + const signinMail = renderSigninLinkEmail({ + signInLink: params.url, + expires: params.expires, + }); -// Guest users can only have a personal space, so it doesn't make sense to have guests when -// personal spaces are deactivated -if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { - nextAuthOptions.providers.push( - CredentialsProvider({ - name: 'Continue as Guest', - id: 'guest-signin', - credentials: {}, - async authorize() { - return addUser({ isGuest: true }); - }, - }), - ); -} + sendEmail({ + to: params.identifier, + subject: 'Sign in to PROCEED', + html: signinMail.html, + text: signinMail.text, + }); + }, + maxAge: + (await getMSConfig({ dontForceDynamicThroughHeaders: true })) + .SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_VERIFICATION_TOKENS * + 60 * + 60, + }), + ); + } -if (env.NODE_ENV === 'development') { - const johnDoeTemplate = { - username: 'johndoe', - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@proceed-labs.org', - id: 'development-id|johndoe', - isGuest: false, - emailVerifiedOn: null, - profileImage: null, - }; + if (env.PROCEED_PUBLIC_IAM_LOGIN_OAUTH_GOOGLE_ACTIVE) { + options.providers.push( + GoogleProvider({ + id: 'google', + clientId: env.IAM_LOGIN_OAUTH_GOOGLE_CLIENT_ID, + clientSecret: env.IAM_LOGIN_OAUTH_GOOGLE_CLIENT_SECRET, + profile(profile) { + return { + id: profile.sub, + name: profile.name, + firstName: profile.given_name, + lastName: profile.family_name, + email: profile.email, + image: profile.picture, + }; + }, + }), + ); + } - nextAuthOptions.providers.push( - CredentialsProvider({ - id: 'development-users', - name: 'Continue with Development User', - credentials: { - username: { - label: 'Username', - type: 'text', - placeholder: 'johndoe | admin', - value: 'admin', + if (env.PROCEED_PUBLIC_IAM_LOGIN_OAUTH_X_ACTIVE) { + options.providers.push( + TwitterProvider({ + id: 'twitter', + clientId: env.IAM_LOGIN_OAUTH_X_CLIENT_ID, + clientSecret: env.IAM_LOGIN_OAUTH_X_CLIENT_SECRET, + profile({ data, email }) { + const nameParts = data.name.split(' '); + const fistName = nameParts[0]; + const lastName = nameParts.slice(1).join(' '); + + return { + email: typeof email === 'string' ? email : undefined, + username: data.username, + id: data.id, + image: data.profile_image_url, + firstName: fistName.length > 0 ? fistName : undefined, + lastName: lastName.length > 0 ? lastName : undefined, + }; }, - }, - async authorize(credentials) { - let user: User | null = null; - - if (credentials.username === 'johndoe') { - user = await getUserByUsername('johndoe'); - if (!user) user = await addUser(johnDoeTemplate); - } else if (credentials.username === 'admin') { - user = await getUserByUsername('admin'); - } + }), + ); + } - return user; - }, - }), - ); -} + // Guest users can only have a personal space, so it doesn't make sense to have guests when + // personal spaces are deactivated + if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { + options.providers.push( + CredentialsProvider({ + name: 'Continue as Guest', + id: 'guest-signin', + credentials: {}, + async authorize() { + return addUser({ isGuest: true }); + }, + }), + ); + } -if (env.PROCEED_PUBLIC_IAM_LOGIN_OAUTH_DISCORD_ACTIVE) { - nextAuthOptions.providers.push( - DiscordProvider({ - id: 'discord', - clientId: env.IAM_LOGIN_OAUTH_DISCORD_CLIENT_ID, - clientSecret: env.IAM_LOGIN_OAUTH_DISCORD_CLIENT_SECRET, - profile(profile) { - const image = profile.avatar - ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` - : null; - - return { ...profile, image }; - }, - }), - ); -} + if (env.NODE_ENV === 'development') { + const johnDoeTemplate = { + username: 'johndoe', + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@proceed-labs.org', + id: 'development-id|johndoe', + isGuest: false, + emailVerifiedOn: null, + profileImage: null, + }; -if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) { - nextAuthOptions.providers.push( - CredentialsProvider({ - name: 'Sign in', - type: 'credentials', - id: 'username-password-signin', - credentials: { - username: { - label: 'Username', - type: 'username', + options.providers.push( + CredentialsProvider({ + id: 'development-users', + name: 'Continue with Development User', + credentials: { + username: { + label: 'Username', + type: 'text', + placeholder: 'johndoe | admin', + value: 'admin', + }, }, - password: { - label: 'Password', - type: 'password', + async authorize(credentials) { + let user: User | null = null; + + if (credentials.username === 'johndoe') { + user = await getUserByUsername('johndoe'); + if (!user) user = await addUser(johnDoeTemplate); + } else if (credentials.username === 'admin') { + user = await getUserByUsername('admin'); + } + + return user; }, - }, - authorize: async (credentials, req) => { - const userAndPassword = await getUserAndPasswordByUsername(credentials.username as string); + }), + ); + } - if (!userAndPassword) return null; + if (env.PROCEED_PUBLIC_IAM_LOGIN_OAUTH_DISCORD_ACTIVE) { + options.providers.push( + DiscordProvider({ + id: 'discord', + clientId: env.IAM_LOGIN_OAUTH_DISCORD_CLIENT_ID, + clientSecret: env.IAM_LOGIN_OAUTH_DISCORD_CLIENT_SECRET, + profile(profile) { + const image = profile.avatar + ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` + : null; + + return { ...profile, image }; + }, + }), + ); + } - const passwordIsCorrect = await comparePassword( - credentials.password as string, - userAndPassword.passwordAccount.password, - ); - if (!passwordIsCorrect) return null; + if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) { + options.providers.push( + CredentialsProvider({ + name: 'Sign in', + type: 'credentials', + id: 'username-password-signin', + credentials: { + username: { + label: 'Username', + type: 'username', + }, + password: { + label: 'Password', + type: 'password', + }, + }, + authorize: async (credentials, req) => { + const userAndPassword = await getUserAndPasswordByUsername( + credentials.username as string, + ); - return userAndPassword as User; - }, - }), - ); -} + if (!userAndPassword) return null; -if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE || env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { - //Vorname, Nachname und Username input feldern, - const credentials: Record = { - firstName: { - type: 'string', - label: 'First Name', - }, - lastName: { - type: 'string', - label: 'Last Name', - }, - username: { - type: 'string', - label: 'Username', - }, - }; + const passwordIsCorrect = await comparePassword( + credentials.password as string, + userAndPassword.passwordAccount.password, + ); + if (!passwordIsCorrect) return null; - if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { - credentials['email'] = { - type: 'email', - label: 'E-Mail', - }; + return userAndPassword as User; + }, + }), + ); } - if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) { - credentials['password'] = { - type: 'password', - label: 'Password', + if ( + env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE || + env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE + ) { + //Vorname, Nachname und Username input feldern, + const credentials: Record = { + firstName: { + type: 'string', + label: 'First Name', + }, + lastName: { + type: 'string', + label: 'Last Name', + }, + username: { + type: 'string', + label: 'Username', + }, }; - } - nextAuthOptions.providers.push( - CredentialsProvider({ - name: 'Register as New User', - type: 'credentials', - id: 'register-as-new-user', - credentials, - authorize: async ( - credentials: Partial< - Record< - 'firstName' | 'lastName' | 'username' | 'email' | 'password' | 'callbackUrl', - string - > - >, - ) => { - let callbackUrl: string | undefined = undefined; - // only allow urls that start with / = redirect to out site - if (credentials.callbackUrl && credentials.callbackUrl.startsWith('/')) { - callbackUrl = credentials.callbackUrl; - } + if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { + credentials['email'] = { + type: 'email', + label: 'E-Mail', + }; + } - let user: User | null = null; + if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) { + credentials['password'] = { + type: 'password', + label: 'Password', + }; + } - // Whenever the email is active, we create the user after he verifies his email - if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { - const [existingUserUsername, existingUserMail] = await Promise.all([ - getUserByUsername(credentials.username as string), - getUserByEmail(credentials.email as string), - ]); - if (existingUserUsername) { - throw new NextAuthUsernameTakenError(); - } - if (existingUserMail) { - throw new NextAuthEmailTakenError(); + options.providers.push( + CredentialsProvider({ + name: 'Register as New User', + type: 'credentials', + id: 'register-as-new-user', + credentials, + authorize: async ( + credentials: Partial< + Record< + 'firstName' | 'lastName' | 'username' | 'email' | 'password' | 'callbackUrl', + string + > + >, + ) => { + let callbackUrl: string | undefined = undefined; + // only allow urls that start with / = redirect to out site + if (credentials.callbackUrl && credentials.callbackUrl.startsWith('/')) { + callbackUrl = credentials.callbackUrl; } - const tokenParams: any = { - identifier: credentials.email, - username: credentials.username, - firstName: credentials.firstName, - lastName: credentials.lastName, - }; - - if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) - tokenParams['passwordHash'] = await hashPassword(credentials.password as string); - - const userRegistrationToken = await createUserRegistrationToken(tokenParams, callbackUrl); - - await saveEmailVerificationToken(userRegistrationToken.verificationToken); - - const signinMail = renderSigninLinkEmail({ - signInLink: userRegistrationToken.redirectUrl, - expires: userRegistrationToken.verificationToken.expires, - }); - - await sendEmail({ - to: credentials.email as string, - subject: 'Sign in to PROCEED', - html: signinMail.html, - text: signinMail.text, - }); - - // This allows nextauth to proceed in the signin flow. - // This dummy user will be caught by the signin callback and will redirect the user back - // to the signin page with a success message. - return { id: '', notARealUser: true }; - } else { - // Only password is enabled -> immediately create user - await db.$transaction(async (tx) => { - user = await addUser( - { - username: credentials.username, - firstName: credentials.firstName, - lastName: credentials.lastName, - isGuest: false, - emailVerifiedOn: null, - }, - tx, + let user: User | null = null; + + // Whenever the email is active, we create the user after he verifies his email + if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { + const [existingUserUsername, existingUserMail] = await Promise.all([ + getUserByUsername(credentials.username as string), + getUserByEmail(credentials.email as string), + ]); + if (existingUserUsername) { + throw new NextAuthUsernameTakenError(); + } + if (existingUserMail) { + throw new NextAuthEmailTakenError(); + } + + const tokenParams: any = { + identifier: credentials.email, + username: credentials.username, + firstName: credentials.firstName, + lastName: credentials.lastName, + }; + + if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) + tokenParams['passwordHash'] = await hashPassword(credentials.password as string); + + const userRegistrationToken = await createUserRegistrationToken( + tokenParams, + callbackUrl, ); - const hashedPassword = await hashPassword(credentials.password as string); - await setUserPassword(user.id, hashedPassword, tx); - }); - } + await saveEmailVerificationToken(userRegistrationToken.verificationToken); + + const signinMail = renderSigninLinkEmail({ + signInLink: userRegistrationToken.redirectUrl, + expires: userRegistrationToken.verificationToken.expires, + }); + + await sendEmail({ + to: credentials.email as string, + subject: 'Sign in to PROCEED', + html: signinMail.html, + text: signinMail.text, + }); + + // This allows nextauth to proceed in the signin flow. + // This dummy user will be caught by the signin callback and will redirect the user back + // to the signin page with a success message. + return { id: '', notARealUser: true }; + } else { + // Only password is enabled -> immediately create user + await db.$transaction(async (tx) => { + user = await addUser( + { + username: credentials.username, + firstName: credentials.firstName, + lastName: credentials.lastName, + isGuest: false, + emailVerifiedOn: null, + }, + tx, + ); + + const hashedPassword = await hashPassword(credentials.password as string); + await setUserPassword(user.id, hashedPassword, tx); + }); + } - return user; - }, - }), - ); + return user; + }, + }), + ); + } + + return options; } -export const { auth, handlers, signIn, signOut } = NextAuth(nextAuthOptions); +export const { auth, handlers, signIn, signOut } = NextAuth(getNextAuthOptions); export type ExtractedProvider = | { diff --git a/src/management-system-v2/lib/data/db/iam/environments.ts b/src/management-system-v2/lib/data/db/iam/environments.ts index df9a02a52..1df75f4b9 100644 --- a/src/management-system-v2/lib/data/db/iam/environments.ts +++ b/src/management-system-v2/lib/data/db/iam/environments.ts @@ -165,8 +165,12 @@ export async function addEnvironment( return newEnvironmentWithId; } -export async function deleteEnvironment(environmentId: string, ability?: Ability) { - const environment = await getEnvironmentById(environmentId); +export async function deleteEnvironment( + environmentId: string, + ability?: Ability, + tx?: Prisma.TransactionClient, +) { + const environment = await getEnvironmentById(environmentId, undefined, undefined, tx); if (!environment) throw new Error('Environment not found'); if (env.PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE && environment.isOrganization) { @@ -176,7 +180,9 @@ export async function deleteEnvironment(environmentId: string, ability?: Ability } if (ability && !ability.can('delete', 'Environment')) throw new UnauthorizedError(); - await db.space.delete({ + + const dbMutator = tx ?? db; + await dbMutator.space.delete({ where: { id: environmentId }, }); } @@ -260,3 +266,26 @@ export async function deleteSpaceLogo(organizationId: string) { //deleteLogo(organizationId); } + +export async function removeInactiveSpaces( + inactiveTimeInMS: number, + tx?: Prisma.TransactionClient, +): Promise<{ count: number }> { + if (!tx) { + return db.$transaction((trx) => removeInactiveSpaces(inactiveTimeInMS, trx)); + } + + const cutoff = new Date(Date.now() - inactiveTimeInMS); + + const deletableSpaces = await tx.space.findMany({ + where: { + isActive: false, + createdOn: { lte: cutoff }, + }, + }); + + if (deletableSpaces.length > 0) { + await Promise.all(deletableSpaces.map((space) => deleteEnvironment(space.id, undefined, tx))); + } + return { count: deletableSpaces.length }; +} diff --git a/src/management-system-v2/lib/data/db/iam/verification-tokens.ts b/src/management-system-v2/lib/data/db/iam/verification-tokens.ts index 9e35cf222..d8fce15af 100644 --- a/src/management-system-v2/lib/data/db/iam/verification-tokens.ts +++ b/src/management-system-v2/lib/data/db/iam/verification-tokens.ts @@ -83,3 +83,19 @@ export async function updateEmailVerificationTokenExpiration( }, }); } + +export async function removeExpiredEmailVerificationTokens( + tx?: Prisma.TransactionClient, +): Promise<{ count: number }> { + if (!tx) { + return db.$transaction((trx) => removeExpiredEmailVerificationTokens(trx)); + } + + const cutoff = new Date(); + + return await tx.emailVerificationToken.deleteMany({ + where: { + expires: { lte: cutoff }, + }, + }); +} diff --git a/src/management-system-v2/lib/data/environments.ts b/src/management-system-v2/lib/data/environments.ts index 95a20898b..e80b2cb9e 100644 --- a/src/management-system-v2/lib/data/environments.ts +++ b/src/management-system-v2/lib/data/environments.ts @@ -60,7 +60,7 @@ export async function deleteOrganizationEnvironments(environmentIds: string[]) { if (!environment?.isOrganization) return userError(`Environment ${environmentId} is not an organization environment`); - deleteEnvironment(environmentId, ability); + await deleteEnvironment(environmentId, ability); } } catch (e) { if (e instanceof UnauthorizedError) diff --git a/src/management-system-v2/lib/data/file-manager-facade.ts b/src/management-system-v2/lib/data/file-manager-facade.ts index 1f97fc66d..8387e7c95 100644 --- a/src/management-system-v2/lib/data/file-manager-facade.ts +++ b/src/management-system-v2/lib/data/file-manager-facade.ts @@ -606,6 +606,32 @@ export async function revertSoftDeleteProcessScriptTask( } } +export async function removeDeletedArtifactsFromDb( + expirationInMs: number, + tx?: Prisma.TransactionClient, +): Promise<{ count: number }> { + if (!tx) { + return db.$transaction((trx) => removeDeletedArtifactsFromDb(expirationInMs, trx)); + } + + const cutoff = new Date(Date.now() - expirationInMs); + + const deletableFiles = await tx.artifact.findMany({ + where: { + deletable: true, + deletedOn: { lte: cutoff }, + }, + select: { filePath: true }, + }); + + if (deletableFiles.length > 0) { + await Promise.all( + deletableFiles.map((file) => deleteProcessArtifact(file.filePath, true, undefined, tx)), + ); + } + return { count: deletableFiles.length }; +} + // Update artifact references for a versioned user task // export async function updateArtifactRefVersionedUserTask(userTask: string, newFileName: string) { // if (!userTask) { diff --git a/src/management-system-v2/lib/email-verification-tokens/utils.ts b/src/management-system-v2/lib/email-verification-tokens/utils.ts index 0006ba867..72924451d 100644 --- a/src/management-system-v2/lib/email-verification-tokens/utils.ts +++ b/src/management-system-v2/lib/email-verification-tokens/utils.ts @@ -3,6 +3,9 @@ import 'server-only'; import { z } from 'zod'; import { EmailVerificationToken } from '@/lib/data/db/iam/verification-tokens'; import { env } from '@/lib/ms-config/env-vars'; +import { getMSConfig } from '../ms-config/ms-config'; + +const MS_IN_HOUR = 1000 * 60 * 60; async function createHash(message: string) { const msgUint8 = new TextEncoder().encode(message); @@ -26,8 +29,12 @@ export async function createChangeEmailVerificationToken({ }) { const identifier = z.string().email().parse(email); + const msConfig = await getMSConfig(); + const token = crypto.randomUUID(); - const expires = new Date(Date.now() + 1000 * 60 * 60 * 24); + const expires = new Date( + Date.now() + MS_IN_HOUR * msConfig.SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_CHANGE_TOKENS, + ); const verificationToken = { type: 'change_email', @@ -65,8 +72,12 @@ export async function createUserRegistrationToken( }, callbackUrl?: string, ) { + const msConfig = await getMSConfig(); + const token = crypto.randomUUID(); - const expires = new Date(Date.now() + 1000 * 60 * 60 * 24); + const expires = new Date( + Date.now() + MS_IN_HOUR * msConfig.SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_REGISTRATION_TOKENS, + ); const verificationToken = { type: 'register_new_user', diff --git a/src/management-system-v2/lib/ms-config/config-schema.ts b/src/management-system-v2/lib/ms-config/config-schema.ts index 71531fb90..65a293b63 100644 --- a/src/management-system-v2/lib/ms-config/config-schema.ts +++ b/src/management-system-v2/lib/ms-config/config-schema.ts @@ -150,10 +150,24 @@ export const msConfigSchema = { IAM_LOGIN_OAUTH_DISCORD_CLIENT_ID: z.string().default(''), IAM_LOGIN_OAUTH_DISCORD_CLIENT_SECRET: z.string().default(''), + SCHEDULER_INTERNAL_ACTIVE: z.string().optional().transform(boolParser), SCHEDULER_INTERVAL: z.string().default('0 3 * * *'), SCHEDULER_TOKEN: z.string().optional(), - SCHEDULER_JOB_DELETE_INACTIVE_GUESTS: z.coerce.number().default(0), - SCHEDULER_JOB_DELETE_OLD_ARTIFACTS: z.coerce.number().default(7), + + SCHEDULER_TASK_DELETE_INACTIVE_GUESTS: z.coerce.number().default(30), + SCHEDULER_TASK_DELETE_OLD_ARTIFACTS: z.coerce.number().default(7), + SCHEDULER_TASK_DELETE_INACTIVE_SPACES: z.coerce.number().default(7), + + PROCEED_PUBLIC_TIMELINE_VIEW: z.string().optional().transform(boolParser), + + MQTT_SERVER_ADDRESS: z.string().url().optional(), + MQTT_USERNAME: z.string().optional(), + MQTT_PASSWORD: z.string().optional(), + MQTT_BASETOPIC: z.string().optional(), + + SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_REGISTRATION_TOKENS: z.coerce.number().default(7), + SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_CHANGE_TOKENS: z.coerce.number().default(7), + SCHEDULING_TASK_EXPIRATION_TIME_EMAIL_VERIFICATION_TOKENS: z.coerce.number().default(24), }, production: { DATABASE_URL: z.string(), diff --git a/src/management-system-v2/lib/ms-config/ms-config.ts b/src/management-system-v2/lib/ms-config/ms-config.ts index 2f1145261..98fe49c4e 100644 --- a/src/management-system-v2/lib/ms-config/ms-config.ts +++ b/src/management-system-v2/lib/ms-config/ms-config.ts @@ -107,9 +107,11 @@ export async function updateMSConfig(config: Record { + const removedArtifacts = await removeDeletedArtifactsFromDb( + msConfig.SCHEDULER_TASK_DELETE_OLD_ARTIFACTS * MS_IN_DAY, + tx, + ); + message += `Removed ${removedArtifacts.count} artifacts.\n`; + + const removedInactiveGuests = await deleteInactiveGuestUsers( + msConfig.SCHEDULER_TASK_DELETE_INACTIVE_GUESTS * MS_IN_DAY, + tx, + ); + message += `Removed ${removedInactiveGuests.count} inactive guests.\n`; + + const inactiveSpaces = await removeInactiveSpaces( + msConfig.SCHEDULER_TASK_DELETE_INACTIVE_SPACES * MS_IN_DAY, + tx, + ); + message += `Removed ${inactiveSpaces.count} inactive spaces.\n`; + + const removedVerificationTokens = await removeExpiredEmailVerificationTokens(tx); + message += `Removed ${removedVerificationTokens.count} expired verification tokens.`; + }); + + return message; +} diff --git a/src/management-system-v2/package.json b/src/management-system-v2/package.json index 1f65a735b..65cfd1229 100644 --- a/src/management-system-v2/package.json +++ b/src/management-system-v2/package.json @@ -53,7 +53,6 @@ "monaco-editor": "0.47.0", "next": "14.2.3", "next-auth": "5.0.0-beta.25", - "node-cron": "^3.0.3", "nodemailer": "6.9.13", "openapi-fetch": "0.8.2", "react": "18.2.0", @@ -72,7 +71,8 @@ "react-resizable": "^3.0.5", "mqtt": "^5.10.1", "bcryptjs": "3.0.2", - "sharp": "0.34.3" + "sharp": "0.34.3", + "cron": "4.3.3" }, "devDependencies": { "@tanstack/eslint-plugin-query": "5.28.11", diff --git a/src/management-system-v2/prisma/migrations/20250911145437_spaces_created_on/migration.sql b/src/management-system-v2/prisma/migrations/20250911145437_spaces_created_on/migration.sql new file mode 100644 index 000000000..6a702ffbc --- /dev/null +++ b/src/management-system-v2/prisma/migrations/20250911145437_spaces_created_on/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "space" ADD COLUMN "createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/src/management-system-v2/prisma/schema.prisma b/src/management-system-v2/prisma/schema.prisma index a3e8a41e3..2fd6a5a83 100644 --- a/src/management-system-v2/prisma/schema.prisma +++ b/src/management-system-v2/prisma/schema.prisma @@ -160,6 +160,7 @@ model Space { roles Role[] engines Engine[] settings SpaceSettings? @relation("spaceSettings") + createdOn DateTime @default(now()) @@map("space") } diff --git a/src/management-system-v2/sweeper-cron/sweeper.js b/src/management-system-v2/sweeper-cron/sweeper.js deleted file mode 100644 index b3e986976..000000000 --- a/src/management-system-v2/sweeper-cron/sweeper.js +++ /dev/null @@ -1,31 +0,0 @@ -require('dotenv').config({ path: '../.env' }); -const cron = require('node-cron'); -const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; - -// Schedule a task to run every Monday at 00:00 (midnight) -// * * * * * -// minute hour day(month) month day(week) - -cron.schedule( - '0 0 * * 1', // Adjust cron timing as needed - async () => { - try { - const res = await fetch(`${BASE_URL}/api/private/file-manager/run-sweeper`, { - method: 'POST', - headers: { - Authorization: `Bearer ${process.env.SWEEPER_TRIGGER_TOKEN}`, // Add auth token - }, - }); - - console.log(res.status, res.statusText); - } catch (error) { - console.error('Error running sweeper:', error.message); - } - }, - { - scheduled: true, - timezone: 'UTC', - }, -); - -console.log('Weekly cron job running.....'); diff --git a/yarn.lock b/yarn.lock index 894b1c241..0a798c433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3024,6 +3024,11 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz" integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA== +"@types/luxon@~3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.7.1.tgz#ef51b960ff86801e4e2de80c68813a96e529d531" + integrity sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg== + "@types/markdown-it@^12.2.3": version "12.2.3" resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz" @@ -5459,6 +5464,14 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/cron/-/cron-4.3.3.tgz#d37cfcbc73ba34a50d9d9ce9b653ae60837377d7" + integrity sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw== + dependencies: + "@types/luxon" "~3.7.0" + luxon "~3.7.0" + cross-env@7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" @@ -10373,6 +10386,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +luxon@~3.7.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" + integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== + machine-uuid@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/machine-uuid/-/machine-uuid-1.2.0.tgz" @@ -11022,13 +11040,6 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-cron@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz" - integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A== - dependencies: - uuid "8.3.2" - node-exceptions@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/node-exceptions/-/node-exceptions-4.0.1.tgz" @@ -15431,11 +15442,6 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" -uuid@8.3.2, uuid@^8.0.0, uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - uuid@9.0.1, uuid@^9.0.0: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" @@ -15451,6 +15457,11 @@ uuid@^3.1.0, uuid@^3.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.0.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz"