Add dedicated endpoint for footer info

This commit is contained in:
Reece
2025-11-28 13:50:48 +00:00
parent e8edc2f969
commit 214ebd9be4
8 changed files with 109 additions and 40 deletions
@@ -159,6 +159,8 @@ public class RequestUriUtils {
|| trimmedUri.startsWith(
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
// enableLogin)
|| trimmedUri.startsWith(
"/api/v1/ui-data/footer-info") // Public footer configuration
|| trimmedUri.startsWith("/v1/api-docs")
|| trimmedUri.startsWith("/api/v1/invite/validate")
|| trimmedUri.startsWith("/api/v1/invite/accept")
@@ -58,6 +58,21 @@ public class UIDataController {
this.runtimePathConfig = runtimePathConfig;
}
@GetMapping("/footer-info")
@Operation(summary = "Get public footer configuration data")
public ResponseEntity<FooterData> getFooterData() {
FooterData data = new FooterData();
data.setAnalyticsEnabled(applicationProperties.getSystem().getEnableAnalytics());
data.setTermsAndConditions(applicationProperties.getLegal().getTermsAndConditions());
data.setPrivacyPolicy(applicationProperties.getLegal().getPrivacyPolicy());
data.setAccessibilityStatement(
applicationProperties.getLegal().getAccessibilityStatement());
data.setCookiePolicy(applicationProperties.getLegal().getCookiePolicy());
data.setImpressum(applicationProperties.getLegal().getImpressum());
return ResponseEntity.ok(data);
}
@GetMapping("/home")
@Operation(summary = "Get home page data")
public ResponseEntity<HomeData> getHomeData() {
@@ -237,6 +252,16 @@ public class UIDataController {
}
// Data classes
@Data
public static class FooterData {
private Boolean analyticsEnabled;
private String termsAndConditions;
private String privacyPolicy;
private String accessibilityStatement;
private String cookiePolicy;
private String impressum;
}
@Data
public static class HomeData {
private boolean showSurveyFromDocker;
@@ -150,7 +150,6 @@ public class ProprietaryUIDataController {
data.setFirstTimeSetup(isFirstTimeSetup);
data.setShowDefaultCredentials(showDefaultCredentials);
data.setEnableAnalytics(applicationProperties.getSystem().getEnableAnalytics());
OAUTH2 oauth = securityProps.getOauth2();
@@ -492,7 +491,6 @@ public class ProprietaryUIDataController {
private boolean altLogin;
private boolean firstTimeSetup;
private boolean showDefaultCredentials;
private Boolean enableAnalytics;
}
@Data
@@ -299,7 +299,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
contextPath + "/api/v1/auth/refresh",
contextPath + "/api/v1/auth/me",
contextPath + "/api/v1/invite/validate",
contextPath + "/api/v1/invite/accept"
contextPath + "/api/v1/invite/accept",
contextPath + "/api/v1/ui-data/footer-info"
};
for (String pattern : publicApiPatterns) {
@@ -54,17 +54,17 @@
--cc-secondary-color: #b0b0b0;
--cc-btn-primary-bg: #4dabf7;
--cc-btn-primary-color: #2d2d2d;
--cc-btn-primary-color: #ffffff;
--cc-btn-primary-border-color: #4dabf7;
--cc-btn-primary-hover-bg: #3d3d3d;
--cc-btn-primary-hover-color: #e5e5e5;
--cc-btn-primary-hover-color: #ffffff;
--cc-btn-primary-hover-border-color: #3d3d3d;
--cc-btn-secondary-bg: #3d3d3d;
--cc-btn-secondary-color: #e5e5e5;
--cc-btn-secondary-color: #ffffff;
--cc-btn-secondary-border-color: #3d3d3d;
--cc-btn-secondary-hover-bg: #4dabf7;
--cc-btn-secondary-hover-color: #2d2d2d;
--cc-btn-secondary-hover-color: #ffffff;
--cc-btn-secondary-hover-border-color: #4dabf7;
--cc-separator-border-color: #555555;
@@ -196,10 +196,6 @@
color: var(--cc-primary-color) !important;
}
#cc-main .cm__btn {
color: var(--cc-btn-primary-color) !important;
}
#cc-main .cm__footer {
color: var(--cc-primary-color) !important;
}
+26 -10
View File
@@ -1,6 +1,7 @@
import { Flex } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useCookieConsent } from '@app/hooks/useCookieConsent';
import { useFooterInfo } from '@app/hooks/useFooterInfo';
interface FooterProps {
privacyPolicy?: string;
@@ -18,11 +19,26 @@ export default function Footer({
accessibilityStatement,
cookiePolicy,
impressum,
analyticsEnabled = false,
analyticsEnabled,
forceLightMode = false
}: FooterProps) {
const { t } = useTranslation();
const { showCookiePreferences } = useCookieConsent({ analyticsEnabled, forceLightMode });
const { footerInfo } = useFooterInfo();
console.log('[Footer] Props analyticsEnabled:', analyticsEnabled);
console.log('[Footer] Fetched footerInfo:', footerInfo);
// Use props if provided, otherwise fall back to fetched footer info
const finalAnalyticsEnabled = analyticsEnabled ?? footerInfo?.analyticsEnabled ?? false;
const finalPrivacyPolicy = privacyPolicy ?? footerInfo?.privacyPolicy;
const finalTermsAndConditions = termsAndConditions ?? footerInfo?.termsAndConditions;
const finalAccessibilityStatement = accessibilityStatement ?? footerInfo?.accessibilityStatement;
const finalCookiePolicy = cookiePolicy ?? footerInfo?.cookiePolicy;
const finalImpressum = impressum ?? footerInfo?.impressum;
console.log('[Footer] Final analyticsEnabled:', finalAnalyticsEnabled);
const { showCookiePreferences } = useCookieConsent({ analyticsEnabled: finalAnalyticsEnabled, forceLightMode });
// Default URLs
const defaultTermsUrl = "https://www.stirling.com/legal/terms-of-service";
@@ -30,9 +46,9 @@ export default function Footer({
const defaultAccessibilityUrl = "https://www.stirling.com/accessibility";
// Use provided URLs or fall back to defaults
const finalTermsUrl = termsAndConditions || defaultTermsUrl;
const finalPrivacyUrl = privacyPolicy || defaultPrivacyUrl;
const finalAccessibilityUrl = accessibilityStatement || defaultAccessibilityUrl;
const finalTermsUrl = finalTermsAndConditions || defaultTermsUrl;
const finalPrivacyUrl = finalPrivacyPolicy || defaultPrivacyUrl;
const finalAccessibilityUrl = finalAccessibilityStatement || defaultAccessibilityUrl;
// Helper to check if a value is valid (not null/undefined/empty string)
const isValidLink = (link?: string) => link && link.trim().length > 0;
@@ -87,27 +103,27 @@ export default function Footer({
>
{t('legal.accessibility', 'Accessibility')}
</a>
{isValidLink(cookiePolicy) && (
{isValidLink(finalCookiePolicy) && (
<a
className="footer-link px-3"
target="_blank"
rel="noopener noreferrer"
href={cookiePolicy}
href={finalCookiePolicy}
>
{t('legal.cookie', 'Cookie Policy')}
</a>
)}
{isValidLink(impressum) && (
{isValidLink(finalImpressum) && (
<a
className="footer-link px-3"
target="_blank"
rel="noopener noreferrer"
href={impressum}
href={finalImpressum}
>
{t('legal.impressum', 'Impressum')}
</a>
)}
{analyticsEnabled && (
{finalAnalyticsEnabled && (
<button
className="footer-link px-3"
id="cookieBanner"
+49
View File
@@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import apiClient from '@app/services/apiClient';
export interface FooterInfo {
analyticsEnabled?: boolean;
termsAndConditions?: string;
privacyPolicy?: string;
accessibilityStatement?: string;
cookiePolicy?: string;
impressum?: string;
}
/**
* Hook to fetch public footer configuration data.
* This endpoint is always accessible without authentication.
*/
export function useFooterInfo() {
const [footerInfo, setFooterInfo] = useState<FooterInfo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchFooterInfo = async () => {
try {
setLoading(true);
console.log('[useFooterInfo] Fetching footer info from /api/v1/ui-data/footer-info...');
const response = await apiClient.get<FooterInfo>('/api/v1/ui-data/footer-info', {
suppressErrorToast: true,
} as any);
console.log('[useFooterInfo] Footer info received:', response.data);
setFooterInfo(response.data);
setError(null);
} catch (err) {
console.error('[useFooterInfo] Failed to fetch footer info:', err);
setError(err as Error);
// Set defaults on error
setFooterInfo({
analyticsEnabled: false,
});
} finally {
setLoading(false);
}
};
fetchFooterInfo();
}, []);
return { footerInfo, loading, error };
}
@@ -5,7 +5,6 @@ import buildLoginSlides from '@app/components/shared/loginSlides';
import styles from '@app/routes/authShared/AuthLayout.module.css';
import { useLogoVariant } from '@app/hooks/useLogoVariant';
import Footer from '@app/components/shared/Footer';
import apiClient from '@app/services/apiClient';
interface AuthLayoutProps {
children: React.ReactNode
@@ -15,23 +14,9 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
const { t } = useTranslation();
const cardRef = useRef<HTMLDivElement | null>(null);
const [hideRightPanel, setHideRightPanel] = useState(false);
const [enableAnalytics, setEnableAnalytics] = useState(false);
const logoVariant = useLogoVariant();
const imageSlides = useMemo(() => buildLoginSlides(logoVariant, t), [logoVariant, t]);
// Fetch enableAnalytics from login endpoint
useEffect(() => {
const fetchLoginData = async () => {
try {
const response = await apiClient.get('/api/v1/proprietary/ui-data/login');
setEnableAnalytics(response.data.enableAnalytics ?? false);
} catch (err) {
console.error('[AuthLayout] Failed to fetch login data:', err);
}
};
fetchLoginData();
}, []);
// Force light mode on auth pages
useEffect(() => {
const htmlElement = document.documentElement;
@@ -84,10 +69,7 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
)}
</div>
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, width: '100%', zIndex: 10 }}>
<Footer
analyticsEnabled={enableAnalytics}
forceLightMode={true}
/>
<Footer forceLightMode={true} />
</div>
</div>
);