diff --git a/apps/journeys-admin/__generated__/UserDeleteCheck.ts b/apps/journeys-admin/__generated__/UserDeleteCheck.ts
new file mode 100644
index 00000000000..ecc0247fde5
--- /dev/null
+++ b/apps/journeys-admin/__generated__/UserDeleteCheck.ts
@@ -0,0 +1,40 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+import { UserDeleteIdType } from "./globalTypes";
+
+// ====================================================
+// GraphQL mutation operation: UserDeleteCheck
+// ====================================================
+
+export interface UserDeleteCheck_userDeleteCheck_logs {
+ __typename: "UserDeleteLogEntry";
+ message: string;
+ level: string;
+ timestamp: string;
+}
+
+export interface UserDeleteCheck_userDeleteCheck {
+ __typename: "UserDeleteCheckResult";
+ userId: string;
+ userEmail: string | null;
+ userFirstName: string;
+ journeysToDelete: number;
+ journeysToTransfer: number;
+ journeysToRemove: number;
+ teamsToDelete: number;
+ teamsToTransfer: number;
+ teamsToRemove: number;
+ logs: UserDeleteCheck_userDeleteCheck_logs[];
+}
+
+export interface UserDeleteCheck {
+ userDeleteCheck: UserDeleteCheck_userDeleteCheck;
+}
+
+export interface UserDeleteCheckVariables {
+ idType: UserDeleteIdType;
+ id: string;
+}
diff --git a/apps/journeys-admin/__generated__/UserDeleteConfirmSubscription.ts b/apps/journeys-admin/__generated__/UserDeleteConfirmSubscription.ts
new file mode 100644
index 00000000000..47dff478978
--- /dev/null
+++ b/apps/journeys-admin/__generated__/UserDeleteConfirmSubscription.ts
@@ -0,0 +1,33 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+import { UserDeleteIdType } from "./globalTypes";
+
+// ====================================================
+// GraphQL subscription operation: UserDeleteConfirmSubscription
+// ====================================================
+
+export interface UserDeleteConfirmSubscription_userDeleteConfirm_log {
+ __typename: "UserDeleteLogEntry";
+ message: string;
+ level: string;
+ timestamp: string;
+}
+
+export interface UserDeleteConfirmSubscription_userDeleteConfirm {
+ __typename: "UserDeleteConfirmProgress";
+ log: UserDeleteConfirmSubscription_userDeleteConfirm_log;
+ done: boolean;
+ success: boolean | null;
+}
+
+export interface UserDeleteConfirmSubscription {
+ userDeleteConfirm: UserDeleteConfirmSubscription_userDeleteConfirm;
+}
+
+export interface UserDeleteConfirmSubscriptionVariables {
+ idType: UserDeleteIdType;
+ id: string;
+}
diff --git a/apps/journeys-admin/__generated__/globalTypes.ts b/apps/journeys-admin/__generated__/globalTypes.ts
index e3474fe6b6c..6e771aeb0df 100644
--- a/apps/journeys-admin/__generated__/globalTypes.ts
+++ b/apps/journeys-admin/__generated__/globalTypes.ts
@@ -328,6 +328,11 @@ export enum TypographyVariant {
subtitle2 = "subtitle2",
}
+export enum UserDeleteIdType {
+ databaseId = "databaseId",
+ email = "email",
+}
+
export enum UserJourneyRole {
editor = "editor",
inviteRequested = "inviteRequested",
diff --git a/apps/journeys-admin/pages/api/clear-auth.ts b/apps/journeys-admin/pages/api/clear-auth.ts
new file mode 100644
index 00000000000..e1bb8467b0f
--- /dev/null
+++ b/apps/journeys-admin/pages/api/clear-auth.ts
@@ -0,0 +1,14 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+
+import { authConfig } from '../../src/libs/auth/config'
+
+export default function handler(
+ _req: NextApiRequest,
+ res: NextApiResponse
+): void {
+ res.setHeader(
+ 'Set-Cookie',
+ `${authConfig.cookieName}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`
+ )
+ res.redirect(307, '/users/sign-in')
+}
diff --git a/apps/journeys-admin/pages/users/delete.tsx b/apps/journeys-admin/pages/users/delete.tsx
new file mode 100644
index 00000000000..8bbd2180d70
--- /dev/null
+++ b/apps/journeys-admin/pages/users/delete.tsx
@@ -0,0 +1,49 @@
+import { GetServerSidePropsContext } from 'next'
+import { useTranslation } from 'next-i18next'
+import { NextSeo } from 'next-seo'
+import { ReactElement } from 'react'
+
+import { PageWrapper } from '../../src/components/PageWrapper'
+import { UserDelete } from '../../src/components/UserDelete'
+import { useAuth } from '../../src/libs/auth'
+import {
+ getAuthTokens,
+ redirectToLogin,
+ toUser
+} from '../../src/libs/auth/getAuthTokens'
+import { initAndAuthApp } from '../../src/libs/initAndAuthApp'
+
+export default function UserDeletePage(): ReactElement {
+ const { t } = useTranslation('apps-journeys-admin')
+ const { user } = useAuth()
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
+ const tokens = await getAuthTokens(ctx)
+ if (tokens == null) return redirectToLogin(ctx)
+ const user = toUser(tokens)
+
+ const { redirect, translations } = await initAndAuthApp({
+ user,
+ locale: ctx.locale,
+ resolvedUrl: ctx.resolvedUrl
+ })
+
+ if (redirect != null) return { redirect }
+
+ return {
+ props: {
+ userSerialized: JSON.stringify(user),
+ ...translations
+ }
+ }
+}
diff --git a/apps/journeys-admin/src/components/PageWrapper/NavigationDrawer/UserNavigation/UserNavigation.tsx b/apps/journeys-admin/src/components/PageWrapper/NavigationDrawer/UserNavigation/UserNavigation.tsx
index e9cbc4cada3..79e46f6e221 100644
--- a/apps/journeys-admin/src/components/PageWrapper/NavigationDrawer/UserNavigation/UserNavigation.tsx
+++ b/apps/journeys-admin/src/components/PageWrapper/NavigationDrawer/UserNavigation/UserNavigation.tsx
@@ -12,6 +12,7 @@ import { useTranslation } from 'next-i18next'
import { MouseEvent, ReactElement, useEffect, useState } from 'react'
import BoxIcon from '@core/shared/ui/icons/Box'
+import Trash2Icon from '@core/shared/ui/icons/Trash2'
import UserProfile3Icon from '@core/shared/ui/icons/UserProfile3'
import { GetMe } from '../../../../../__generated__/GetMe'
@@ -147,6 +148,23 @@ export function UserNavigation({
/>
)}
+ {data.me?.__typename === 'AuthenticatedUser' &&
+ data.me.superAdmin === true && (
+
+
+
+
+
+
+ )}
{data.me?.__typename === 'AuthenticatedUser' && (
({
+ useRouter: () => ({
+ push: mockPush,
+ query: {}
+ })
+}))
+
+jest.mock('notistack', () => ({
+ useSnackbar: () => ({
+ enqueueSnackbar: jest.fn()
+ })
+}))
+
+const mockUseSuspenseQuery = jest.fn()
+jest.mock('@apollo/client', () => {
+ const actual = jest.requireActual('@apollo/client')
+ return {
+ ...actual,
+ useSuspenseQuery: (...args: unknown[]) => mockUseSuspenseQuery(...args)
+ }
+})
+
+describe('UserDeleteWithErrorBoundary', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockUseSuspenseQuery.mockReturnValue({
+ data: {
+ me: {
+ __typename: 'AuthenticatedUser',
+ id: 'user-1',
+ superAdmin: true
+ }
+ }
+ })
+ })
+
+ it('should render the form for superAdmin users', () => {
+ const { getAllByText, getByText } = render(
+
+
+
+ )
+
+ expect(getAllByText('Delete User').length).toBeGreaterThanOrEqual(1)
+ expect(getByText('Check')).toBeInTheDocument()
+ expect(getByText('Warning')).toBeInTheDocument()
+ })
+
+ it('should redirect non-superAdmin users', () => {
+ mockUseSuspenseQuery.mockReturnValue({
+ data: {
+ me: {
+ __typename: 'AuthenticatedUser',
+ id: 'user-1',
+ superAdmin: false
+ }
+ }
+ })
+
+ render(
+
+
+
+ )
+
+ expect(mockPush).toHaveBeenCalledWith('/')
+ })
+
+ it('should render empty for non-superAdmin', () => {
+ mockUseSuspenseQuery.mockReturnValue({
+ data: {
+ me: {
+ __typename: 'AuthenticatedUser',
+ id: 'user-1',
+ superAdmin: false
+ }
+ }
+ })
+
+ const { queryByText } = render(
+
+
+
+ )
+
+ expect(queryByText('Check')).not.toBeInTheDocument()
+ })
+
+ it('should have delete button disabled before check', () => {
+ const { getAllByRole } = render(
+
+
+
+ )
+
+ const deleteUserButtons = getAllByRole('button', { name: 'Delete User' })
+ const actionBtn = deleteUserButtons[deleteUserButtons.length - 1]
+ expect(actionBtn).toBeDisabled()
+ })
+
+ it('should have check button disabled when input is empty', () => {
+ const { getByText } = render(
+
+
+
+ )
+
+ expect(getByText('Check').closest('button')).toBeDisabled()
+ })
+
+ it('should render lookup type selector with email as default', () => {
+ const { getByLabelText } = render(
+
+
+
+ )
+
+ expect(getByLabelText('Lookup By')).toBeInTheDocument()
+ })
+
+ it('should render logs textfield', () => {
+ const { getByRole } = render(
+
+
+
+ )
+
+ const logsField = getByRole('textbox', { name: 'Logs' })
+ expect(logsField).toBeInTheDocument()
+ expect(logsField).toHaveAttribute('readonly')
+ })
+})
diff --git a/apps/journeys-admin/src/components/UserDelete/UserDelete.tsx b/apps/journeys-admin/src/components/UserDelete/UserDelete.tsx
new file mode 100644
index 00000000000..3b3dd512714
--- /dev/null
+++ b/apps/journeys-admin/src/components/UserDelete/UserDelete.tsx
@@ -0,0 +1,428 @@
+import {
+ ApolloError,
+ gql,
+ useMutation,
+ useSubscription,
+ useSuspenseQuery
+} from '@apollo/client'
+import Alert from '@mui/material/Alert'
+import AlertTitle from '@mui/material/AlertTitle'
+import Box from '@mui/material/Box'
+import Button from '@mui/material/Button'
+import Dialog from '@mui/material/Dialog'
+import DialogActions from '@mui/material/DialogActions'
+import DialogContent from '@mui/material/DialogContent'
+import DialogContentText from '@mui/material/DialogContentText'
+import DialogTitle from '@mui/material/DialogTitle'
+import FormControl from '@mui/material/FormControl'
+import InputLabel from '@mui/material/InputLabel'
+import MenuItem from '@mui/material/MenuItem'
+import Select from '@mui/material/Select'
+import Stack from '@mui/material/Stack'
+import TextField from '@mui/material/TextField'
+import Typography from '@mui/material/Typography'
+import { useRouter } from 'next/router'
+import { useTranslation } from 'next-i18next'
+import { useSnackbar } from 'notistack'
+import {
+ Component,
+ ErrorInfo,
+ ReactElement,
+ ReactNode,
+ Suspense,
+ useCallback,
+ useEffect,
+ useRef,
+ useState
+} from 'react'
+
+import { GetMe } from '../../../__generated__/GetMe'
+import { UserDeleteIdType } from '../../../__generated__/globalTypes'
+import {
+ UserDeleteCheck,
+ UserDeleteCheckVariables
+} from '../../../__generated__/UserDeleteCheck'
+import {
+ UserDeleteConfirmSubscription,
+ UserDeleteConfirmSubscriptionVariables
+} from '../../../__generated__/UserDeleteConfirmSubscription'
+import { GET_ME } from '../PageWrapper/NavigationDrawer/UserNavigation/UserNavigation'
+
+interface LogEntry {
+ message: string
+ level: string
+ timestamp: string
+}
+
+export const USER_DELETE_CHECK = gql`
+ mutation UserDeleteCheck($idType: UserDeleteIdType!, $id: String!) {
+ userDeleteCheck(idType: $idType, id: $id) {
+ userId
+ userEmail
+ userFirstName
+ journeysToDelete
+ journeysToTransfer
+ journeysToRemove
+ teamsToDelete
+ teamsToTransfer
+ teamsToRemove
+ logs {
+ message
+ level
+ timestamp
+ }
+ }
+ }
+`
+
+export const USER_DELETE_CONFIRM = gql`
+ subscription UserDeleteConfirmSubscription(
+ $idType: UserDeleteIdType!
+ $id: String!
+ ) {
+ userDeleteConfirm(idType: $idType, id: $id) {
+ log {
+ message
+ level
+ timestamp
+ }
+ done
+ success
+ }
+ }
+`
+
+interface UserDeleteErrorBoundaryProps {
+ children: ReactNode
+}
+
+interface UserDeleteErrorBoundaryState {
+ hasError: boolean
+ error: Error | null
+}
+
+class UserDeleteErrorBoundary extends Component<
+ UserDeleteErrorBoundaryProps,
+ UserDeleteErrorBoundaryState
+> {
+ constructor(props: UserDeleteErrorBoundaryProps) {
+ super(props)
+ this.state = { hasError: false, error: null }
+ }
+
+ static getDerivedStateFromError(error: Error): UserDeleteErrorBoundaryState {
+ return { hasError: true, error }
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
+ console.error('UserDelete error:', error, errorInfo)
+ }
+
+ render(): ReactNode {
+ if (this.state.hasError) {
+ return
+ }
+ return this.props.children
+ }
+}
+
+function UserDeleteErrorFallback({
+ error
+}: {
+ error: Error | null
+}): ReactElement {
+ const { t } = useTranslation('apps-journeys-admin')
+ return (
+
+
+ {t('Something went wrong')}
+
+ {error?.message ?? t('An unexpected error occurred.')}
+
+
+
+ )
+}
+
+export function UserDeleteWithErrorBoundary(): ReactElement {
+ return (
+
+
+
+
+
+ )
+}
+
+interface ConfirmVars {
+ idType: UserDeleteIdType
+ id: string
+}
+
+const levelLabel: Record = {
+ error: 'ERROR',
+ warn: 'WARN',
+ info: 'INFO'
+}
+
+function UserDeleteContent(): ReactElement {
+ const { t } = useTranslation('apps-journeys-admin')
+ const router = useRouter()
+ const { enqueueSnackbar } = useSnackbar()
+ const { data } = useSuspenseQuery(GET_ME, {
+ variables: { input: { redirect: router?.query?.redirect } }
+ })
+
+ const [idType, setIdType] = useState(UserDeleteIdType.email)
+ const [userId, setUserId] = useState('')
+ const [logs, setLogs] = useState([])
+ const [checkComplete, setCheckComplete] = useState(false)
+ const [confirmOpen, setConfirmOpen] = useState(false)
+ const [confirmVars, setConfirmVars] = useState(null)
+
+ const [userDeleteCheck, { loading: checkLoading }] = useMutation<
+ UserDeleteCheck,
+ UserDeleteCheckVariables
+ >(USER_DELETE_CHECK)
+
+ useSubscription<
+ UserDeleteConfirmSubscription,
+ UserDeleteConfirmSubscriptionVariables
+ >(USER_DELETE_CONFIRM, {
+ skip: confirmVars == null,
+ variables: confirmVars ?? { idType: UserDeleteIdType.email, id: '' },
+ onData: ({ data: subData }) => {
+ const progress = subData.data?.userDeleteConfirm
+ if (progress == null) return
+
+ setLogs((prev) => [...prev, progress.log])
+
+ if (progress.done) {
+ setConfirmVars(null)
+
+ if (progress.success === true) {
+ enqueueSnackbar(t('User deleted successfully'), {
+ variant: 'success'
+ })
+ setCheckComplete(false)
+ } else {
+ enqueueSnackbar(t('User deletion failed. Check logs for details.'), {
+ variant: 'error'
+ })
+ }
+ }
+ },
+ onError: (error) => {
+ const message = error.graphQLErrors[0]?.message ?? error.message
+ setLogs((prev) => [
+ ...prev,
+ {
+ message: `Error: ${message}`,
+ level: 'error',
+ timestamp: new Date().toISOString()
+ }
+ ])
+ enqueueSnackbar(message, { variant: 'error', preventDuplicate: true })
+ setConfirmVars(null)
+ }
+ })
+
+ const confirmLoading = confirmVars != null
+
+ const isSuperAdmin =
+ data.me?.__typename === 'AuthenticatedUser' && data.me.superAdmin === true
+
+ useEffect(() => {
+ if (!isSuperAdmin) {
+ void router.push('/')
+ }
+ }, [isSuperAdmin, router])
+
+ const logBoxRef = useRef(null)
+
+ useEffect(() => {
+ const el = logBoxRef.current
+ if (el != null) {
+ el.scrollTop = el.scrollHeight
+ }
+ }, [logs])
+
+ const logText = logs
+ .map((log) => {
+ const time = new Date(log.timestamp).toLocaleTimeString()
+ return `[${time}] ${levelLabel[log.level] ?? log.level.toUpperCase()}: ${log.message}`
+ })
+ .join('\n')
+
+ const handleCheck = useCallback(async () => {
+ if (userId.trim() === '') return
+
+ setLogs([])
+ setCheckComplete(false)
+
+ try {
+ const { data: checkData } = await userDeleteCheck({
+ variables: { idType, id: userId.trim() }
+ })
+
+ if (checkData?.userDeleteCheck != null) {
+ setLogs(checkData.userDeleteCheck.logs)
+ setCheckComplete(true)
+ }
+ } catch (error) {
+ if (error instanceof ApolloError) {
+ const message = error.graphQLErrors[0]?.message ?? error.message
+ setLogs((prev) => [
+ ...prev,
+ {
+ message: `Error: ${message}`,
+ level: 'error',
+ timestamp: new Date().toISOString()
+ }
+ ])
+ enqueueSnackbar(message, { variant: 'error', preventDuplicate: true })
+ }
+ }
+ }, [idType, userId, userDeleteCheck, enqueueSnackbar])
+
+ const handleConfirmDelete = useCallback(() => {
+ setConfirmOpen(false)
+ setConfirmVars({ idType, id: userId.trim() })
+ }, [idType, userId])
+
+ if (!isSuperAdmin) return <>>
+
+ return (
+
+
+ {t('Delete User')}
+
+
+
+ {t('Warning')}
+
+ {t(
+ 'This action permanently deletes a user and their associated data. This cannot be undone. Always run a check first to review what will be deleted.'
+ )}
+
+
+
+
+
+ {t('Lookup By')}
+
+
+
+ {
+ setUserId(e.target.value)
+ setCheckComplete(false)
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') void handleCheck()
+ }}
+ disabled={checkLoading || confirmLoading}
+ fullWidth
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/journeys-admin/src/components/UserDelete/index.ts b/apps/journeys-admin/src/components/UserDelete/index.ts
new file mode 100644
index 00000000000..6f5d664123e
--- /dev/null
+++ b/apps/journeys-admin/src/components/UserDelete/index.ts
@@ -0,0 +1 @@
+export { UserDeleteWithErrorBoundary as UserDelete } from './UserDelete'
diff --git a/apps/journeys-admin/src/libs/apolloClient/apolloClient.ts b/apps/journeys-admin/src/libs/apolloClient/apolloClient.ts
index 2b6fcb640fc..279254eb31e 100644
--- a/apps/journeys-admin/src/libs/apolloClient/apolloClient.ts
+++ b/apps/journeys-admin/src/libs/apolloClient/apolloClient.ts
@@ -12,6 +12,7 @@ import {
} from '@apollo/client'
import { EntityStore, StoreObject } from '@apollo/client/cache'
import { setContext } from '@apollo/client/link/context'
+import { onError } from '@apollo/client/link/error'
import { getMainDefinition } from '@apollo/client/utilities'
import DebounceLink from 'apollo-link-debounce'
import { getApp } from 'firebase/app'
@@ -20,6 +21,8 @@ import { createClient } from 'graphql-sse'
import { useMemo } from 'react'
import { Observable } from 'zen-observable-ts'
+import { logout } from '../auth/firebase'
+
import { cache } from './cache'
const ssrMode = typeof window === 'undefined'
@@ -118,6 +121,21 @@ export function createApolloClient(
}
})
+ const errorLink = onError(({ graphQLErrors }) => {
+ if (
+ !ssrMode &&
+ graphQLErrors?.some((e) => e.extensions?.code === 'UNAUTHENTICATED') ===
+ true
+ ) {
+ void logout()
+ // Return an empty observable to suppress the error so React does not crash
+ // while logout() clears the Firebase token and redirects to sign-in
+ return new Observable((observer) => {
+ observer.complete()
+ })
+ }
+ })
+
const mutationQueueLink = new MutationQueueLink()
const debounceLink = new DebounceLink(DEFAULT_DEBOUNCE_TIMEOUT)
@@ -134,7 +152,13 @@ export function createApolloClient(
httpLink
)
- const link = from([debounceLink, mutationQueueLink, authLink, splitLink])
+ const link = from([
+ errorLink,
+ debounceLink,
+ mutationQueueLink,
+ authLink,
+ splitLink
+ ])
return new ApolloClient({
ssrMode,
diff --git a/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.spec.tsx b/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.spec.tsx
index d75275450af..d1cfe0f8a04 100644
--- a/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.spec.tsx
+++ b/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.spec.tsx
@@ -15,6 +15,31 @@ const meData = {
}
describe('checkConditionalRedirect', () => {
+ it('redirects to clear-auth when GetMe throws UNAUTHENTICATED', async () => {
+ const unauthError = {
+ graphQLErrors: [{ extensions: { code: 'UNAUTHENTICATED' } }]
+ }
+ const apolloClient = {
+ query: jest.fn().mockRejectedValueOnce(unauthError)
+ } as unknown as ApolloClient
+ expect(
+ await checkConditionalRedirect({ apolloClient, resolvedUrl: '/' })
+ ).toEqual({
+ destination: '/api/clear-auth',
+ permanent: false
+ })
+ })
+
+ it('rethrows non-UNAUTHENTICATED errors from GetMe', async () => {
+ const networkError = new Error('Network error')
+ const apolloClient = {
+ query: jest.fn().mockRejectedValueOnce(networkError)
+ } as unknown as ApolloClient
+ await expect(
+ checkConditionalRedirect({ apolloClient, resolvedUrl: '/' })
+ ).rejects.toThrow('Network error')
+ })
+
it('calls apollo apolloClient', async () => {
const apolloClient = {
query: jest
diff --git a/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.ts b/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.ts
index ed9fbefd86d..43d43064673 100644
--- a/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.ts
+++ b/apps/journeys-admin/src/libs/checkConditionalRedirect/checkConditionalRedirect.ts
@@ -49,12 +49,32 @@ export async function checkConditionalRedirect({
redirect = `?redirect=${encodeURIComponent(resolvedUrl)}`
}
- const { data: me } = await apolloClient.query({
- query: GET_ME,
- variables: { input: { redirect } }
- })
+ let meResult: GetMe | undefined
+ try {
+ const { data } = await apolloClient.query({
+ query: GET_ME,
+ variables: { input: { redirect } }
+ })
+ meResult = data
+ } catch (error) {
+ const isUnauthenticated =
+ error != null &&
+ typeof error === 'object' &&
+ 'graphQLErrors' in error &&
+ Array.isArray((error as { graphQLErrors: unknown[] }).graphQLErrors) &&
+ (
+ error as { graphQLErrors: Array<{ extensions?: { code?: string } }> }
+ ).graphQLErrors.some((e) => e.extensions?.code === 'UNAUTHENTICATED')
+
+ if (isUnauthenticated) {
+ return { destination: '/api/clear-auth', permanent: false }
+ }
+ throw error
+ }
+
+ const me = meResult
- if (me.me?.__typename === 'AuthenticatedUser') {
+ if (me?.me?.__typename === 'AuthenticatedUser') {
if (!(me.me?.emailVerified ?? false)) {
if (resolvedUrl.startsWith('/users/verify')) return
return {
@@ -64,7 +84,7 @@ export async function checkConditionalRedirect({
}
}
- if (me.me?.__typename === 'AnonymousUser' && allowGuest) {
+ if (me?.me?.__typename === 'AnonymousUser' && allowGuest) {
return
}
diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json
index 5a3e18cf9a8..c36f6e0023b 100644
--- a/libs/locales/en/apps-journeys-admin.json
+++ b/libs/locales/en/apps-journeys-admin.json
@@ -41,6 +41,7 @@
"Customize Template": "Customize Template",
"Journey not found. Redirected to templates.": "Journey not found. Redirected to templates.",
"Journey Templates": "Journey Templates",
+ "Delete User": "Delete User",
"Sign In": "Sign In",
"Create New Account or Log in": "Create New Account or Log in",
"A question about sign in": "A question about sign in",
@@ -1067,6 +1068,22 @@
"End User License Agreement": "End User License Agreement",
"Community Guidelines": "Community Guidelines",
"I agree with listed above conditions and requirements": "I agree with listed above conditions and requirements",
+ "Something went wrong": "Something went wrong",
+ "An unexpected error occurred.": "An unexpected error occurred.",
+ "User deleted successfully": "User deleted successfully",
+ "User deletion failed. Check logs for details.": "User deletion failed. Check logs for details.",
+ "This action permanently deletes a user and their associated data. This cannot be undone. Always run a check first to review what will be deleted.": "This action permanently deletes a user and their associated data. This cannot be undone. Always run a check first to review what will be deleted.",
+ "Lookup By": "Lookup By",
+ "Database ID": "Database ID",
+ "User email to delete": "User email to delete",
+ "Database ID to delete": "Database ID to delete",
+ "User Email": "User Email",
+ "Checking...": "Checking...",
+ "Logs": "Logs",
+ "Deleting...": "Deleting...",
+ "Confirm User Deletion": "Confirm User Deletion",
+ "Are you sure you want to permanently delete this user? This action cannot be undone. All associated data including journeys, team memberships, and related records will be permanently removed.": "Are you sure you want to permanently delete this user? This action cannot be undone. All associated data including journeys, team memberships, and related records will be permanently removed.",
+ "Delete Permanently": "Delete Permanently",
"Open conversation": "Open conversation",
"Chat Platform": "Chat Platform",
"Username": "Username",