Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
487 changes: 483 additions & 4 deletions desktop/src-tauri/src/lib.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion desktop/src-tauri/src/observation_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ mod tests {
assert!(result.is_err(), "expected error for {name}");
continue;
}
let compiled = result.expect(&name);
let compiled = result.expect(name);
let fragments = fixture["expectedSqlFragmentsByDialect"]["desktop"]
.as_array()
.or_else(|| fixture["expectedSqlFragments"].as_array());
Expand Down
35 changes: 35 additions & 0 deletions desktop/src/hooks/useProfileAutoSynkAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useCallback, useEffect, useState } from 'react';
import {
selectAuthSessionForActiveProfile,
useCustodianStore,
} from '../store/useCustodianStore';

/**
* Silent sign-in on profile change (refresh token or keyring password), matching Sync page behavior.
* Uses {@link recoverActiveProfileAuth} so expired stored tokens are renewed, not reused blindly.
*/
export function useProfileAutoSynkAuth(activeProfileId: string | undefined) {
const authSession = useCustodianStore(selectAuthSessionForActiveProfile);
const recoverActiveProfileAuth = useCustodianStore(
s => s.recoverActiveProfileAuth,
);
const [authBlocked, setAuthBlocked] = useState(false);

const refreshAuth = useCallback(async (): Promise<boolean> => {
const ok = await recoverActiveProfileAuth();
setAuthBlocked(
!ok && !selectAuthSessionForActiveProfile(useCustodianStore.getState()),
);
return ok;
}, [recoverActiveProfileAuth]);

useEffect(() => {
void refreshAuth();
}, [activeProfileId, refreshAuth]);

const ensureAuth = useCallback(async (): Promise<boolean> => {
return refreshAuth();
}, [refreshAuth]);

return { authSession, authBlocked, ensureAuth, refreshAuth };
}
12 changes: 12 additions & 0 deletions desktop/src/lib/tauriClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
ActiveBundleFormEntry,
AppBundleState,
DownloadAndApplyAppBundleResult,
PushDevMirrorAppBundleResult,
CreateObservationSqliteIndexesResult,
CustomAppDevMirrorResult,
BundleFormSpec,
Expand Down Expand Up @@ -230,6 +231,17 @@ export const tauriClient = {
hash: args.hash,
},
),
/** Zip dev mirror, push to Synkronus, and activate the new bundle version. */
pushDevMirrorAppBundle: (args: {
baseUrl: string;
bearerToken: string;
xOdeVersion: string;
}) =>
invokeSafe<PushDevMirrorAppBundleResult>('push_dev_mirror_app_bundle', {
baseUrl: args.baseUrl,
bearerToken: args.bearerToken,
xOdeVersion: args.xOdeVersion,
}),
listActiveBundleForms: () =>
invokeSafe<ActiveBundleFormEntry[]>('list_active_bundle_forms'),
readBundleFormSpec: (formType: string) =>
Expand Down
74 changes: 74 additions & 0 deletions desktop/src/lib/workbenchBundleAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { isTauri } from '@tauri-apps/api/core';
import { message } from '@tauri-apps/plugin-dialog';
import {
selectAuthSessionForActiveProfile,
useCustodianStore,
} from '../store/useCustodianStore';
import type { ServerProfile } from '../types/domain';

async function alertAuthRequired(body: string): Promise<void> {
if (isTauri()) {
await message(body, {
title: 'Authentication required',
kind: 'warning',
});
} else {
window.alert(body);
}
}

export type EnsureWorkbenchBundleAuthOptions = {
active: ServerProfile | null | undefined;
baseUrl: string;
onAuthRequired: (body: string) => void | Promise<void>;
messages?: {
noProfile?: string;
noServerUrl?: string;
authFailed?: string;
};
};

/**
* Ensures the active profile has a server URL and bearer token for Synkronus bundle ops.
* Returns the token when ready, or null after prompting the user.
*/
export async function ensureWorkbenchBundleAuth(
options: EnsureWorkbenchBundleAuthOptions,
): Promise<string | null> {
const { active, baseUrl, onAuthRequired, messages = {} } = options;

if (!active) {
await onAuthRequired(
messages.noProfile ??
'Select a profile in Profiles before app bundle operations.',
);
return null;
}
if (!baseUrl) {
await onAuthRequired(
messages.noServerUrl ??
'Set a server URL for this profile in Profiles before app bundle operations.',
);
return null;
}
const ok = await useCustodianStore.getState().recoverActiveProfileAuth();
const token = selectAuthSessionForActiveProfile(
useCustodianStore.getState(),
)?.token;
if (!ok || !token) {
await onAuthRequired(
messages.authFailed ??
'Could not sign in automatically. Open Profiles to authenticate (save a password or sign in manually).',
);
return null;
}
return token;
}

