mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-04-24 11:18:35 -05:00
feat: activity detection (#2055)
* feat: activity detection * address comments
This commit is contained in:
@@ -100,6 +100,11 @@ export interface APICloudMetadata {
|
||||
* @example true
|
||||
*/
|
||||
requireBillingForManagedCompute?: boolean;
|
||||
/**
|
||||
* the inactivity timeout to log out for user sessions in milliseconds
|
||||
* @example 3600000
|
||||
*/
|
||||
inactivityLogoutMs?: number;
|
||||
}
|
||||
|
||||
export interface APIErrors {
|
||||
@@ -494,6 +499,9 @@ export interface LogLine {
|
||||
timestamp: string;
|
||||
instance: string;
|
||||
line: string;
|
||||
metadata?: object;
|
||||
retryCount?: number;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export interface LogLineList {
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface ApiConfig<SecurityDataType = unknown>
|
||||
|
||||
export enum ContentType {
|
||||
Json = "application/json",
|
||||
JsonApi = "application/vnd.api+json",
|
||||
FormData = "multipart/form-data",
|
||||
UrlEncoded = "application/x-www-form-urlencoded",
|
||||
Text = "text/plain",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
interface UseInactivityDetectionOptions {
|
||||
timeoutMs?: number;
|
||||
throttleMs?: number;
|
||||
events?: string[];
|
||||
onInactive?: () => void;
|
||||
}
|
||||
|
||||
export function useInactivityDetection(
|
||||
options: UseInactivityDetectionOptions = {},
|
||||
) {
|
||||
const {
|
||||
timeoutMs = -1, // -1 means disabled
|
||||
throttleMs = 1000, // 1 second throttle
|
||||
events = [
|
||||
'mousedown',
|
||||
'mousemove',
|
||||
'keypress',
|
||||
'scroll',
|
||||
'touchstart',
|
||||
'click',
|
||||
],
|
||||
onInactive = () => {},
|
||||
} = options;
|
||||
|
||||
const enabled = timeoutMs > 0;
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const throttleRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const resetTimeout = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
onInactive();
|
||||
}, timeoutMs);
|
||||
}, [onInactive, timeoutMs]);
|
||||
|
||||
const throttledResetTimeout = useCallback(() => {
|
||||
if (throttleRef.current) {
|
||||
return; // Already throttled
|
||||
}
|
||||
|
||||
throttleRef.current = setTimeout(() => {
|
||||
throttleRef.current = null;
|
||||
}, throttleMs);
|
||||
|
||||
resetTimeout();
|
||||
}, [throttleMs, resetTimeout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetTimeout();
|
||||
|
||||
events.forEach((event) => {
|
||||
document.addEventListener(event, throttledResetTimeout, true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (throttleRef.current) {
|
||||
clearTimeout(throttleRef.current);
|
||||
}
|
||||
events.forEach((event) => {
|
||||
document.removeEventListener(event, throttledResetTimeout, true);
|
||||
});
|
||||
};
|
||||
}, [
|
||||
enabled,
|
||||
timeoutMs,
|
||||
throttleMs,
|
||||
resetTimeout,
|
||||
events,
|
||||
throttledResetTimeout,
|
||||
]);
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -1,20 +1,49 @@
|
||||
import MainNav from '@/components/molecules/nav-bar/nav-bar';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import api, { queries, TenantVersion, User } from '@/lib/api';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import SupportChat from '@/components/molecules/support-chat';
|
||||
import AnalyticsProvider from '@/components/molecules/analytics-provider';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useContextFromParent } from '@/lib/outlet';
|
||||
import { useTenant } from '@/lib/atoms';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useInactivityDetection } from '@/pages/auth/hooks/use-inactivity-detection';
|
||||
import { cloudApi } from '@/lib/api/api';
|
||||
|
||||
export default function Authenticated() {
|
||||
const [hasHasBanner, setHasBanner] = useState(false);
|
||||
|
||||
const { tenant } = useTenant();
|
||||
|
||||
const { data: cloudMetadata } = useQuery({
|
||||
queryKey: ['metadata'],
|
||||
queryFn: async () => {
|
||||
const res = await cloudApi.metadataGet();
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ['user:update:logout'],
|
||||
mutationFn: async () => {
|
||||
await api.userUpdateLogout();
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate('/auth/login');
|
||||
},
|
||||
});
|
||||
|
||||
useInactivityDetection({
|
||||
timeoutMs: cloudMetadata?.inactivityLogoutMs || -1,
|
||||
onInactive: () => {
|
||||
logoutMutation.mutate();
|
||||
},
|
||||
});
|
||||
|
||||
const userQuery = useQuery({
|
||||
queryKey: ['user:get:current'],
|
||||
retry: false,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@/components/v1/ui/button';
|
||||
import { Separator } from '@/components/v1/ui/separator';
|
||||
import { useState } from 'react';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import api, { queries, TenantVersion, UpdateTenantRequest } from '@/lib/api';
|
||||
import { Switch } from '@/components/v1/ui/switch';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@/components/v1/ui/dialog';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert';
|
||||
import { useCurrentTenantId, useTenantDetails } from '@/hooks/use-tenant';
|
||||
import { cloudApi } from '@/lib/api/api';
|
||||
|
||||
export default function TenantSettings() {
|
||||
const { tenant } = useTenantDetails();
|
||||
@@ -33,6 +34,8 @@ export default function TenantSettings() {
|
||||
<Separator className="my-4" />
|
||||
<AnalyticsOptOut />
|
||||
<Separator className="my-4" />
|
||||
<InactivityTimeout />
|
||||
<Separator className="my-4" />
|
||||
<TenantVersionSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,3 +242,74 @@ const AnalyticsOptOut: React.FC = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const InactivityTimeout: React.FC = () => {
|
||||
const { data: cloudMetadata } = useQuery({
|
||||
queryKey: ['metadata'],
|
||||
queryFn: async () => {
|
||||
const res = await cloudApi.metadataGet();
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const formatTimeoutMs = (timeoutMs: number | undefined) => {
|
||||
if (!timeoutMs || timeoutMs <= 0) {
|
||||
return 'Disabled';
|
||||
}
|
||||
|
||||
const minutes = Math.floor(timeoutMs / 60000);
|
||||
if (minutes < 60) {
|
||||
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
|
||||
if (remainingMinutes === 0) {
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''} ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}`;
|
||||
};
|
||||
|
||||
const isDisabled =
|
||||
!cloudMetadata?.inactivityLogoutMs || cloudMetadata.inactivityLogoutMs <= 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-xl font-semibold leading-tight text-foreground">
|
||||
Inactivity Timeout
|
||||
</h2>
|
||||
<Separator className="my-4" />
|
||||
{isDisabled ? (
|
||||
<>
|
||||
<p className="text-gray-700 dark:text-gray-300 my-4">
|
||||
Inactivity timeout is currently <strong>disabled</strong>. This
|
||||
feature automatically logs out users after a period of inactivity to
|
||||
enhance security.
|
||||
</p>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
To enable inactivity timeout for your tenant, please contact
|
||||
support.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-700 dark:text-gray-300 my-4">
|
||||
Current inactivity logout timeout:{' '}
|
||||
<strong>
|
||||
{formatTimeoutMs(cloudMetadata?.inactivityLogoutMs)}
|
||||
</strong>
|
||||
</p>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Please contact support to change this configuration.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user