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
+62 -33
View File
@@ -163,54 +163,80 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { 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); const { valid } = await api.users.verifyPassword(password);
if (!valid) { if (!valid) {
throw new Error('Incorrect password'); throw new Error('Incorrect password');
} }
// Step 2: Try register on remote, then fall back to login // Step 2: Compute the user's true home identity
const homeInstance = window.location.host; // 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); const tempClient = createApiClient(origin, () => null);
let response: AuthResponse | null = null; let response: AuthResponse | null = null;
const finalUsername = `${currentUser.username}@${homeInstance}`; let finalUsername: string;
// 2a: Attempt registration with namespaced username if (targetIsHome) {
try { // Target IS the user's home instance — they already have a native account.
response = await tempClient.auth.register({ // Just login with bare username, no registration or homeInstance params.
username: finalUsername, finalUsername = bareUsername;
password,
displayName: displayName || currentUser.displayName || undefined,
homeInstance,
homeUserId: currentUser.id,
});
} catch (err) {
const message = (err as Error).message;
if (message.includes('already taken') || message.includes('409') ||
message.includes('Registration is currently closed') || message.includes('403')) {
// Already registered or registration closed — fall through to login
} else {
throw err;
}
}
// 2b: If registration didn't work, try login
if (!response) {
try { try {
response = await tempClient.auth.login({ response = await tempClient.auth.login({
username: finalUsername, username: bareUsername,
password, password,
}); });
} catch { } catch {
// Namespaced login failed — try legacy plain username as fallback 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 {
response = await tempClient.auth.register({
username: finalUsername,
password,
displayName: displayName || currentUser.displayName || undefined,
homeInstance: trueHomeHost,
homeUserId: trueHomeUserId,
});
} catch (err) {
const message = (err as Error).message;
if (message.includes('already taken') || message.includes('409') ||
message.includes('Registration is currently closed') || message.includes('403')) {
// Already registered or registration closed — fall through to login
} else {
throw err;
}
}
// 2b: If registration didn't work, try login
if (!response) {
try { try {
response = await tempClient.auth.login({ response = await tempClient.auth.login({
username: currentUser.username, username: finalUsername,
password, password,
}); });
} catch { } catch {
throw new DifferentPasswordError(currentUser.username); // Namespaced login failed — try legacy plain username as fallback
try {
response = await tempClient.auth.login({
username: bareUsername,
password,
});
} catch {
throw new DifferentPasswordError(bareUsername);
}
} }
} }
} }
@@ -242,10 +268,13 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
// Open WebSocket connection to the remote instance // Open WebSocket connection to the remote instance
connectInstance(origin, response.token); connectInstance(origin, response.token);
// Sync full home profile to new remote (fire-and-forget) // Sync home profile to new remote (fire-and-forget).
syncProfileToRemote(instance).catch((err) => { // Skip when target is home — home is the source of truth for profile data.
console.warn(`[ProfileSync] Initial sync to ${origin} failed:`, err); if (!targetIsHome) {
}); syncProfileToRemote(instance).catch((err) => {
console.warn(`[ProfileSync] Initial sync to ${origin} failed:`, err);
});
}
// Sync instance list to all instances (fire-and-forget) // Sync instance list to all instances (fire-and-forget)
get().syncInstanceList().catch(() => {}); get().syncInstanceList().catch(() => {});