export async function promptNavigateToProfilesForBundleAuth(
body: string,
navigate: (path: string) => void,
): Promise<void> {
await alertAuthRequired(body);
navigate('/data/profiles');
}
30 changes: 4 additions & 26 deletions desktop/src/pages/SyncPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { ForcePushMissingAttachmentsDialog } from '../components/ForcePushMissingAttachmentsDialog';
import { useProfileAutoSynkAuth } from '../hooks/useProfileAutoSynkAuth';
import { tauriClient } from '../lib/tauriClient';
import { tryAutoSynkAuth } from '../lib/autoSynkAuth';
import { confirmDestructiveAction } from '../lib/destructivePolicy';
import {
auditPendingPushMissingAttachments,
Expand All @@ -12,7 +12,6 @@ import { pushConfirmMessage } from '../lib/syncUiCopy';
import { useSynkServerStatus } from '../hooks/useSynkServerStatus';
import {
selectActiveProfileState,
selectAuthSessionForActiveProfile,
selectBundleActivity,
selectPausedSyncJob,
selectSyncActivity,
Expand All @@ -28,7 +27,9 @@ function formatDate(value?: string | null) {

export function SyncPage() {
const activeProfile = useCustodianStore(selectActiveProfileState);
const authSession = useCustodianStore(selectAuthSessionForActiveProfile);
const { authSession, authBlocked, ensureAuth } = useProfileAutoSynkAuth(
activeProfile?.id,
);
const syncActivity = useCustodianStore(selectSyncActivity);
const syncPausedJob = useCustodianStore(selectPausedSyncJob);
const bundleActivity = useCustodianStore(selectBundleActivity);
Expand All @@ -54,7 +55,6 @@ export function SyncPage() {
profileLabel,
);

const [authBlocked, setAuthBlocked] = useState(false);
const [missingAttachmentIssues, setMissingAttachmentIssues] = useState<
MissingAttachmentIssue[] | null
>(null);
Expand All @@ -72,15 +72,6 @@ export function SyncPage() {
.catch(() => setIndexStatus(null));
}, []);

useEffect(() => {
void (async () => {
const ok = await tryAutoSynkAuth();
setAuthBlocked(
!ok && !selectAuthSessionForActiveProfile(useCustodianStore.getState()),
);
})();
}, [activeProfile?.id]);

useEffect(() => {
void loadHealth();
void refreshPausedSyncJob();
Expand All @@ -94,19 +85,6 @@ export function SyncPage() {
refreshIndexStatus();
}, [bundleActivity, refreshIndexStatus]);

async function ensureAuth(): Promise<boolean> {
if (authSession?.token) {
return true;
}
const ok = await tryAutoSynkAuth();
if (!ok) {
setAuthBlocked(true);
return false;
}
setAuthBlocked(false);
return true;
}

async function recreateObservationIndexes() {
await ensureBundleApplyEventPipeline();
try {
Expand Down
64 changes: 22 additions & 42 deletions desktop/src/pages/WorkbenchBundlesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { isTauri } from '@tauri-apps/api/core';
import { confirm, message } from '@tauri-apps/plugin-dialog';
import { confirm } from '@tauri-apps/plugin-dialog';
import {
Configuration,
DefaultApi,
type AppBundleManifest,
} from '../generated/synkronus-client';
import { useProfileAutoSynkAuth } from '../hooks/useProfileAutoSynkAuth';
import { appBundleUpdateAvailable } from '../lib/appBundleStatus';
import {
bundleBannerLineFromProgress,
Expand All @@ -17,6 +17,10 @@ import { readBundleCache, writeBundleCache } from '../lib/bundleCacheMeta';
import { isUnauthorizedSynkError } from '../lib/synkAuthErrors';
import { SYNKRONUS_CLIENT_VERSION } from '../lib/synkConstants';
import { tauriClient } from '../lib/tauriClient';
import {
ensureWorkbenchBundleAuth,
promptNavigateToProfilesForBundleAuth,
} from '../lib/workbenchBundleAuth';
import {
selectActiveProfileState,
selectAuthSessionForActiveProfile,
Expand Down Expand Up @@ -55,17 +59,6 @@ function bundleButtonLabel(
}
}

async function alertAuthRequired(body: string): Promise<void> {
if (isTauri()) {
await message(body, {
title: 'Authentication required',
kind: 'warning',
});
} else {
window.alert(body);
}
}

async function fetchServerBundleInfoFromApi(
baseUrl: string,
token: string,
Expand Down Expand Up @@ -103,9 +96,7 @@ async function fetchServerBundleInfoFromApi(
export function WorkbenchBundlesPage() {
const navigate = useNavigate();
const active = useCustodianStore(selectActiveProfileState);
const ensureActiveProfileAuth = useCustodianStore(
s => s.ensureActiveProfileAuth,
);
useProfileAutoSynkAuth(active?.id);
const recoverActiveProfileAuth = useCustodianStore(
s => s.recoverActiveProfileAuth,
);
Expand Down Expand Up @@ -175,39 +166,28 @@ export function WorkbenchBundlesPage() {

const promptNavigateToProfiles = useCallback(
async (body: string) => {
await alertAuthRequired(body);
navigate('/data/profiles');
await promptNavigateToProfilesForBundleAuth(body, navigate);
},
[navigate],
);

const ensureAuthForBundleOps = useCallback(async (): Promise<
string | null
> => {
if (!active) {
await promptNavigateToProfiles(
'Select a profile in Profiles before checking for app bundle updates.',
);
return null;
}
if (!baseUrl) {
await promptNavigateToProfiles(
'Set a server URL for this profile in Profiles before checking for app bundle updates.',
);
return null;
}
const ok = await ensureActiveProfileAuth();
const token = selectAuthSessionForActiveProfile(
useCustodianStore.getState(),
)?.token;
if (!ok || !token) {
await promptNavigateToProfiles(
'Could not sign in automatically. Open Profiles to authenticate (save a password or sign in manually).',
);
return null;
}
return token;
}, [active, baseUrl, ensureActiveProfileAuth, promptNavigateToProfiles]);
return ensureWorkbenchBundleAuth({
active,
baseUrl,
onAuthRequired: promptNavigateToProfiles,
messages: {
noProfile:
'Select a profile in Profiles before checking for app bundle updates.',
noServerUrl:
'Set a server URL for this profile in Profiles before checking for app bundle updates.',
authFailed:
'Could not sign in automatically. Open Profiles to authenticate (save a password or sign in manually).',
},
});
}, [active, baseUrl, promptNavigateToProfiles]);

const applyDownloadedBundle = useCallback(
async (m: AppBundleManifest, token: string, skipConfirm: boolean) => {
Expand Down
Loading
Loading