mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
fix paths and frontend validation
This commit is contained in:
34
frontend/src/core/auth/UseSession.tsx
Normal file
34
frontend/src/core/auth/UseSession.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createContext, useContext, type ReactNode } from 'react';
|
||||
|
||||
interface AuthContextValue {
|
||||
session: null;
|
||||
user: null;
|
||||
loading: boolean;
|
||||
error: null;
|
||||
signOut: () => Promise<void>;
|
||||
refreshSession: () => Promise<void>;
|
||||
}
|
||||
|
||||
const defaultValue: AuthContextValue = {
|
||||
session: null,
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
signOut: async () => {},
|
||||
refreshSession: async () => {},
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue>(defaultValue);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return <AuthContext.Provider value={defaultValue}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
return useContext(AuthContext);
|
||||
}
|
||||
|
||||
export function useAuthDebug(): AuthContextValue {
|
||||
return useAuth();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ONBOARDING_SESSION_EVENT,
|
||||
SERVER_LICENSE_REQUEST_EVENT,
|
||||
type ServerLicenseRequestPayload,
|
||||
} from '@core/constants/events';
|
||||
} from '@app/constants/events';
|
||||
import { useServerExperience } from '@app/hooks/useServerExperience';
|
||||
|
||||
interface InitialModalHandlers {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './AnimatedSlideBackground.module.css';
|
||||
import { AnimatedSlideBackgroundProps } from '../../../types/types';
|
||||
import styles from '@app/components/onboarding/slides/AnimatedSlideBackground.module.css';
|
||||
import { AnimatedSlideBackgroundProps } from '@app/types/types';
|
||||
|
||||
type CircleStyles = React.CSSProperties & {
|
||||
'--circle-move-x'?: string;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SlideConfig } from '../../../types/types';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
|
||||
import { DesktopInstallTitle, type OSOption } from './DesktopInstallTitle';
|
||||
import { SlideConfig } from '@app/types/types';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig';
|
||||
import { DesktopInstallTitle, type OSOption } from '@app/components/onboarding/slides/DesktopInstallTitle';
|
||||
|
||||
export type { OSOption };
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { SlideConfig, LicenseNotice } from '../../../types/types';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
|
||||
import { SlideConfig, LicenseNotice } from '@app/types/types';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig';
|
||||
|
||||
interface PlanOverviewSlideProps {
|
||||
isAdmin: boolean;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Select } from '@mantine/core';
|
||||
import styles from '../InitialOnboardingModal/InitialOnboardingModal.module.css';
|
||||
import { SlideConfig } from '../../../types/types';
|
||||
import { SlideConfig } from '@app/types/types';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig';
|
||||
import i18n from '@app/i18n';
|
||||
|
||||
interface SecurityCheckSlideProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { SlideConfig, LicenseNotice } from '../../../types/types';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
|
||||
import { SlideConfig, LicenseNotice } from '@app/types/types';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig';
|
||||
import i18n from '@app/i18n';
|
||||
|
||||
interface ServerLicenseSlideProps {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { SlideConfig } from '../../../types/types';
|
||||
import styles from '../InitialOnboardingModal/InitialOnboardingModal.module.css';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
|
||||
import { SlideConfig } from '@app/types/types';
|
||||
import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css';
|
||||
import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig';
|
||||
|
||||
function WelcomeSlideTitle() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnimatedCircleConfig } from '../../../types/types';
|
||||
import { AnimatedCircleConfig } from '@app/types/types';
|
||||
|
||||
/**
|
||||
* Unified circle background configuration used across all onboarding slides.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import {
|
||||
UPGRADE_BANNER_ALERT_EVENT,
|
||||
type UpgradeBannerAlertPayload,
|
||||
} from '@core/constants/events';
|
||||
} from '@app/constants/events';
|
||||
|
||||
export interface LicenseAlertState {
|
||||
active: boolean;
|
||||
|
||||
157
frontend/src/core/hooks/useServerExperience.ts
Normal file
157
frontend/src/core/hooks/useServerExperience.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
|
||||
const SELF_REPORTED_ADMIN_KEY = 'stirling-self-reported-admin';
|
||||
const FREE_TIER_LIMIT = 5;
|
||||
|
||||
type UserCountSource = 'admin' | 'estimate' | 'unknown';
|
||||
|
||||
export type ServerScenarioKey =
|
||||
| 'unknown'
|
||||
| 'licensed'
|
||||
| 'no-login-user-under-limit-no-license'
|
||||
| 'no-login-admin-under-limit-no-license'
|
||||
| 'no-login-user-over-limit-no-license'
|
||||
| 'no-login-admin-over-limit-no-license'
|
||||
| 'login-user-under-limit-no-license'
|
||||
| 'login-admin-under-limit-no-license'
|
||||
| 'login-user-over-limit-no-license'
|
||||
| 'login-admin-over-limit-no-license';
|
||||
|
||||
export interface ServerExperienceValue {
|
||||
loginEnabled: boolean;
|
||||
configIsAdmin: boolean;
|
||||
effectiveIsAdmin: boolean;
|
||||
selfReportedAdmin: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isNewServer: boolean | null;
|
||||
isNewUser: boolean | null;
|
||||
premiumEnabled: boolean | null;
|
||||
license: string | undefined;
|
||||
runningProOrHigher: boolean | undefined;
|
||||
runningEE: boolean | undefined;
|
||||
hasPaidLicense: boolean;
|
||||
licenseKeyValid: boolean | null;
|
||||
licenseLoading: boolean;
|
||||
licenseInfoAvailable: boolean;
|
||||
totalUsers: number | null;
|
||||
weeklyActiveUsers: number | null;
|
||||
userCountLoading: boolean;
|
||||
userCountError: string | null;
|
||||
userCountSource: UserCountSource;
|
||||
userCountResolved: boolean;
|
||||
overFreeTierLimit: boolean | null;
|
||||
freeTierLimit: number;
|
||||
refreshUserCounts: () => Promise<void>;
|
||||
setSelfReportedAdmin: (value: boolean) => void;
|
||||
scenarioKey: ServerScenarioKey;
|
||||
}
|
||||
|
||||
function readSelfReportedAdmin(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return window.localStorage.getItem(SELF_REPORTED_ADMIN_KEY) === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function useServerExperience(): ServerExperienceValue {
|
||||
const { config } = useAppConfig();
|
||||
const [selfReportedAdmin, setSelfReportedAdminState] = useState<boolean>(readSelfReportedAdmin);
|
||||
|
||||
const loginEnabled = config?.enableLogin !== false;
|
||||
const configIsAdmin = Boolean(config?.isAdmin);
|
||||
const effectiveIsAdmin = configIsAdmin || (!loginEnabled && selfReportedAdmin);
|
||||
const hasPaidLicense = config?.license === 'PRO' || config?.license === 'ENTERPRISE';
|
||||
|
||||
const setSelfReportedAdmin = useCallback((value: boolean) => {
|
||||
setSelfReportedAdminState(value);
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (value) {
|
||||
window.localStorage.setItem(SELF_REPORTED_ADMIN_KEY, 'true');
|
||||
} else {
|
||||
window.localStorage.removeItem(SELF_REPORTED_ADMIN_KEY);
|
||||
}
|
||||
} catch {
|
||||
// ignore storage write failures
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key === SELF_REPORTED_ADMIN_KEY) {
|
||||
setSelfReportedAdminState(event.newValue === 'true');
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', handleStorage);
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.isNewServer && !loginEnabled && !selfReportedAdmin) {
|
||||
setSelfReportedAdmin(true);
|
||||
}
|
||||
}, [config?.isNewServer, loginEnabled, selfReportedAdmin, setSelfReportedAdmin]);
|
||||
|
||||
const scenarioKey: ServerScenarioKey = useMemo(() => {
|
||||
if (hasPaidLicense) {
|
||||
return 'licensed';
|
||||
}
|
||||
return 'unknown';
|
||||
}, [hasPaidLicense]);
|
||||
|
||||
const value = useMemo<ServerExperienceValue>(() => ({
|
||||
loginEnabled,
|
||||
configIsAdmin,
|
||||
effectiveIsAdmin,
|
||||
selfReportedAdmin,
|
||||
isAuthenticated: false,
|
||||
isNewServer: config?.isNewServer ?? null,
|
||||
isNewUser: config?.isNewUser ?? null,
|
||||
premiumEnabled: config?.premiumEnabled ?? null,
|
||||
license: config?.license,
|
||||
runningProOrHigher: config?.runningProOrHigher,
|
||||
runningEE: config?.runningEE,
|
||||
hasPaidLicense,
|
||||
licenseKeyValid: config?.premiumEnabled ?? null,
|
||||
licenseLoading: false,
|
||||
licenseInfoAvailable: false,
|
||||
totalUsers: null,
|
||||
weeklyActiveUsers: null,
|
||||
userCountLoading: false,
|
||||
userCountError: null,
|
||||
userCountSource: 'unknown',
|
||||
userCountResolved: false,
|
||||
overFreeTierLimit: null,
|
||||
freeTierLimit: FREE_TIER_LIMIT,
|
||||
refreshUserCounts: async () => {},
|
||||
setSelfReportedAdmin,
|
||||
scenarioKey,
|
||||
}), [
|
||||
config?.isNewServer,
|
||||
config?.isNewUser,
|
||||
config?.license,
|
||||
config?.premiumEnabled,
|
||||
config?.runningEE,
|
||||
config?.runningProOrHigher,
|
||||
configIsAdmin,
|
||||
effectiveIsAdmin,
|
||||
hasPaidLicense,
|
||||
loginEnabled,
|
||||
scenarioKey,
|
||||
selfReportedAdmin,
|
||||
setSelfReportedAdmin,
|
||||
]);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
192
frontend/src/core/testing/serverExperienceSimulations.ts
Normal file
192
frontend/src/core/testing/serverExperienceSimulations.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { AppConfig } from '@app/contexts/AppConfigContext';
|
||||
|
||||
interface LicenseInfo {
|
||||
licenseType: string;
|
||||
enabled: boolean;
|
||||
maxUsers: number;
|
||||
hasKey: boolean;
|
||||
}
|
||||
|
||||
interface WauResponse {
|
||||
trackingSince: string;
|
||||
daysOnline: number;
|
||||
totalUniqueBrowsers: number;
|
||||
weeklyActiveUsers: number;
|
||||
}
|
||||
|
||||
interface AdminUsageResponse {
|
||||
totalUsers?: number;
|
||||
}
|
||||
|
||||
interface SimulationScenario {
|
||||
label: string;
|
||||
appConfig: AppConfig;
|
||||
wau?: WauResponse;
|
||||
adminUsage?: AdminUsageResponse;
|
||||
licenseInfo: LicenseInfo;
|
||||
}
|
||||
|
||||
const DEV_TESTING_MODE = false;
|
||||
const SIMULATION_INDEX = 0;
|
||||
|
||||
const FREE_LICENSE_INFO: LicenseInfo = {
|
||||
licenseType: 'NORMAL',
|
||||
enabled: false,
|
||||
maxUsers: 5,
|
||||
hasKey: false,
|
||||
};
|
||||
|
||||
const BASE_NO_LOGIN_CONFIG: AppConfig = {
|
||||
enableAnalytics: true,
|
||||
appVersion: '2.0.0',
|
||||
serverCertificateEnabled: false,
|
||||
enableAlphaFunctionality: false,
|
||||
serverPort: 8080,
|
||||
premiumEnabled: false,
|
||||
runningProOrHigher: false,
|
||||
runningEE: false,
|
||||
enableLogin: false,
|
||||
activeSecurity: false,
|
||||
languages: [],
|
||||
contextPath: '/',
|
||||
license: 'NORMAL',
|
||||
baseUrl: 'http://localhost',
|
||||
enableEmailInvites: true,
|
||||
};
|
||||
|
||||
const BASE_LOGIN_CONFIG: AppConfig = {
|
||||
...BASE_NO_LOGIN_CONFIG,
|
||||
enableLogin: true,
|
||||
activeSecurity: true,
|
||||
};
|
||||
|
||||
const SIMULATION_SCENARIOS: SimulationScenario[] = [
|
||||
{
|
||||
label: 'no-login-user-under-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_NO_LOGIN_CONFIG,
|
||||
},
|
||||
wau: {
|
||||
trackingSince: '2025-11-18T23:20:12.520884200Z',
|
||||
daysOnline: 0,
|
||||
totalUniqueBrowsers: 3,
|
||||
weeklyActiveUsers: 3,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'no-login-admin-under-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_NO_LOGIN_CONFIG,
|
||||
},
|
||||
wau: {
|
||||
trackingSince: '2025-10-01T00:00:00Z',
|
||||
daysOnline: 14,
|
||||
totalUniqueBrowsers: 4,
|
||||
weeklyActiveUsers: 4,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'no-login-user-over-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_NO_LOGIN_CONFIG,
|
||||
},
|
||||
wau: {
|
||||
trackingSince: '2025-09-01T00:00:00Z',
|
||||
daysOnline: 30,
|
||||
totalUniqueBrowsers: 12,
|
||||
weeklyActiveUsers: 9,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'no-login-admin-over-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_NO_LOGIN_CONFIG,
|
||||
},
|
||||
wau: {
|
||||
trackingSince: '2025-08-15T00:00:00Z',
|
||||
daysOnline: 45,
|
||||
totalUniqueBrowsers: 18,
|
||||
weeklyActiveUsers: 12,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'login-user-under-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_LOGIN_CONFIG,
|
||||
isAdmin: false,
|
||||
},
|
||||
adminUsage: {
|
||||
totalUsers: 3,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'login-admin-under-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_LOGIN_CONFIG,
|
||||
isAdmin: true,
|
||||
},
|
||||
adminUsage: {
|
||||
totalUsers: 4,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'login-user-over-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_LOGIN_CONFIG,
|
||||
isAdmin: false,
|
||||
},
|
||||
adminUsage: {
|
||||
totalUsers: 12,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
{
|
||||
label: 'login-admin-over-limit (no-license)',
|
||||
appConfig: {
|
||||
...BASE_LOGIN_CONFIG,
|
||||
isAdmin: true,
|
||||
},
|
||||
adminUsage: {
|
||||
totalUsers: 57,
|
||||
},
|
||||
licenseInfo: { ...FREE_LICENSE_INFO },
|
||||
},
|
||||
];
|
||||
|
||||
function getActiveScenario(): SimulationScenario | null {
|
||||
if (!DEV_TESTING_MODE) {
|
||||
return null;
|
||||
}
|
||||
const scenario = SIMULATION_SCENARIOS[SIMULATION_INDEX];
|
||||
if (!scenario) {
|
||||
console.warn('[Simulation] SIMULATION_INDEX out of range, using live backend.');
|
||||
return null;
|
||||
}
|
||||
console.warn(`[Simulation] Using scenario #${SIMULATION_INDEX} (${scenario.label}).`);
|
||||
return scenario;
|
||||
}
|
||||
|
||||
export function getSimulatedAppConfig(): AppConfig | null {
|
||||
return getActiveScenario()?.appConfig ?? null;
|
||||
}
|
||||
|
||||
export function getSimulatedWauResponse(): WauResponse | null {
|
||||
return getActiveScenario()?.wau ?? null;
|
||||
}
|
||||
|
||||
export function getSimulatedAdminUsage(): AdminUsageResponse | null {
|
||||
return getActiveScenario()?.adminUsage ?? null;
|
||||
}
|
||||
|
||||
export function getSimulatedLicenseInfo(): LicenseInfo | null {
|
||||
return getActiveScenario()?.licenseInfo ?? null;
|
||||
}
|
||||
|
||||
export const DEV_TESTING_ENABLED = DEV_TESTING_MODE;
|
||||
|
||||
Reference in New Issue
Block a user