Multisite Kezelés¶
A FormFiller támogatja a több bérlős (multi-tenant) működést, ahol egyetlen alkalmazás példány több, egymástól elkülönített site-ot szolgál ki.
Multisite Architektúra a Frontend-en¶
flowchart TB
subgraph App["FRONTEND ALKALMAZÁS"]
subgraph SC["SITE CONTEXT"]
CS[currentSite]
AS[availableSites]
SW[switchSite]
end
SC --> Sites
subgraph Sites["SITE-OK"]
subgraph SA["Site A"]
SA1[Adatok]
SA2[Configok]
SA3[Users]
end
subgraph SB["Site B"]
SB1[Adatok]
SB2[Configok]
SB3[Users]
end
subgraph SCC["Site C"]
SC1[Adatok]
SC2[Configok]
SC3[Users]
end
end
end
Site Azonosítás¶
Subdomain Alapú¶
// utils/siteDetection.ts
export function detectSiteFromUrl(): string | null {
const hostname = window.location.hostname;
const parts = hostname.split('.');
if (parts.length >= 3) {
return parts[0]; // subdomain = site slug
}
return null; // Fő domain, nincs site
}
Path Alapú¶
export function detectSiteFromPath(): string | null {
const pathParts = window.location.pathname.split('/');
if (pathParts[1] && isValidSiteSlug(pathParts[1])) {
return pathParts[1];
}
return null;
}
SiteContext¶
Context Definíció¶
interface Site {
id: string;
name: string;
slug: string;
settings: SiteSettings;
branding?: SiteBranding;
}
interface SiteContextType {
currentSite: Site | null;
availableSites: Site[];
isLoading: boolean;
switchSite: (siteId: string) => Promise<void>;
refreshSite: () => Promise<void>;
}
const SiteContext = createContext<SiteContextType | null>(null);
SiteProvider¶
export function SiteProvider({ children }: { children: React.ReactNode }) {
const [currentSite, setCurrentSite] = useState<Site | null>(null);
const [availableSites, setAvailableSites] = useState<Site[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const initSite = async () => {
const siteSlug = detectSiteFromUrl() || detectSiteFromPath();
if (siteSlug) {
const site = await siteService.getBySlug(siteSlug);
setCurrentSite(site);
}
const sites = await siteService.getAvailableSites();
setAvailableSites(sites);
setIsLoading(false);
};
initSite();
}, []);
const switchSite = async (siteId: string) => {
const site = availableSites.find(s => s.id === siteId);
if (site) {
// URL frissítés
if (site.slug !== currentSite?.slug) {
window.location.href = `https://${site.slug}.formfiller.com`;
}
}
};
return (
<SiteContext.Provider value={{ currentSite, availableSites, isLoading, switchSite }}>
{children}
</SiteContext.Provider>
);
}
Hook Használat¶
import { useSite } from '../contexts/SiteContext';
function MyComponent() {
const { currentSite, availableSites, switchSite } = useSite();
return (
<div>
<h1>{currentSite?.name}</h1>
<SiteSwitcher sites={availableSites} onSwitch={switchSite} />
</div>
);
}
API Hívások Site Kontextusban¶
Site Header¶
Minden API hívás tartalmazza az aktuális site azonosítót:
// api/client.ts
apiClient.interceptors.request.use(config => {
const { currentSite } = useSiteStore.getState();
if (currentSite) {
config.headers['X-Site-ID'] = currentSite.id;
}
return config;
});
Site-Specifikus Endpoint-ok¶
// services/dataService.ts
export const dataService = {
async query(configId: string, options: QueryOptions) {
// Site automatikusan a header-ben
return apiClient.get(`/api/data/${configId}`, { params: options });
},
async save(configId: string, data: any) {
// Site automatikusan a header-ben
return apiClient.post(`/api/data/${configId}`, data);
}
};
Site Váltó Komponens¶
function SiteSwitcher() {
const { currentSite, availableSites, switchSite } = useSite();
const { hasPermission } = usePermissions();
// Csak ha több site-hoz van hozzáférés
if (availableSites.length <= 1) {
return null;
}
return (
<DropDownButton
text={currentSite?.name || 'Site kiválasztása'}
icon="globe"
items={availableSites}
keyExpr="id"
displayExpr="name"
onItemClick={e => switchSite(e.itemData.id)}
itemRender={site => (
<div className="site-item">
{site.branding?.logo && <img src={site.branding.logo} alt="" />}
<span>{site.name}</span>
{site.id === currentSite?.id && <Icon name="check" />}
</div>
)}
/>
);
}
Site Branding¶
Branding Konfiguráció¶
interface SiteBranding {
logo?: string;
favicon?: string;
primaryColor?: string;
secondaryColor?: string;
customCss?: string;
}
Branding Alkalmazás¶
function BrandingProvider({ children }: { children: React.ReactNode }) {
const { currentSite } = useSite();
useEffect(() => {
if (currentSite?.branding) {
const { branding } = currentSite;
// Favicon
if (branding.favicon) {
document.querySelector('link[rel="icon"]')?.setAttribute('href', branding.favicon);
}
// CSS változók
if (branding.primaryColor) {
document.documentElement.style.setProperty('--primary-color', branding.primaryColor);
}
// Egyedi CSS
if (branding.customCss) {
const style = document.createElement('style');
style.textContent = branding.customCss;
document.head.appendChild(style);
}
}
}, [currentSite]);
return <>{children}</>;
}
Site-Specifikus Konfigurációk¶
Konfiguráció Szűrés¶
function ConfigList() {
const { currentSite } = useSite();
const [configs, setConfigs] = useState<Config[]>([]);
useEffect(() => {
// API automatikusan szűri site alapján
const loadConfigs = async () => {
const data = await configService.getAll();
setConfigs(data);
};
loadConfigs();
}, [currentSite]); // Site váltáskor újratöltés
return <DataGrid dataSource={configs} />;
}
Megosztott vs Site-Specifikus Configok¶
interface Config {
id: string;
title: string;
siteId?: string; // null = globális, minden site-on elérhető
isGlobal: boolean; // Kifejezetten megosztott
}
// Szűrés a frontenden (opcionális, backend is szűr)
const visibleConfigs = configs.filter(c =>
c.isGlobal || c.siteId === currentSite?.id
);
Regisztráció és Site Létrehozás¶
Site Létrehozás Regisztrációkor¶
function RegisterWithSite() {
const [formData, setFormData] = useState({
// Felhasználói adatok
name: '',
email: '',
password: '',
// Site adatok
siteName: '',
siteSlug: ''
});
const [slugAvailable, setSlugAvailable] = useState<boolean | null>(null);
// Slug elérhetőség ellenőrzés
const checkSlugAvailability = debounce(async (slug: string) => {
if (slug.length < 3) return;
const available = await siteService.checkSlugAvailability(slug);
setSlugAvailable(available);
}, 500);
const handleSubmit = async () => {
await authService.registerWithSite(formData);
// Átirányítás az új site-ra
window.location.href = `https://${formData.siteSlug}.formfiller.com`;
};
return (
<Form>
{/* User fields */}
<SimpleItem dataField="name" />
<SimpleItem dataField="email" />
<SimpleItem dataField="password" editorType="dxTextBox" editorOptions={{ mode: 'password' }} />
{/* Site fields */}
<GroupItem caption="Új munkaterület létrehozása">
<SimpleItem dataField="siteName" label={{ text: 'Munkaterület neve' }} />
<SimpleItem
dataField="siteSlug"
label={{ text: 'URL azonosító' }}
editorOptions={{
onValueChanged: e => checkSlugAvailability(e.value)
}}
/>
{slugAvailable === false && (
<div className="error">Ez az azonosító már foglalt</div>
)}
</GroupItem>
</Form>
);
}
Felhasználó Meghívás Site-ra¶
function InviteUserToSite() {
const { currentSite } = useSite();
const [email, setEmail] = useState('');
const [role, setRole] = useState('viewer');
const handleInvite = async () => {
await siteService.inviteUser(currentSite.id, {
email,
role
});
notify('Meghívó elküldve', 'success');
};
return (
<Form>
<SimpleItem
dataField="email"
label={{ text: 'Email cím' }}
editorOptions={{ value: email, onValueChanged: e => setEmail(e.value) }}
/>
<SimpleItem
dataField="role"
label={{ text: 'Szerepkör' }}
editorType="dxSelectBox"
editorOptions={{
items: availableRoles,
value: role,
onValueChanged: e => setRole(e.value)
}}
/>
<ButtonItem>
<ButtonOptions text="Meghívás" onClick={handleInvite} />
</ButtonItem>
</Form>
);
}
Routing Multisite Módban¶
// App.tsx
function App() {
const { currentSite, isLoading } = useSite();
if (isLoading) {
return <LoadingScreen />;
}
// Ha nincs site és kötelező
if (!currentSite && REQUIRE_SITE) {
return <Navigate to="/select-site" />;
}
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/form/:configId" element={<FormPage />} />
{/* Site admin - csak owner/admin */}
<Route
path="/site-settings"
element={
<PermissionGate role={['owner', 'admin']}>
<SiteSettingsPage />
</PermissionGate>
}
/>
</Routes>
);
}
Best Practices¶
1. Site Kontextus Mindig Elérhető¶
// Biztosítsd, hogy SiteProvider a komponens fa tetején legyen
<AuthProvider>
<SiteProvider>
<PermissionProvider>
<App />
</PermissionProvider>
</SiteProvider>
</AuthProvider>
2. Site Váltás Kezelése¶
// Site váltáskor töröld a cache-t
const switchSite = async (siteId: string) => {
// Cache törlés
queryClient.clear();
// Navigáció az új site-ra
// ...
};