fix: case-insensitive usernames and federated space ownership detection

Enforce lowercase usernames at registration/login, migrate existing
usernames, and fix ownership checks for federated spaces by resolving
user identity per-instance with getMyUserIdForOrigin.
This commit is contained in:
Jannis Braun
2026-03-11 18:09:45 +01:00
parent 77ed2a70c4
commit 373d685a55
5 changed files with 50 additions and 19 deletions
@@ -71,9 +71,9 @@ export function RegisterPage() {
return;
}
if (!/^[a-zA-Z0-9_]+$/.test(trimmed)) {
if (!/^[a-z0-9_]+$/.test(trimmed)) {
setUsernameStatus('invalid');
setUsernameStatusMessage('Username can only contain letters, numbers, and underscores');
setUsernameStatusMessage('Username can only contain lowercase letters, numbers, and underscores');
return;
}
@@ -141,8 +141,8 @@ export function RegisterPage() {
setError('Username must be between 3 and 32 characters');
return;
}
if (!/^[a-zA-Z0-9_]+$/.test(trimmed)) {
setError('Username can only contain letters, numbers, and underscores');
if (!/^[a-z0-9_]+$/.test(trimmed)) {
setError('Username can only contain lowercase letters, numbers, and underscores');
return;
}
if (usernameStatus === 'taken' || usernameStatus === 'invalid') {
@@ -250,7 +250,7 @@ export function RegisterPage() {
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
onChange={(e) => setUsername(e.target.value.toLowerCase())}
className="w-full px-3 py-2.5 bg-surface-input border-none rounded text-txt-primary outline-none focus:ring-2 focus:ring-accent-primary transition-all"
autoFocus
autoComplete="username"
@@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import ReactDOM from 'react-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import { useSpaceStore } from '../../stores/spaceStore';
import { useSpaceStore, getMyUserIdForOrigin } from '../../stores/spaceStore';
import { useChatStore } from '../../stores/chatStore';
import { useUIStore } from '../../stores/uiStore';
import { useInstanceStore } from '../../stores/instanceStore';
@@ -167,7 +167,6 @@ function InstanceDivider({ label, disconnected }: { label: string; disconnected:
function SpaceContextMenu({ spaceId, x, y, onClose }: { spaceId: string; x: number; y: number; onClose: () => void }) {
const menuRef = useRef<HTMLDivElement>(null);
const space = useSpaceStore((s) => s.spaces.find(sp => sp.id === spaceId));
const currentUserId = useAuthStore((s) => s.user?.id);
const leaveSpace = useSpaceStore((s) => s.leaveSpace);
const generateInvite = useSpaceStore((s) => s.generateInvite);
const currentSpaceId = useSpaceStore((s) => s.currentSpaceId);
@@ -177,7 +176,7 @@ function SpaceContextMenu({ spaceId, x, y, onClose }: { spaceId: string; x: numb
const navigate = useNavigate();
const [showTransferModal, setShowTransferModal] = useState(false);
const isOwner = space?.ownerId === currentUserId;
const isOwner = space?.ownerId === getMyUserIdForOrigin((space as any)?._instanceOrigin ?? '');
// Close on click-outside and scroll
useEffect(() => {
@@ -6,7 +6,7 @@ import { useAuthStore } from '../../../stores/authStore';
import { useUIStore } from '../../../stores/uiStore';
import { useNavigate } from 'react-router-dom';
import { api } from '../../../api/client';
import { getApiForOrigin } from '../../../stores/spaceStore';
import { getApiForOrigin, getMyUserIdForOrigin } from '../../../stores/spaceStore';
import { hasPermissionBit, PermissionBits } from '../../../utils/permissions';
interface OverviewPanelProps {
@@ -23,7 +23,7 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
const navigate = useNavigate();
const space = spaces.find((s) => s.id === spaceId);
const isOwner = space?.ownerId === currentUser?.id;
const isOwner = space?.ownerId === getMyUserIdForOrigin((space as any)?._instanceOrigin ?? '');
const myPerms = spacePermissions.get(spaceId);
const canManageSpace = hasPermissionBit(myPerms, PermissionBits.MANAGE_SPACE);