@shipfox/client-auth 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/components/email-code-verification.d.ts.map +1 -1
- package/dist/components/email-code-verification.js +1 -1
- package/dist/components/email-code-verification.js.map +1 -1
- package/dist/components/workspace-switcher.stories.d.ts +1 -1
- package/dist/continuation.d.ts +1 -1
- package/dist/continuation.d.ts.map +1 -1
- package/dist/continuation.js +1 -1
- package/dist/continuation.js.map +1 -1
- package/dist/hooks/api/auth-mapper.d.ts +21 -0
- package/dist/hooks/api/auth-mapper.d.ts.map +1 -0
- package/dist/hooks/api/auth-mapper.js +25 -0
- package/dist/hooks/api/auth-mapper.js.map +1 -0
- package/dist/hooks/api/login-auth.d.ts +3 -25
- package/dist/hooks/api/login-auth.d.ts.map +1 -1
- package/dist/hooks/api/login-auth.js +5 -2
- package/dist/hooks/api/login-auth.js.map +1 -1
- package/dist/hooks/api/password-reset-auth.d.ts +3 -25
- package/dist/hooks/api/password-reset-auth.d.ts.map +1 -1
- package/dist/hooks/api/password-reset-auth.js +5 -2
- package/dist/hooks/api/password-reset-auth.js.map +1 -1
- package/dist/hooks/api/signup-auth.d.ts +1 -25
- package/dist/hooks/api/signup-auth.d.ts.map +1 -1
- package/dist/hooks/api/signup-auth.js +5 -2
- package/dist/hooks/api/signup-auth.js.map +1 -1
- package/dist/hooks/api/verify-email-auth.d.ts +1 -12
- package/dist/hooks/api/verify-email-auth.d.ts.map +1 -1
- package/dist/hooks/api/verify-email-auth.js +6 -3
- package/dist/hooks/api/verify-email-auth.js.map +1 -1
- package/dist/hooks/api/workspace-auth.d.ts +3 -17
- package/dist/hooks/api/workspace-auth.d.ts.map +1 -1
- package/dist/hooks/api/workspace-auth.js +5 -2
- package/dist/hooks/api/workspace-auth.js.map +1 -1
- package/dist/hooks/api/workspace-mapper.d.ts +8 -0
- package/dist/hooks/api/workspace-mapper.d.ts.map +1 -0
- package/dist/hooks/api/workspace-mapper.js +9 -0
- package/dist/hooks/api/workspace-mapper.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/invitation-context.d.ts +9 -14
- package/dist/pages/invitation-context.d.ts.map +1 -1
- package/dist/pages/invitation-context.js +4 -27
- package/dist/pages/invitation-context.js.map +1 -1
- package/dist/pages/login-page.js +2 -2
- package/dist/pages/login-page.js.map +1 -1
- package/dist/pages/logout-page.js +1 -1
- package/dist/pages/logout-page.js.map +1 -1
- package/dist/pages/password-reset-page.d.ts.map +1 -1
- package/dist/pages/password-reset-page.js +1 -4
- package/dist/pages/password-reset-page.js.map +1 -1
- package/dist/pages/signup-page.js +9 -9
- package/dist/pages/signup-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/components/email-code-verification.tsx +1 -1
- package/src/continuation.ts +1 -1
- package/src/hooks/api/auth-mapper.test.ts +77 -0
- package/src/hooks/api/auth-mapper.ts +39 -0
- package/src/hooks/api/login-auth.test.ts +1 -1
- package/src/hooks/api/login-auth.ts +8 -3
- package/src/hooks/api/password-reset-auth.test.ts +1 -1
- package/src/hooks/api/password-reset-auth.ts +15 -9
- package/src/hooks/api/signup-auth.ts +8 -3
- package/src/hooks/api/verify-email-auth.ts +17 -11
- package/src/hooks/api/workspace-auth.ts +8 -3
- package/src/hooks/api/workspace-mapper.test.ts +21 -0
- package/src/hooks/api/workspace-mapper.ts +11 -0
- package/src/index.ts +0 -1
- package/src/pages/invitation-context.ts +8 -31
- package/src/pages/login-page.test.tsx +1 -1
- package/src/pages/login-page.tsx +2 -2
- package/src/pages/logout-page.tsx +1 -1
- package/src/pages/password-reset-page.tsx +1 -4
- package/src/pages/signup-page.test.tsx +1 -1
- package/src/pages/signup-page.tsx +9 -9
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/auth-shell.d.ts +0 -13
- package/dist/components/auth-shell.d.ts.map +0 -1
- package/dist/components/auth-shell.js +0 -68
- package/dist/components/auth-shell.js.map +0 -1
- package/src/components/auth-shell.tsx +0 -68
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
"@swc/helpers": "^0.5.17",
|
|
37
37
|
"@tanstack/react-form": "^1.32.0",
|
|
38
38
|
"@shipfox/api-auth-dto": "7.1.0",
|
|
39
|
-
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
40
39
|
"@shipfox/client-api": "4.0.0",
|
|
41
|
-
"@shipfox/
|
|
40
|
+
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
41
|
+
"@shipfox/client-invitations": "5.0.0",
|
|
42
|
+
"@shipfox/client-shell": "5.0.0",
|
|
42
43
|
"@shipfox/client-ui": "4.0.0",
|
|
43
44
|
"@shipfox/react-ui": "0.3.5"
|
|
44
45
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
1
2
|
import {Button} from '@shipfox/react-ui/button';
|
|
2
3
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
3
4
|
import {FormField, FormFieldInput} from '@shipfox/react-ui/form-field';
|
|
4
5
|
import type {HeaderProps} from '@shipfox/react-ui/typography';
|
|
5
6
|
import type {FormEvent, ReactNode, Ref} from 'react';
|
|
6
7
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
7
|
-
import {AuthShell} from './auth-shell.js';
|
|
8
8
|
|
|
9
9
|
const EIGHT_DIGIT_CODE_RE = /^\d{8}$/u;
|
|
10
10
|
|
package/src/continuation.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {SignupResponseDto} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {toSignupResult} from './auth-mapper.js';
|
|
3
|
+
|
|
4
|
+
const baseUser: SignupResponseDto['user'] = {
|
|
5
|
+
id: '11111111-1111-4111-8111-111111111111',
|
|
6
|
+
email: 'signup@example.com',
|
|
7
|
+
name: null,
|
|
8
|
+
email_verified_at: null,
|
|
9
|
+
status: 'active',
|
|
10
|
+
created_at: '2026-04-27T00:00:00.000Z',
|
|
11
|
+
updated_at: '2026-04-27T00:00:00.000Z',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('toSignupResult', () => {
|
|
15
|
+
test('maps only the user when no challenge, membership, or accept error is present', () => {
|
|
16
|
+
const result = toSignupResult({user: baseUser});
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({user: {id: baseUser.id, email: baseUser.email}});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('maps the user name and email verification when present', () => {
|
|
22
|
+
const result = toSignupResult({
|
|
23
|
+
user: {...baseUser, name: 'Ada', email_verified_at: '2026-04-27T00:00:00.000Z'},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.user).toEqual({
|
|
27
|
+
id: baseUser.id,
|
|
28
|
+
email: baseUser.email,
|
|
29
|
+
name: 'Ada',
|
|
30
|
+
emailVerifiedAt: '2026-04-27T00:00:00.000Z',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('maps the email challenge into camelCase fields', () => {
|
|
35
|
+
const result = toSignupResult({
|
|
36
|
+
user: baseUser,
|
|
37
|
+
email_challenge: {
|
|
38
|
+
id: '22222222-2222-4222-8222-222222222222',
|
|
39
|
+
next_resend_available_at: '2026-04-27T00:01:00.000Z',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.emailChallenge).toEqual({
|
|
44
|
+
id: '22222222-2222-4222-8222-222222222222',
|
|
45
|
+
nextResendAvailableAt: '2026-04-27T00:01:00.000Z',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('maps the invitation membership into camelCase fields', () => {
|
|
50
|
+
const result = toSignupResult({
|
|
51
|
+
user: baseUser,
|
|
52
|
+
membership: {
|
|
53
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
54
|
+
user_id: baseUser.id,
|
|
55
|
+
workspace_id: '44444444-4444-4444-8444-444444444444',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.membership).toEqual({
|
|
60
|
+
id: '33333333-3333-4333-8333-333333333333',
|
|
61
|
+
userId: baseUser.id,
|
|
62
|
+
workspaceId: '44444444-4444-4444-8444-444444444444',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('maps the invitation accept error as-is', () => {
|
|
67
|
+
const result = toSignupResult({
|
|
68
|
+
user: baseUser,
|
|
69
|
+
accept_error: {code: 'invitation-expired', message: 'This invitation has expired.'},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.acceptError).toEqual({
|
|
73
|
+
code: 'invitation-expired',
|
|
74
|
+
message: 'This invitation has expired.',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {SignupResponseDto} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {
|
|
3
|
+
toAuthenticatedSession,
|
|
4
|
+
toUserIdentity,
|
|
5
|
+
type UserIdentity,
|
|
6
|
+
} from '@shipfox/client-shell/runtime';
|
|
7
|
+
|
|
8
|
+
export {toAuthenticatedSession};
|
|
9
|
+
|
|
10
|
+
export interface SignupResult {
|
|
11
|
+
user: UserIdentity;
|
|
12
|
+
emailChallenge?: {id: string; nextResendAvailableAt: string};
|
|
13
|
+
membership?: {id: string; userId: string; workspaceId: string};
|
|
14
|
+
acceptError?: {code: string; message: string};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toSignupResult(dto: SignupResponseDto): SignupResult {
|
|
18
|
+
return {
|
|
19
|
+
user: toUserIdentity(dto.user),
|
|
20
|
+
...(dto.email_challenge
|
|
21
|
+
? {
|
|
22
|
+
emailChallenge: {
|
|
23
|
+
id: dto.email_challenge.id,
|
|
24
|
+
nextResendAvailableAt: dto.email_challenge.next_resend_available_at,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
...(dto.membership
|
|
29
|
+
? {
|
|
30
|
+
membership: {
|
|
31
|
+
id: dto.membership.id,
|
|
32
|
+
userId: dto.membership.user_id,
|
|
33
|
+
workspaceId: dto.membership.workspace_id,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
...(dto.accept_error ? {acceptError: dto.accept_error} : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -37,7 +37,7 @@ describe('loginAuth', () => {
|
|
|
37
37
|
const result = await loginAuth(body);
|
|
38
38
|
|
|
39
39
|
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
40
|
-
expect(result.
|
|
40
|
+
expect(result.accessToken).toBe('access-token');
|
|
41
41
|
expect(request.url).toBe('https://api.example.test/auth/login');
|
|
42
42
|
expect(request.method).toBe('POST');
|
|
43
43
|
expect(requestBody).toEqual(body);
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import {type LoginBodyDto, loginResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
3
|
import {useMutation} from '@tanstack/react-query';
|
|
4
4
|
import {useAuthTransition} from '#state/auth.js';
|
|
5
|
+
import {toAuthenticatedSession} from './auth-mapper.js';
|
|
5
6
|
|
|
6
7
|
export async function loginAuth(body: LoginBodyDto) {
|
|
7
|
-
|
|
8
|
+
const response = await checkedApiRequest(loginResponseSchema, '/auth/login', {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
body,
|
|
11
|
+
});
|
|
12
|
+
return toAuthenticatedSession(response);
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
export function useLoginAuth() {
|
|
@@ -60,7 +60,7 @@ describe('confirmPasswordReset', () => {
|
|
|
60
60
|
const result = await confirmPasswordReset(body);
|
|
61
61
|
|
|
62
62
|
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
63
|
-
expect(result.
|
|
63
|
+
expect(result.accessToken).toBe('reset-access-token');
|
|
64
64
|
expect(request.url).toBe('https://api.example.test/auth/password-reset/confirm');
|
|
65
65
|
expect(request.method).toBe('POST');
|
|
66
66
|
expect(requestBody).toEqual(body);
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
PasswordResetConfirmBodyDto,
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
type PasswordResetConfirmBodyDto,
|
|
3
|
+
type PasswordResetRequestBodyDto,
|
|
4
|
+
passwordResetConfirmResponseSchema,
|
|
5
5
|
} from '@shipfox/api-auth-dto';
|
|
6
|
-
import {apiRequest} from '@shipfox/client-api';
|
|
6
|
+
import {apiRequest, checkedApiRequest} from '@shipfox/client-api';
|
|
7
7
|
import {useMutation} from '@tanstack/react-query';
|
|
8
8
|
import {useAuthTransition} from '#state/auth.js';
|
|
9
|
+
import {toAuthenticatedSession} from './auth-mapper.js';
|
|
9
10
|
|
|
10
11
|
export async function requestPasswordReset(body: PasswordResetRequestBodyDto) {
|
|
11
12
|
await apiRequest<void>('/auth/password-reset', {method: 'POST', body});
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export async function confirmPasswordReset(body: PasswordResetConfirmBodyDto) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const response = await checkedApiRequest(
|
|
17
|
+
passwordResetConfirmResponseSchema,
|
|
18
|
+
'/auth/password-reset/confirm',
|
|
19
|
+
{
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body,
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
return toAuthenticatedSession(response);
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export function useRequestPasswordResetAuth() {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import {type SignupBodyDto, signupResponseSchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
3
|
import {useMutation} from '@tanstack/react-query';
|
|
4
|
+
import {toSignupResult} from './auth-mapper.js';
|
|
4
5
|
|
|
5
6
|
async function signupAuth(body: SignupBodyDto) {
|
|
6
|
-
|
|
7
|
+
const response = await checkedApiRequest(signupResponseSchema, '/auth/signup', {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
body,
|
|
10
|
+
});
|
|
11
|
+
return toSignupResult(response);
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
export function useSignupAuth() {
|
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
VerifyEmailConfirmBodyDto,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
type VerifyEmailConfirmBodyDto,
|
|
3
|
+
type VerifyEmailResendBodyDto,
|
|
4
|
+
verifyEmailConfirmResponseSchema,
|
|
5
|
+
verifyEmailResendResponseSchema,
|
|
6
6
|
} from '@shipfox/api-auth-dto';
|
|
7
|
-
import {
|
|
7
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
8
8
|
import {useMutation} from '@tanstack/react-query';
|
|
9
9
|
import {useAuthTransition} from '#state/auth.js';
|
|
10
|
+
import {toAuthenticatedSession} from './auth-mapper.js';
|
|
10
11
|
|
|
11
12
|
async function verifyEmailAuth(body: VerifyEmailConfirmBodyDto) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const response = await checkedApiRequest(
|
|
14
|
+
verifyEmailConfirmResponseSchema,
|
|
15
|
+
'/auth/verify-email/confirm',
|
|
16
|
+
{
|
|
17
|
+
method: 'POST',
|
|
18
|
+
body,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
return toAuthenticatedSession(response);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
async function resendEmailVerificationAuth(body: VerifyEmailResendBodyDto) {
|
|
19
|
-
return await
|
|
25
|
+
return await checkedApiRequest(verifyEmailResendResponseSchema, '/auth/verify-email/resend', {
|
|
20
26
|
method: 'POST',
|
|
21
27
|
body,
|
|
22
28
|
});
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import {type CreateWorkspaceBodyDto, workspaceResponseSchema} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
3
|
import {listUserWorkspaces, userWorkspacesQueryKey} from '@shipfox/client-shell/runtime';
|
|
4
4
|
import {useMutation} from '@tanstack/react-query';
|
|
5
5
|
import {useRefreshAuth} from './refresh-auth.js';
|
|
6
|
+
import {toWorkspace} from './workspace-mapper.js';
|
|
6
7
|
|
|
7
8
|
export async function createWorkspace(body: CreateWorkspaceBodyDto) {
|
|
8
|
-
|
|
9
|
+
const response = await checkedApiRequest(workspaceResponseSchema, '/workspaces', {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
body,
|
|
12
|
+
});
|
|
13
|
+
return toWorkspace(response);
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
export {listUserWorkspaces, userWorkspacesQueryKey};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {WorkspaceResponseDto} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import {toWorkspace} from './workspace-mapper.js';
|
|
3
|
+
|
|
4
|
+
describe('toWorkspace', () => {
|
|
5
|
+
test.each([
|
|
6
|
+
'active',
|
|
7
|
+
'suspended',
|
|
8
|
+
'deleted',
|
|
9
|
+
] as const)('maps a %s workspace to its domain shape', (status) => {
|
|
10
|
+
const dto: WorkspaceResponseDto = {
|
|
11
|
+
id: '11111111-1111-4111-8111-111111111111',
|
|
12
|
+
name: 'Acme',
|
|
13
|
+
status,
|
|
14
|
+
settings: {},
|
|
15
|
+
created_at: '2026-04-27T00:00:00.000Z',
|
|
16
|
+
updated_at: '2026-04-27T00:00:00.000Z',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
expect(toWorkspace(dto)).toEqual({id: dto.id, name: dto.name, status});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type {WorkspaceResponseDto} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
|
|
3
|
+
export interface Workspace {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: 'active' | 'suspended' | 'deleted';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function toWorkspace(dto: WorkspaceResponseDto): Workspace {
|
|
10
|
+
return {id: dto.id, name: dto.name, status: dto.status};
|
|
11
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './components/auth-guard.js';
|
|
2
2
|
export * from './components/auth-provider.js';
|
|
3
|
-
export * from './components/auth-shell.js';
|
|
4
3
|
export {WorkspaceCrumb, type WorkspaceCrumbProps} from './components/workspace-crumb.js';
|
|
5
4
|
export {WorkspaceSwitcher, type WorkspaceSwitcherProps} from './components/workspace-switcher.js';
|
|
6
5
|
export * from './hooks/index.js';
|
|
@@ -1,39 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {useQuery} from '@tanstack/react-query';
|
|
1
|
+
import {
|
|
2
|
+
type InvitationPreview,
|
|
3
|
+
pendingInvitation,
|
|
4
|
+
usePreviewInvitation,
|
|
5
|
+
} from '@shipfox/client-invitations';
|
|
7
6
|
import {parseRedirectContext} from '#/components/redirect-context.js';
|
|
8
7
|
|
|
9
8
|
export function extractInvitationToken(redirect: unknown): string | undefined {
|
|
10
9
|
return parseRedirectContext(redirect).invitationToken;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
async function fetchPreview(token: string): Promise<PreviewInvitationResponseDto> {
|
|
14
|
-
const params = new URLSearchParams({token});
|
|
15
|
-
return await apiRequest<PreviewInvitationResponseDto>(
|
|
16
|
-
`/invitations/preview?${params.toString()}`,
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function pendingInvitation(
|
|
21
|
-
data: PreviewInvitationResponseDto | undefined,
|
|
22
|
-
): PreviewInvitationPendingDto | undefined {
|
|
23
|
-
return data?.status === 'pending' ? data : undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Mirrors the hook in `@shipfox/client-invitations` but lives in `client-auth`
|
|
28
|
-
* to avoid a circular dependency. Signup/login pages call this to lock the
|
|
29
|
-
* email field when arriving from an invitation link.
|
|
30
|
-
*/
|
|
31
12
|
export function useInvitationContext(token: string | undefined) {
|
|
32
|
-
return
|
|
33
|
-
queryKey: ['invitations', 'preview', token ?? ''] as const,
|
|
34
|
-
queryFn: () => fetchPreview(token as string),
|
|
35
|
-
enabled: Boolean(token),
|
|
36
|
-
retry: false,
|
|
37
|
-
staleTime: 30_000,
|
|
38
|
-
});
|
|
13
|
+
return usePreviewInvitation(token);
|
|
39
14
|
}
|
|
15
|
+
|
|
16
|
+
export {type InvitationPreview, pendingInvitation};
|
|
@@ -70,7 +70,7 @@ describe('LoginPage', () => {
|
|
|
70
70
|
return Promise.resolve(
|
|
71
71
|
jsonResponse({
|
|
72
72
|
status: 'pending',
|
|
73
|
-
workspace_id: '
|
|
73
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
74
74
|
workspace_name: 'Invite Workspace',
|
|
75
75
|
email: 'invitee@example.com',
|
|
76
76
|
invited_by_display: 'owner@example.com',
|
package/src/pages/login-page.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {loginBodySchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
2
3
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
3
4
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
4
5
|
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
@@ -8,7 +9,6 @@ import {useForm} from '@tanstack/react-form';
|
|
|
8
9
|
import {Link, useSearch} from '@tanstack/react-router';
|
|
9
10
|
import {useAtom} from 'jotai';
|
|
10
11
|
import {useEffect, useRef, useState} from 'react';
|
|
11
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
12
12
|
import {useLoginAuth} from '#hooks/api/login-auth.js';
|
|
13
13
|
import {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';
|
|
14
14
|
import {loginErrorToFormError} from './form-errors.js';
|
|
@@ -81,7 +81,7 @@ export function LoginPage() {
|
|
|
81
81
|
? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`
|
|
82
82
|
: undefined;
|
|
83
83
|
const headerTitle = invitationPending
|
|
84
|
-
? `Join ${invitationPending.
|
|
84
|
+
? `Join ${invitationPending.workspaceName}`
|
|
85
85
|
: 'Connect to Shipfox';
|
|
86
86
|
const headerDescription = invitationPending
|
|
87
87
|
? 'Log in to accept your invitation.'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
1
2
|
import {Text} from '@shipfox/react-ui/typography';
|
|
2
3
|
import {useNavigate, useRouter, useSearch} from '@tanstack/react-router';
|
|
3
4
|
import {useEffect} from 'react';
|
|
4
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
5
5
|
import {sanitizeRedirectPath} from '#/components/redirect-target.js';
|
|
6
6
|
import {useLogoutAuth} from '#hooks/api/logout-auth.js';
|
|
7
7
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
passwordResetConfirmBodySchema,
|
|
3
3
|
passwordResetRequestBodySchema,
|
|
4
4
|
} from '@shipfox/api-auth-dto';
|
|
5
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
5
6
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
6
7
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
7
8
|
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
@@ -11,12 +12,10 @@ import {useForm} from '@tanstack/react-form';
|
|
|
11
12
|
import {Link, useNavigate, useSearch} from '@tanstack/react-router';
|
|
12
13
|
import {useAtom} from 'jotai';
|
|
13
14
|
import {useEffect, useRef, useState} from 'react';
|
|
14
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
15
15
|
import {
|
|
16
16
|
useConfirmPasswordResetAuth,
|
|
17
17
|
useRequestPasswordResetAuth,
|
|
18
18
|
} from '#hooks/api/password-reset-auth.js';
|
|
19
|
-
import {useRefreshAuth} from '#hooks/api/refresh-auth.js';
|
|
20
19
|
import {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';
|
|
21
20
|
import {
|
|
22
21
|
passwordResetConfirmErrorToFormError,
|
|
@@ -148,7 +147,6 @@ function PasswordResetRequest() {
|
|
|
148
147
|
|
|
149
148
|
function PasswordResetConfirm({token}: {token: string}) {
|
|
150
149
|
const confirmPasswordReset = useConfirmPasswordResetAuth();
|
|
151
|
-
const refreshAuth = useRefreshAuth();
|
|
152
150
|
const navigate = useNavigate();
|
|
153
151
|
const [, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
154
152
|
const [formError, setFormError] = useState<string | undefined>();
|
|
@@ -163,7 +161,6 @@ function PasswordResetConfirm({token}: {token: string}) {
|
|
|
163
161
|
new_password: value.new_password,
|
|
164
162
|
});
|
|
165
163
|
await confirmPasswordReset.mutateAsync(body);
|
|
166
|
-
await refreshAuth();
|
|
167
164
|
setAuthFormDraft(initialAuthFormDraft);
|
|
168
165
|
toast.success('Your password has been changed. You are now logged in.');
|
|
169
166
|
await navigate({to: '/', replace: true});
|
|
@@ -137,7 +137,7 @@ describe('SignupPage', () => {
|
|
|
137
137
|
return Promise.resolve(
|
|
138
138
|
jsonResponse({
|
|
139
139
|
status: 'pending',
|
|
140
|
-
workspace_id: '
|
|
140
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
141
141
|
workspace_name: 'Invite Workspace',
|
|
142
142
|
email: 'invitee@example.com',
|
|
143
143
|
invited_by_display: 'owner@example.com',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {signupBodySchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
2
3
|
import {displayNameFieldError} from '@shipfox/client-ui';
|
|
3
4
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
4
5
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -10,7 +11,6 @@ import {useForm} from '@tanstack/react-form';
|
|
|
10
11
|
import {Link, useNavigate, useSearch} from '@tanstack/react-router';
|
|
11
12
|
import {useAtom} from 'jotai';
|
|
12
13
|
import {useEffect, useRef, useState} from 'react';
|
|
13
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
14
14
|
import {EmailCodeVerification} from '#/components/email-code-verification.js';
|
|
15
15
|
import {useRefreshAuth} from '#hooks/api/refresh-auth.js';
|
|
16
16
|
import {useSignupAuth} from '#hooks/api/signup-auth.js';
|
|
@@ -67,16 +67,16 @@ export function SignupPage() {
|
|
|
67
67
|
// Refresh failures don't block the success message — the next API
|
|
68
68
|
// call's 401 handling will re-route the user.
|
|
69
69
|
}
|
|
70
|
-
toast.success(`You joined ${invitationPending.
|
|
70
|
+
toast.success(`You joined ${invitationPending.workspaceName}.`);
|
|
71
71
|
await navigate({
|
|
72
72
|
to: '/workspaces/$wid',
|
|
73
|
-
params: {wid: result.membership.
|
|
73
|
+
params: {wid: result.membership.workspaceId},
|
|
74
74
|
});
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
if (invitationToken && result.
|
|
79
|
-
toast.error(result.
|
|
78
|
+
if (invitationToken && result.acceptError) {
|
|
79
|
+
toast.error(result.acceptError.message);
|
|
80
80
|
await navigate({
|
|
81
81
|
to: '/invitations/accept',
|
|
82
82
|
search: {token: invitationToken},
|
|
@@ -84,12 +84,12 @@ export function SignupPage() {
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
if (!result.
|
|
87
|
+
if (!result.emailChallenge) {
|
|
88
88
|
throw new Error('Signup did not return an email verification challenge');
|
|
89
89
|
}
|
|
90
|
-
setEmailChallenge({email: result.user.email, id: result.
|
|
90
|
+
setEmailChallenge({email: result.user.email, id: result.emailChallenge.id});
|
|
91
91
|
setResendError(undefined);
|
|
92
|
-
setNextResendAvailableAt(result.
|
|
92
|
+
setNextResendAvailableAt(result.emailChallenge.nextResendAvailableAt);
|
|
93
93
|
} catch (error) {
|
|
94
94
|
const mapped = signupErrorToFormError(error);
|
|
95
95
|
if (mapped.kind === 'field') {
|
|
@@ -181,7 +181,7 @@ export function SignupPage() {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
const headerTitle = invitationPending
|
|
184
|
-
? `Join ${invitationPending.
|
|
184
|
+
? `Join ${invitationPending.workspaceName}`
|
|
185
185
|
: 'Create your Shipfox account';
|
|
186
186
|
const headerDescription = invitationPending
|
|
187
187
|
? `Create an account to accept your invitation.`
|