mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-15 04:18:20 +00:00
fix(invitation): keep login return target on join route
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
ff69136ddb
commit
f12019cf14
5 changed files with 51 additions and 4 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue