Compare commits

..
Author SHA1 Message Date
Anthony Stirling 79e2366d67 fix audit 2025-11-28 20:57:56 +00:00
James BruntonandGitHub 731743b618 Don't block desktop app on backend starting up (#5041)
# Description of Changes
Start bundled backend instantly on startup of app and don't wait on it
being fully up to spawn app. This is techincally wasteful curently on
self-hosted mode where everything runs remotely, but in the future we'll
probably route simple operations to the local machine regardless of
connection, and it stops unnecessary waiting in the offline mode.
2025-11-27 15:54:35 +00:00
ConnorYohandGitHub 04c4aec0d8 Disable admin plan section when no login (#5039)
Admin plan section matches other admin sections

<img width="1013" height="629" alt="image"
src="https://github.com/user-attachments/assets/39e9fad7-461c-491d-99cb-4b140292f2f4"
/>
<img width="730" height="595" alt="image"
src="https://github.com/user-attachments/assets/b26354d2-5401-40b3-8ca7-3b48b26b644e"
/>
2025-11-27 13:57:04 +00:00
Anthony StirlingandGitHub f63df148ad Create LICENSE 2025-11-27 11:14:15 +00:00
Anthony StirlingandGitHub 6f0be94bd6 Update LICENSE with proprietary directory details
Added licensing information for proprietary directories.
2025-11-27 11:11:53 +00:00
12 changed files with 113 additions and 39 deletions
+4
View File
@@ -6,6 +6,10 @@ Portions of this software are licensed as follows:
* All content that resides under the "app/proprietary/" directory of this repository,
if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE".
* All content that resides under the "frontend/src/proprietary/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/proprietary/LICENSE".
* All content that resides under the "frontend/src/desktop/" directory of this repository,
if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE".
* Content outside of the above mentioned directories or restrictions above is
available under the MIT License as defined below.
@@ -398,10 +398,6 @@ pub async fn start_backend(
e
})?;
// Wait for the backend to start
println!("⏳ Waiting for backend startup...");
tokio::time::sleep(std::time::Duration::from_millis(10000)).await;
// Reset the starting flag since startup is complete
reset_starting_flag();
add_log("✅ Backend startup sequence completed, starting flag cleared".to_string());
+12 -1
View File
@@ -66,7 +66,7 @@ pub fn run() {
// Emit a generic notification that files were added (frontend will re-read storage)
let _ = app.emit("files-changed", ());
}))
.setup(|_app| {
.setup(|app| {
add_log("🚀 Tauri app setup started".to_string());
// Process command line arguments on first launch
@@ -78,6 +78,17 @@ pub fn run() {
}
}
// Start backend immediately, non-blocking
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
add_log("🚀 Starting bundled backend in background".to_string());
let connection_state = app_handle.state::<AppConnectionState>();
if let Err(e) = commands::backend::start_backend(app_handle.clone(), connection_state).await {
add_log(format!("⚠️ Backend start failed: {}", e));
}
});
add_log("🔍 DEBUG: Setup completed".to_string());
Ok(())
})
+5 -3
View File
@@ -4,7 +4,7 @@ import auditService, { AuditFilters } from '@app/services/auditService';
/**
* Shared hook for managing audit filters across components
*/
export function useAuditFilters(initialFilters: Partial<AuditFilters> = {}) {
export function useAuditFilters(initialFilters: Partial<AuditFilters> = {}, loginEnabled: boolean = true) {
const [eventTypes, setEventTypes] = useState<string[]>([]);
const [users, setUsers] = useState<string[]>([]);
const [filters, setFilters] = useState<AuditFilters>({
@@ -30,8 +30,10 @@ export function useAuditFilters(initialFilters: Partial<AuditFilters> = {}) {
}
};
fetchMetadata();
}, []);
if (loginEnabled) {
fetchMetadata();
}
}, [loginEnabled]);
const handleFilterChange = (key: keyof AuditFilters, value: any) => {
setFilters((prev) => ({ ...prev, [key]: value }));
@@ -31,10 +31,10 @@ export function AppProviders({ children }: { children: ReactNode }) {
}
}, [setupComplete, isFirstLaunch, connectionMode]);
// Only start bundled backend if in SaaS mode (local backend) and setup is complete
// Self-hosted mode connects to remote server so doesn't need local backend
const shouldStartBackend = setupComplete && !isFirstLaunch && connectionMode === 'saas';
useBackendInitializer(shouldStartBackend);
// Initialize monitoring for bundled backend (already started in Rust)
// This sets up port detection and health checks
const shouldMonitorBackend = setupComplete && !isFirstLaunch && connectionMode === 'saas';
useBackendInitializer(shouldMonitorBackend);
// Show setup wizard on first launch
if (isFirstLaunch && !setupComplete) {
@@ -51,23 +51,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
}}
>
<SetupWizard
onComplete={async () => {
// Wait for backend to become healthy before reloading
// This prevents reloading mid-startup which would interrupt the backend
const maxWaitTime = 60000; // 60 seconds max
const checkInterval = 1000; // Check every second
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (tauriBackendService.isBackendHealthy()) {
window.location.reload();
return;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
// If we timeout, reload anyway
console.warn('[AppProviders] Backend health check timeout, reloading anyway...');
onComplete={() => {
window.location.reload();
}}
/>
@@ -61,7 +61,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
}
await connectionModeService.switchToSaaS(serverConfig.url);
await tauriBackendService.startBackend();
tauriBackendService.startBackend().catch(console.error);
onComplete();
} catch (err) {
console.error('SaaS login failed:', err);
+51
View File
@@ -0,0 +1,51 @@
Stirling PDF User License
Copyright (c) 2025 Stirling PDF Inc.
License Scope & Usage Rights
Production use of the Stirling PDF Software is only permitted with a valid Stirling PDF User License.
For purposes of this license, “the Software” refers to the Stirling PDF application and any associated documentation files
provided by Stirling PDF Inc. You or your organization may not use the Software in production, at scale, or for business-critical
processes unless you have agreed to, and remain in compliance with, the Stirling PDF Subscription Terms of Service
(https://www.stirlingpdf.com/terms) or another valid agreement with Stirling PDF, and hold an active User License subscription
covering the appropriate number of licensed users.
Trial and Minimal Use
You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that:
* Use is limited to the capabilities and restrictions defined by the Software itself;
* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts.
Continued use beyond this scope requires a valid Stirling PDF User License.
Modifications and Derivative Works
You may modify the Software only for development or internal testing purposes. Any such modifications or derivative works:
* May not be deployed in production environments without a valid User License;
* May not be distributed or sublicensed;
* Remain the intellectual property of Stirling PDF and/or its licensors;
* May only be used, copied, or exploited in accordance with the terms of a valid Stirling PDF User License subscription.
Prohibited Actions
Unless explicitly permitted by a paid license or separate agreement, you may not:
* Use the Software in production environments;
* Copy, merge, distribute, sublicense, or sell the Software;
* Remove or alter any licensing or copyright notices;
* Circumvent access restrictions or licensing requirements.
Third-Party Components
The Stirling PDF Software may include components subject to separate open source licenses. Such components remain governed by
their original license terms as provided by their respective owners.
Disclaimer
THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -13,9 +13,12 @@ import { InfoBanner } from '@app/components/shared/InfoBanner';
import { useLicenseAlert } from '@app/hooks/useLicenseAlert';
import { isSupabaseConfigured } from '@app/services/supabaseClient';
import { getPreferredCurrency, setCachedCurrency } from '@app/utils/currencyDetection';
import { useLoginRequired } from '@app/hooks/useLoginRequired';
import LoginRequiredBanner from '@core/components/shared/config/LoginRequiredBanner';
const AdminPlanSection: React.FC = () => {
const { t, i18n } = useTranslation();
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
const { openCheckout } = useCheckout();
const { licenseInfo, refetchLicense } = useLicense();
const [currency, setCurrency] = useState<string>(() => {
@@ -39,6 +42,11 @@ const AdminPlanSection: React.FC = () => {
}, [error]);
const handleSaveLicense = async () => {
// Block save if login is disabled
if (!validateLoginEnabled()) {
return;
}
try {
setSavingLicense(true);
// Allow empty string to clear/remove license
@@ -86,6 +94,11 @@ const AdminPlanSection: React.FC = () => {
];
const handleManageClick = useCallback(async () => {
// Block access if login is disabled
if (!validateLoginEnabled()) {
return;
}
try {
// Only allow PRO or ENTERPRISE licenses to access billing portal
if (!licenseInfo?.licenseType || licenseInfo.licenseType === 'NORMAL') {
@@ -112,7 +125,7 @@ const AdminPlanSection: React.FC = () => {
body: error.message || 'Please try again or contact support.',
});
}
}, [licenseInfo, t]);
}, [licenseInfo, t, validateLoginEnabled]);
const handleCurrencyChange = useCallback((newCurrency: string) => {
setCurrency(newCurrency);
@@ -122,6 +135,11 @@ const AdminPlanSection: React.FC = () => {
const handleUpgradeClick = useCallback(
(planGroup: PlanTierGroup) => {
// Block access if login is disabled
if (!validateLoginEnabled()) {
return;
}
// Only allow upgrades for server and enterprise tiers
if (planGroup.tier === 'free') {
return;
@@ -151,7 +169,7 @@ const AdminPlanSection: React.FC = () => {
},
});
},
[openCheckout, currency, refetch, licenseInfo, t]
[openCheckout, currency, refetch, licenseInfo, t, validateLoginEnabled]
);
const shouldShowLicenseWarning = licenseAlert.active && licenseAlert.audience === 'admin';
@@ -200,7 +218,9 @@ const AdminPlanSection: React.FC = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{shouldShowLicenseWarning && (
<LoginRequiredBanner show={!loginEnabled} />
{shouldShowLicenseWarning && (
<InfoBanner
icon="warning-rounded"
tone="warning"
@@ -231,6 +251,7 @@ const AdminPlanSection: React.FC = () => {
currency={currency}
onCurrencyChange={handleCurrencyChange}
currencyOptions={currencyOptions}
loginEnabled={loginEnabled}
/>
<Divider />
@@ -288,11 +309,11 @@ const AdminPlanSection: React.FC = () => {
onChange={(e) => setLicenseKeyInput(e.target.value)}
placeholder={licenseInfo?.licenseKey || '00000000-0000-0000-0000-000000000000'}
type="password"
disabled={savingLicense}
disabled={!loginEnabled || savingLicense}
/>
<Group justify="flex-end">
<Button onClick={handleSaveLicense} loading={savingLicense} size="sm">
<Button onClick={handleSaveLicense} loading={savingLicense} size="sm" disabled={!loginEnabled}>
{t('admin.settings.save', 'Save Changes')}
</Button>
</Group>
@@ -35,7 +35,7 @@ const AuditEventsTable: React.FC<AuditEventsTableProps> = ({ loginEnabled = true
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters({
page: 0,
pageSize: 20,
});
}, loginEnabled);
useEffect(() => {
const fetchEvents = async () => {
@@ -23,7 +23,7 @@ const AuditExportSection: React.FC<AuditExportSectionProps> = ({ loginEnabled =
const [exporting, setExporting] = useState(false);
// Use shared filters hook
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters();
const { filters, eventTypes, users, handleFilterChange, handleClearFilters } = useAuditFilters({}, loginEnabled);
const handleExport = async () => {
if (!loginEnabled) return;
@@ -15,6 +15,7 @@ interface AvailablePlansSectionProps {
currency?: string;
onCurrencyChange?: (currency: string) => void;
currencyOptions?: { value: string; label: string }[];
loginEnabled?: boolean;
}
const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
@@ -25,6 +26,7 @@ const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
currency,
onCurrencyChange,
currencyOptions,
loginEnabled = true,
}) => {
const { t } = useTranslation();
const [showComparison, setShowComparison] = useState(false);
@@ -91,6 +93,7 @@ const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
clearable={false}
w={300}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
disabled={!loginEnabled}
/>
)}
</Group>
@@ -113,6 +116,7 @@ const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
currentTier={currentTier}
onUpgradeClick={onUpgradeClick}
onManageClick={onManageClick}
loginEnabled={loginEnabled}
/>
))}
</div>
@@ -15,9 +15,10 @@ interface PlanCardProps {
currentTier?: 'free' | 'server' | 'enterprise' | null;
onUpgradeClick: (planGroup: PlanTierGroup) => void;
onManageClick?: () => void;
loginEnabled?: boolean;
}
const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngrade, currentLicenseInfo, currentTier, onUpgradeClick, onManageClick }) => {
const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngrade, currentLicenseInfo, currentTier, onUpgradeClick, onManageClick, loginEnabled = true }) => {
const { t } = useTranslation();
// Render Free plan
@@ -174,7 +175,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngra
variant={isCurrentTier ? 'filled' : isDowngrade ? 'filled' : isEnterpriseBlockedForFree ? 'light' : 'filled'}
fullWidth
onClick={() => isCurrentTier && onManageClick ? onManageClick() : onUpgradeClick(planGroup)}
disabled={isDowngrade || isEnterpriseBlockedForFree}
disabled={!loginEnabled || isDowngrade || isEnterpriseBlockedForFree}
>
{isCurrentTier
? t('plan.manage', 'Manage')