fix: resolve connectToRemote identity for federated users

Federated users (e.g. youruser@nova browsing orbit) could not add
instances via Settings → Connections because connectToRemote assumed
window.location.host was the home instance, producing a double-@ username
like youruser@nova.ddns.net@orbit.ddns.net that failed server
validation. Now derives trueHomeHost/bareUsername/trueHomeUserId from the
user's actual homeInstance fields and branches the auth flow: login-only
with bare username when targeting home, register with correct namespacing
for third-party remotes. Also skips profile sync to home instance since
it's the source of truth.
This commit is contained in:
Jannis Braun
2026-03-11 02:27:21 +01:00
parent 36d84c5974
commit 10be7a44f8
+38 -9
View File
@@ -163,18 +163,43 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
set({ isLoading: true, error: null });
try {
// Step 1: Verify password against HOME instance
// Step 1: Verify password against the instance we're currently browsing
const { valid } = await api.users.verifyPassword(password);
if (!valid) {
throw new Error('Incorrect password');
}
// Step 2: Try register on remote, then fall back to login
const homeInstance = window.location.host;
// Step 2: Compute the user's true home identity
// If we're a federated user (e.g. youruser@nova browsing orbit),
// homeInstance points to the real home, not window.location.host.
const trueHomeHost = currentUser.homeInstance ?? window.location.host;
const bareUsername = currentUser.username.includes('@')
? currentUser.username.split('@')[0]!
: currentUser.username;
const trueHomeUserId = currentUser.homeUserId ?? currentUser.id;
const targetHost = new URL(origin).host;
const targetIsHome = targetHost === trueHomeHost;
const tempClient = createApiClient(origin, () => null);
let response: AuthResponse | null = null;
const finalUsername = `${currentUser.username}@${homeInstance}`;
let finalUsername: string;
if (targetIsHome) {
// Target IS the user's home instance — they already have a native account.
// Just login with bare username, no registration or homeInstance params.
finalUsername = bareUsername;
try {
response = await tempClient.auth.login({
username: bareUsername,
password,
});
} catch {
throw new DifferentPasswordError(bareUsername);
}
} else {
// Target is a remote/third-party instance — register as user@homeHost
finalUsername = `${bareUsername}@${trueHomeHost}`;
// 2a: Attempt registration with namespaced username
try {
@@ -182,8 +207,8 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
username: finalUsername,
password,
displayName: displayName || currentUser.displayName || undefined,
homeInstance,
homeUserId: currentUser.id,
homeInstance: trueHomeHost,
homeUserId: trueHomeUserId,
});
} catch (err) {
const message = (err as Error).message;
@@ -206,11 +231,12 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
// Namespaced login failed — try legacy plain username as fallback
try {
response = await tempClient.auth.login({
username: currentUser.username,
username: bareUsername,
password,
});
} catch {
throw new DifferentPasswordError(currentUser.username);
throw new DifferentPasswordError(bareUsername);
}
}
}
}
@@ -242,10 +268,13 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
// Open WebSocket connection to the remote instance
connectInstance(origin, response.token);
// Sync full home profile to new remote (fire-and-forget)
// Sync home profile to new remote (fire-and-forget).
// Skip when target is home — home is the source of truth for profile data.
if (!targetIsHome) {
syncProfileToRemote(instance).catch((err) => {
console.warn(`[ProfileSync] Initial sync to ${origin} failed:`, err);
});
}
// Sync instance list to all instances (fire-and-forget)
get().syncInstanceList().catch(() => {});