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
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,18 @@ export const ConnectionErrorBanner: React.FC<ConnectionErrorBannerProps> = ({ me

useEffect(() => {
setDismissed(false);
}, [failure?.state, failure?.lastError]);
}, [failure?.state, failure?.lastError, failure?.errorKind]);

if (dismissed || !failure) return null;

const isNetworkFailure = failure.state === ConnectionState.NETWORK_ERROR;
const isOAuthCallbackFailure = !isNetworkFailure && failure.errorKind === 'oauth-callback';
const defaultMessage = isNetworkFailure
? 'Can\'t reach the server — check your connection and retry.'
: 'Your session has expired — please sign in again.';
const action = isNetworkFailure ? 'Retry' : 'Sign in';
: isOAuthCallbackFailure
? `Sign-in didn't complete: ${failure.lastError}`
: 'Your session has expired — please sign in again.';
const action = isNetworkFailure ? 'Retry' : isOAuthCallbackFailure ? 'Try again' : 'Sign in';
const onAction = isNetworkFailure
? onRetry ?? (() => ConnectionManager.getInstance().reconnect())
: onSignIn ?? (() => ConnectionManager.getInstance().startOAuth(false));
Expand Down
28 changes: 27 additions & 1 deletion apps/shell-ui/src/connection/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export class ConnectionManager implements IConnectionManager {
state: ConnectionState.CONNECTED,
lastConnected: new Date(),
lastError: undefined,
errorKind: undefined,
retryAttempt: 0,
progressMessage: undefined,
});
Expand All @@ -277,7 +278,7 @@ export class ConnectionManager implements IConnectionManager {
this.clearServicesCache();
// Don't overwrite AUTH_FAILED state
if (this.connectionStatus.state !== ConnectionState.AUTH_FAILED) {
this.updateConnectionStatus({ state: ConnectionState.CONNECTING });
this.updateConnectionStatus({ state: ConnectionState.CONNECTING, errorKind: undefined });
}
this.emit('shell:disconnected', { reason: reason ?? 'unknown', hasError: hasError ?? false });
},
Expand Down Expand Up @@ -453,6 +454,26 @@ export class ConnectionManager implements IConnectionManager {

const params = new URLSearchParams(window.location.search);
const code = params.get('code');
// Only honor `auth_error`: it is set exclusively by our own OAuth
// callback (the registered redirect_uri is the server callback, which
// wraps every Zitadel failure as `auth_error` before redirecting here).
// The generic OAuth `error`/`error_description` params never legitimately
// reach the app this way, so reading them would let any unrelated app
// deep-link (`/app?error=…`) hijack bootstrap into a false sign-in banner.
const errorDescription = params.get('auth_error');

if (errorDescription) {
window.history.replaceState({}, '', window.location.pathname);
this.updateConnectionStatus({
state: ConnectionState.AUTH_FAILED,
lastError: errorDescription,
progressMessage: undefined,
errorKind: 'oauth-callback',
});
this.clearPendingAppId();
return null;
}

const sessionAppId = this.getSessionAppId();

// ── OAuth callback — exchange authorization code for a session ────
Expand Down Expand Up @@ -644,6 +665,7 @@ export class ConnectionManager implements IConnectionManager {
this.updateConnectionStatus({
state: ConnectionState.CONNECTING,
lastError: undefined,
errorKind: undefined,
});

try {
Expand Down Expand Up @@ -678,6 +700,7 @@ export class ConnectionManager implements IConnectionManager {
state: this.getConnectionFailureState(error),
lastError: errorMessage,
progressMessage: undefined,
errorKind: undefined,
});

this.emit('shell:error', { error });
Expand All @@ -702,6 +725,7 @@ export class ConnectionManager implements IConnectionManager {
this.updateConnectionStatus({
state: ConnectionState.DISCONNECTED,
progressMessage: undefined,
errorKind: undefined,
});
}

Expand Down Expand Up @@ -749,6 +773,7 @@ export class ConnectionManager implements IConnectionManager {
? 'Your session has expired — please sign in again.'
: error instanceof Error ? error.message : String(error),
progressMessage: undefined,
errorKind: state === ConnectionState.AUTH_FAILED ? 'session' : undefined,
});
return !isNetworkFailure;
}
Expand Down Expand Up @@ -1118,6 +1143,7 @@ export class ConnectionManager implements IConnectionManager {
this.connectionStatus.lastFailure = {
state: updates.state,
lastError: updates.lastError ?? this.connectionStatus.lastError,
errorKind: updates.errorKind ?? this.connectionStatus.errorKind,
};
} else if (
updates.state === ConnectionState.CONNECTED &&
Expand Down
8 changes: 8 additions & 0 deletions packages/shared-ui/src/types/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ export interface ConnectionStatus {
/** Last error message (cleared on successful connect). */
lastError?: string;

/**
* Discriminates AUTH_FAILED origins so recovery UI can phrase the message:
* 'oauth-callback' — the IdP returned an error on the OAuth callback;
* 'session' — an existing session was rejected or expired.
*/
errorKind?: 'oauth-callback' | 'session';

/**
* Most recent unrecovered failure, latched across later transitions.
* Persist-mode reconnect attempts report CONNECTING and a post-failure
Expand All @@ -110,6 +117,7 @@ export interface ConnectionStatus {
lastFailure?: {
state: ConnectionState.NETWORK_ERROR | ConnectionState.AUTH_FAILED;
lastError?: string;
errorKind?: 'oauth-callback' | 'session';
};

/** True if we have necessary credentials/config to attempt connection. */
Expand Down
Loading