fix(invitation): keep login return target on join route

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-10-12 12:32:12 +08:00
parent ff69136ddb
commit f12019cf14
5 changed files with 51 additions and 4 deletions

View file

@ -4,6 +4,7 @@ import com.iflytek.skillhub.domain.audit.AuditLogService;
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
import com.iflytek.skillhub.domain.user.UserAccount;
import com.iflytek.skillhub.domain.user.UserAccountRepository;
import com.iflytek.skillhub.domain.user.UserStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@ -136,6 +137,30 @@ class NamespaceInvitationServiceTest {
.isInstanceOf(DomainNotFoundException.class);
}
@Test
void nonActiveUserCannotPreviewOrAcceptAndDoesNotConsumeInvitation() {
NamespaceInvitationService.Created created = service.create("team", "owner");
ArgumentCaptor<NamespaceInvitation> saved = ArgumentCaptor.forClass(NamespaceInvitation.class);
verify(invitations).save(saved.capture());
NamespaceInvitation invitation = saved.getValue();
when(invitations.findByTokenHash(invitation.getTokenHash())).thenReturn(Optional.of(invitation));
when(invitations.findNamespaceIdByTokenHash(invitation.getTokenHash())).thenReturn(Optional.of(7L));
when(invitations.findByNamespaceId(7L)).thenReturn(Optional.of(invitation));
UserAccount pending = mock(UserAccount.class);
when(pending.getStatus()).thenReturn(UserStatus.PENDING);
when(users.findById("guest")).thenReturn(Optional.of(pending));
when(users.findLockedById("guest")).thenReturn(Optional.of(pending));
clearInvocations(invitations, members, audit);
assertThatThrownBy(() -> service.preview(created.token(), "guest"))
.isInstanceOf(DomainNotFoundException.class);
assertThatThrownBy(() -> service.accept(created.token(), "guest"))
.isInstanceOf(DomainNotFoundException.class);
verify(members, never()).save(any());
verify(invitations, never()).save(any());
assertThat(invitation.getConsumedAt()).isNull();
}
@Test
void existingMemberCanOpenCurrentConsumedInvitationWithoutReconsuming() {
NamespaceInvitationService.Created created = service.create("team", "owner");

View file

@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { isRedirect } from '@tanstack/react-router'
// The router module captures window.location.search at module load time.
// We test the exported ORIGINAL_URL_SEARCH constant and the buildReturnTo
@ -51,4 +52,23 @@ describe('router', () => {
const childPaths = children.map((route) => route.fullPath ?? route.path)
expect(childPaths).toContain('/device')
})
it('returns unauthenticated invitation visitors to /join even with a query and deployment base path', async () => {
const children = (router.routeTree.children ?? []) as Array<{
fullPath?: string
options?: { beforeLoad?: (context: { location: { pathname: string; searchStr: string; hash: string } }) => Promise<unknown> }
}>
const join = children.find((route) => route.fullPath === '/join')
expect(join?.options?.beforeLoad).toBeDefined()
await expect(join!.options!.beforeLoad!({
location: { pathname: '/skillhub/join', searchStr: '?x=1', hash: '#invitation-token' },
})).rejects.toSatisfy((error: unknown) => {
expect(isRedirect(error)).toBe(true)
if (!isRedirect(error)) return false
expect(error.options.to).toBe('/login')
expect(error.options.search).toEqual({ returnTo: '/join' })
return true
})
})
})

View file

@ -236,6 +236,7 @@ const rootRoute = createRootRoute({
})
const requireAuth = createRequireAuth(getCurrentUser)
const requireJoinAuth = createRequireAuth(getCurrentUser, '/join')
const redirectAuthenticated = createRedirectAuthenticated(getCurrentUser)
const landingRoute = createRoute({
@ -329,7 +330,7 @@ const namespaceRoute = createRoute({
const joinRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'join',
beforeLoad: requireAuth,
beforeLoad: requireJoinAuth,
component: JoinPage,
})

View file

@ -53,13 +53,13 @@ export function createRedirectAuthenticated(getCurrentUser: () => Promise<unknow
}
}
export function createRequireAuth(getCurrentUser: () => Promise<unknown>) {
export function createRequireAuth(getCurrentUser: () => Promise<unknown>, returnTo?: string) {
return async function requireAuth({ location }: { location: RouteLocationLike }) {
const user = await getCurrentUser()
if (!user) {
throw redirect({
to: '/login',
search: { returnTo: buildReturnTo(location) },
search: { returnTo: returnTo ?? buildReturnTo(location) },
})
}
return { user }

View file

@ -14,7 +14,8 @@ describe('namespace invitation URL handoff', () => {
window.history.replaceState(null, '', `/join?from=mail#${token}`)
captureInvitationFragment()
expect(storedInvitationToken()).toBe(token)
expect(window.location.href).toContain('/join?from=mail')
expect(window.location.pathname).toBe('/join')
expect(window.location.search).toBe('?from=mail')
expect(window.location.hash).toBe('')
})