create-aws-project 1.2.1
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/README.md +118 -0
- package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
- package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
- package/dist/__tests__/generator.spec.d.ts +1 -0
- package/dist/__tests__/generator.spec.js +162 -0
- package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
- package/dist/__tests__/validation/project-name.spec.js +57 -0
- package/dist/__tests__/wizard.spec.d.ts +1 -0
- package/dist/__tests__/wizard.spec.js +232 -0
- package/dist/aws/iam.d.ts +75 -0
- package/dist/aws/iam.js +264 -0
- package/dist/aws/organizations.d.ts +79 -0
- package/dist/aws/organizations.js +168 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +206 -0
- package/dist/commands/setup-github.d.ts +4 -0
- package/dist/commands/setup-github.js +185 -0
- package/dist/generator/copy-file.d.ts +15 -0
- package/dist/generator/copy-file.js +56 -0
- package/dist/generator/generate-project.d.ts +14 -0
- package/dist/generator/generate-project.js +81 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +3 -0
- package/dist/generator/replace-tokens.d.ts +29 -0
- package/dist/generator/replace-tokens.js +68 -0
- package/dist/github/secrets.d.ts +109 -0
- package/dist/github/secrets.js +275 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/prompts/auth.d.ts +3 -0
- package/dist/prompts/auth.js +23 -0
- package/dist/prompts/aws-config.d.ts +2 -0
- package/dist/prompts/aws-config.js +14 -0
- package/dist/prompts/features.d.ts +2 -0
- package/dist/prompts/features.js +10 -0
- package/dist/prompts/github-setup.d.ts +53 -0
- package/dist/prompts/github-setup.js +208 -0
- package/dist/prompts/org-structure.d.ts +9 -0
- package/dist/prompts/org-structure.js +93 -0
- package/dist/prompts/platforms.d.ts +2 -0
- package/dist/prompts/platforms.js +12 -0
- package/dist/prompts/project-name.d.ts +2 -0
- package/dist/prompts/project-name.js +8 -0
- package/dist/prompts/theme.d.ts +2 -0
- package/dist/prompts/theme.js +14 -0
- package/dist/templates/index.d.ts +4 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/manifest.d.ts +11 -0
- package/dist/templates/manifest.js +99 -0
- package/dist/templates/tokens.d.ts +39 -0
- package/dist/templates/tokens.js +37 -0
- package/dist/templates/types.d.ts +52 -0
- package/dist/templates/types.js +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/dist/validation/project-name.d.ts +1 -0
- package/dist/validation/project-name.js +12 -0
- package/dist/wizard.d.ts +2 -0
- package/dist/wizard.js +81 -0
- package/package.json +68 -0
- package/templates/.github/actions/build-and-test/action.yml +24 -0
- package/templates/.github/actions/deploy-cdk/action.yml +46 -0
- package/templates/.github/actions/deploy-web/action.yml +72 -0
- package/templates/.github/actions/setup/action.yml +29 -0
- package/templates/.github/pull_request_template.md +15 -0
- package/templates/.github/workflows/deploy-dev.yml +80 -0
- package/templates/.github/workflows/deploy-prod.yml +67 -0
- package/templates/.github/workflows/deploy-stage.yml +77 -0
- package/templates/.github/workflows/pull-request.yml +72 -0
- package/templates/.vscode/extensions.json +7 -0
- package/templates/.vscode/settings.json +67 -0
- package/templates/apps/api/.eslintrc.json +18 -0
- package/templates/apps/api/cdk/app.ts +93 -0
- package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
- package/templates/apps/api/cdk/cdk.json +73 -0
- package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
- package/templates/apps/api/cdk/org-stack.ts +67 -0
- package/templates/apps/api/cdk/static-stack.ts +361 -0
- package/templates/apps/api/cdk/tsconfig.json +39 -0
- package/templates/apps/api/cdk/user-stack.ts +255 -0
- package/templates/apps/api/jest.config.ts +38 -0
- package/templates/apps/api/lambdas.yml +84 -0
- package/templates/apps/api/project.json.template +58 -0
- package/templates/apps/api/src/__tests__/setup.ts +10 -0
- package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
- package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
- package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
- package/templates/apps/api/src/handlers/users/index.ts +17 -0
- package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
- package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
- package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
- package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
- package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
- package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
- package/templates/apps/api/src/models/UserModel.ts +109 -0
- package/templates/apps/api/src/schemas/user.schema.ts +44 -0
- package/templates/apps/api/src/services/user-service.ts +108 -0
- package/templates/apps/api/src/utils/auth-context.ts +60 -0
- package/templates/apps/api/src/utils/common/helpers.ts +26 -0
- package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
- package/templates/apps/api/src/utils/response.ts +52 -0
- package/templates/apps/api/src/utils/validator.ts +75 -0
- package/templates/apps/api/tsconfig.app.json +15 -0
- package/templates/apps/api/tsconfig.json +19 -0
- package/templates/apps/api/tsconfig.spec.json +17 -0
- package/templates/apps/mobile/.env.example +5 -0
- package/templates/apps/mobile/.eslintrc.json +33 -0
- package/templates/apps/mobile/app.json +33 -0
- package/templates/apps/mobile/assets/.gitkeep +0 -0
- package/templates/apps/mobile/babel.config.js +19 -0
- package/templates/apps/mobile/index.js +7 -0
- package/templates/apps/mobile/jest.config.ts +22 -0
- package/templates/apps/mobile/metro.config.js +35 -0
- package/templates/apps/mobile/package.json +22 -0
- package/templates/apps/mobile/project.json.template +64 -0
- package/templates/apps/mobile/src/App.tsx +367 -0
- package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
- package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
- package/templates/apps/mobile/src/config/api.ts +16 -0
- package/templates/apps/mobile/src/store/user-store.ts +56 -0
- package/templates/apps/mobile/src/test-setup.ts +10 -0
- package/templates/apps/mobile/tsconfig.json +22 -0
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/.eslintrc.json +26 -0
- package/templates/apps/web/index.html +13 -0
- package/templates/apps/web/jest.config.ts +24 -0
- package/templates/apps/web/package.json +15 -0
- package/templates/apps/web/project.json.template +66 -0
- package/templates/apps/web/src/App.tsx +352 -0
- package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
- package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
- package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
- package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
- package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
- package/templates/apps/web/src/auth/index.ts +7 -0
- package/templates/apps/web/src/auth/use-auth.ts +16 -0
- package/templates/apps/web/src/config/amplify-config.ts +31 -0
- package/templates/apps/web/src/config/api.ts +38 -0
- package/templates/apps/web/src/config/auth0-config.ts +17 -0
- package/templates/apps/web/src/main.tsx +41 -0
- package/templates/apps/web/src/store/user-store.ts +56 -0
- package/templates/apps/web/src/styles.css +165 -0
- package/templates/apps/web/src/test-setup.ts +1 -0
- package/templates/apps/web/src/theme/index.ts +30 -0
- package/templates/apps/web/src/vite-env.d.ts +19 -0
- package/templates/apps/web/tsconfig.app.json +24 -0
- package/templates/apps/web/tsconfig.json +22 -0
- package/templates/apps/web/tsconfig.spec.json +28 -0
- package/templates/apps/web/vite.config.ts +87 -0
- package/templates/manifest.json +28 -0
- package/templates/packages/api-client/.eslintrc.json +18 -0
- package/templates/packages/api-client/jest.config.ts +13 -0
- package/templates/packages/api-client/package.json +8 -0
- package/templates/packages/api-client/project.json.template +34 -0
- package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
- package/templates/packages/api-client/src/api-client.ts +201 -0
- package/templates/packages/api-client/src/config.ts +193 -0
- package/templates/packages/api-client/src/index.ts +9 -0
- package/templates/packages/api-client/tsconfig.json +22 -0
- package/templates/packages/api-client/tsconfig.lib.json +11 -0
- package/templates/packages/api-client/tsconfig.spec.json +14 -0
- package/templates/packages/common-types/.eslintrc.json +18 -0
- package/templates/packages/common-types/package.json +6 -0
- package/templates/packages/common-types/project.json.template +26 -0
- package/templates/packages/common-types/src/api.types.ts +24 -0
- package/templates/packages/common-types/src/auth.types.ts +36 -0
- package/templates/packages/common-types/src/common.types.ts +46 -0
- package/templates/packages/common-types/src/index.ts +19 -0
- package/templates/packages/common-types/src/lambda.types.ts +39 -0
- package/templates/packages/common-types/src/user.types.ts +31 -0
- package/templates/packages/common-types/tsconfig.json +19 -0
- package/templates/packages/common-types/tsconfig.lib.json +11 -0
- package/templates/root/.editorconfig +23 -0
- package/templates/root/.nvmrc +1 -0
- package/templates/root/eslint.config.js +61 -0
- package/templates/root/jest.preset.js +16 -0
- package/templates/root/nx.json +29 -0
- package/templates/root/package.json +131 -0
- package/templates/root/tsconfig.base.json +29 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
2
|
+
import { ChakraProvider } from '@chakra-ui/react';
|
|
3
|
+
import { useUserStore } from '../store/user-store';
|
|
4
|
+
import App from '../App';
|
|
5
|
+
import theme from '../theme';
|
|
6
|
+
import { apiClient } from '../config/api';
|
|
7
|
+
import { ApiError } from '{{PACKAGE_SCOPE}}/api-client';
|
|
8
|
+
|
|
9
|
+
// Mock the apiClient
|
|
10
|
+
jest.mock('../config/api', () => ({
|
|
11
|
+
apiClient: {
|
|
12
|
+
getUsers: jest.fn(),
|
|
13
|
+
createUser: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const mockApiClient = apiClient as jest.Mocked<typeof apiClient>;
|
|
18
|
+
|
|
19
|
+
const renderWithChakra = async (component: React.ReactElement) => {
|
|
20
|
+
let result;
|
|
21
|
+
await act(async () => {
|
|
22
|
+
result = render(<ChakraProvider theme={theme}>{component}</ChakraProvider>);
|
|
23
|
+
});
|
|
24
|
+
// Wait for any pending async operations
|
|
25
|
+
await act(async () => {
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
27
|
+
});
|
|
28
|
+
return result!;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('App', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
// Reset store before each test
|
|
34
|
+
useUserStore.setState({
|
|
35
|
+
user: null,
|
|
36
|
+
users: [],
|
|
37
|
+
isLoading: false,
|
|
38
|
+
error: null,
|
|
39
|
+
});
|
|
40
|
+
// Reset mocks
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
// Default mock implementations
|
|
43
|
+
mockApiClient.getUsers.mockResolvedValue([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
// Wait for any pending state updates to flush
|
|
48
|
+
await act(async () => {
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should render the app title', async () => {
|
|
54
|
+
await renderWithChakra(<App />);
|
|
55
|
+
expect(screen.getByText('{{PROJECT_NAME_TITLE}}')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should render welcome message', async () => {
|
|
59
|
+
await renderWithChakra(<App />);
|
|
60
|
+
expect(screen.getByText('Welcome to the Web Client')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should render load demo user button initially', async () => {
|
|
64
|
+
await renderWithChakra(<App />);
|
|
65
|
+
expect(screen.getByText('Load Demo User')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should display user info when demo user is loaded', async () => {
|
|
69
|
+
await renderWithChakra(<App />);
|
|
70
|
+
|
|
71
|
+
await act(async () => {
|
|
72
|
+
const loadButton = screen.getByText('Load Demo User');
|
|
73
|
+
fireEvent.click(loadButton);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(screen.getByText('Current User:')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
expect(screen.getByText('demo@example.com')).toBeInTheDocument();
|
|
80
|
+
expect(screen.getByText('Demo User')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should clear user when clear button is clicked', async () => {
|
|
84
|
+
await renderWithChakra(<App />);
|
|
85
|
+
|
|
86
|
+
// Load user
|
|
87
|
+
await act(async () => {
|
|
88
|
+
const loadButton = screen.getByText('Load Demo User');
|
|
89
|
+
fireEvent.click(loadButton);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Verify user is loaded
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(screen.getByText('Current User:')).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Clear user
|
|
98
|
+
await act(async () => {
|
|
99
|
+
const clearButton = screen.getByText('Clear User');
|
|
100
|
+
fireEvent.click(clearButton);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Should show load button again (user is null, but users array still has data)
|
|
104
|
+
// So we check that "Current User:" is not visible anymore
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(screen.queryByText('Current User:')).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should list all features', async () => {
|
|
111
|
+
await renderWithChakra(<App />);
|
|
112
|
+
|
|
113
|
+
expect(screen.getByText(/React 18 with TypeScript/)).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText(/Vite for fast development/)).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByText(/Chakra UI component library/)).toBeInTheDocument();
|
|
116
|
+
expect(screen.getByText(/Zustand for state management/)).toBeInTheDocument();
|
|
117
|
+
expect(screen.getByText(/Jest for testing/)).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle API error when fetching users', async () => {
|
|
121
|
+
const apiError = new ApiError('Network error', 500, 'SERVER_ERROR');
|
|
122
|
+
mockApiClient.getUsers.mockRejectedValue(apiError);
|
|
123
|
+
|
|
124
|
+
await renderWithChakra(<App />);
|
|
125
|
+
|
|
126
|
+
// Click fetch users button to trigger error
|
|
127
|
+
await act(async () => {
|
|
128
|
+
const fetchButton = screen.getByText('Fetch Users from API');
|
|
129
|
+
fireEvent.click(fetchButton);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Wait for error toast to appear (the App shows toast on error)
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
expect(mockApiClient.getUsers).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle generic error when fetching users', async () => {
|
|
139
|
+
mockApiClient.getUsers.mockRejectedValue(new Error('Generic error'));
|
|
140
|
+
|
|
141
|
+
await renderWithChakra(<App />);
|
|
142
|
+
|
|
143
|
+
await act(async () => {
|
|
144
|
+
const fetchButton = screen.getByText('Fetch Users from API');
|
|
145
|
+
fireEvent.click(fetchButton);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(mockApiClient.getUsers).toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should create user successfully', async () => {
|
|
154
|
+
const newUser = {
|
|
155
|
+
id: 'new-id',
|
|
156
|
+
email: 'new@example.com',
|
|
157
|
+
name: 'New User',
|
|
158
|
+
createdAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
mockApiClient.createUser.mockResolvedValue(newUser);
|
|
161
|
+
|
|
162
|
+
await renderWithChakra(<App />);
|
|
163
|
+
|
|
164
|
+
await act(async () => {
|
|
165
|
+
const createButton = screen.getByText('Create Test User');
|
|
166
|
+
fireEvent.click(createButton);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(mockApiClient.createUser).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Verify user was added to store
|
|
174
|
+
expect(useUserStore.getState().users).toContainEqual(newUser);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle API error when creating user', async () => {
|
|
178
|
+
const apiError = new ApiError('Validation error', 400, 'VALIDATION_ERROR');
|
|
179
|
+
mockApiClient.createUser.mockRejectedValue(apiError);
|
|
180
|
+
|
|
181
|
+
await renderWithChakra(<App />);
|
|
182
|
+
|
|
183
|
+
await act(async () => {
|
|
184
|
+
const createButton = screen.getByText('Create Test User');
|
|
185
|
+
fireEvent.click(createButton);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await waitFor(() => {
|
|
189
|
+
expect(mockApiClient.createUser).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle generic error when creating user', async () => {
|
|
194
|
+
mockApiClient.createUser.mockRejectedValue(new Error('Generic error'));
|
|
195
|
+
|
|
196
|
+
await renderWithChakra(<App />);
|
|
197
|
+
|
|
198
|
+
await act(async () => {
|
|
199
|
+
const createButton = screen.getByText('Create Test User');
|
|
200
|
+
fireEvent.click(createButton);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(mockApiClient.createUser).toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should fetch users on mount and display count', async () => {
|
|
209
|
+
const mockUsers = [
|
|
210
|
+
{ id: '1', email: 'user1@example.com', name: 'User 1', createdAt: '2024-01-01' },
|
|
211
|
+
{ id: '2', email: 'user2@example.com', name: 'User 2', createdAt: '2024-01-02' },
|
|
212
|
+
];
|
|
213
|
+
mockApiClient.getUsers.mockResolvedValue(mockUsers);
|
|
214
|
+
|
|
215
|
+
await renderWithChakra(<App />);
|
|
216
|
+
|
|
217
|
+
await waitFor(() => {
|
|
218
|
+
expect(mockApiClient.getUsers).toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Verify users were loaded into store
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(useUserStore.getState().users).toEqual(mockUsers);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should display error message when error state is set', async () => {
|
|
228
|
+
// Set error state before rendering
|
|
229
|
+
useUserStore.setState({
|
|
230
|
+
user: null,
|
|
231
|
+
users: [],
|
|
232
|
+
isLoading: false,
|
|
233
|
+
error: 'Test error message',
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await renderWithChakra(<App />);
|
|
237
|
+
|
|
238
|
+
expect(screen.getByText(/Error: Test error message/)).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useUserStore } from '../../store/user-store';
|
|
2
|
+
import type { User } from '{{PACKAGE_SCOPE}}/common-types';
|
|
3
|
+
|
|
4
|
+
describe('UserStore', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset store before each test
|
|
7
|
+
useUserStore.setState({
|
|
8
|
+
user: null,
|
|
9
|
+
users: [],
|
|
10
|
+
isLoading: false,
|
|
11
|
+
error: null,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should initialize with empty state', () => {
|
|
16
|
+
const state = useUserStore.getState();
|
|
17
|
+
expect(state.user).toBeNull();
|
|
18
|
+
expect(state.users).toEqual([]);
|
|
19
|
+
expect(state.isLoading).toBe(false);
|
|
20
|
+
expect(state.error).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should set user', () => {
|
|
24
|
+
const user: User = {
|
|
25
|
+
id: '1',
|
|
26
|
+
email: 'test@example.com',
|
|
27
|
+
name: 'Test User',
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
useUserStore.getState().setUser(user);
|
|
32
|
+
expect(useUserStore.getState().user).toEqual(user);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should add user to users array', () => {
|
|
36
|
+
const user: User = {
|
|
37
|
+
id: '1',
|
|
38
|
+
email: 'test@example.com',
|
|
39
|
+
name: 'Test User',
|
|
40
|
+
createdAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
useUserStore.getState().addUser(user);
|
|
44
|
+
expect(useUserStore.getState().users).toHaveLength(1);
|
|
45
|
+
expect(useUserStore.getState().users[0]).toEqual(user);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should update user in users array', () => {
|
|
49
|
+
const user: User = {
|
|
50
|
+
id: '1',
|
|
51
|
+
email: 'test@example.com',
|
|
52
|
+
name: 'Test User',
|
|
53
|
+
createdAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useUserStore.getState().addUser(user);
|
|
57
|
+
useUserStore.getState().updateUser('1', { name: 'Updated User' });
|
|
58
|
+
|
|
59
|
+
const updatedUser = useUserStore.getState().users[0];
|
|
60
|
+
expect(updatedUser.name).toBe('Updated User');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should remove user from users array', () => {
|
|
64
|
+
const user: User = {
|
|
65
|
+
id: '1',
|
|
66
|
+
email: 'test@example.com',
|
|
67
|
+
name: 'Test User',
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useUserStore.getState().addUser(user);
|
|
72
|
+
expect(useUserStore.getState().users).toHaveLength(1);
|
|
73
|
+
|
|
74
|
+
useUserStore.getState().removeUser('1');
|
|
75
|
+
expect(useUserStore.getState().users).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should set loading state', () => {
|
|
79
|
+
useUserStore.getState().setLoading(true);
|
|
80
|
+
expect(useUserStore.getState().isLoading).toBe(true);
|
|
81
|
+
|
|
82
|
+
useUserStore.getState().setLoading(false);
|
|
83
|
+
expect(useUserStore.getState().isLoading).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should set and clear error', () => {
|
|
87
|
+
useUserStore.getState().setError('Test error');
|
|
88
|
+
expect(useUserStore.getState().error).toBe('Test error');
|
|
89
|
+
|
|
90
|
+
useUserStore.getState().clearError();
|
|
91
|
+
expect(useUserStore.getState().error).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should update current user when updating by id', () => {
|
|
95
|
+
const user: User = {
|
|
96
|
+
id: '1',
|
|
97
|
+
email: 'test@example.com',
|
|
98
|
+
name: 'Test User',
|
|
99
|
+
createdAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
useUserStore.getState().setUser(user);
|
|
103
|
+
useUserStore.getState().addUser(user);
|
|
104
|
+
useUserStore.getState().updateUser('1', { name: 'Updated User' });
|
|
105
|
+
|
|
106
|
+
expect(useUserStore.getState().user?.name).toBe('Updated User');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not update users that do not match the id', () => {
|
|
110
|
+
const user1: User = {
|
|
111
|
+
id: '1',
|
|
112
|
+
email: 'user1@example.com',
|
|
113
|
+
name: 'User One',
|
|
114
|
+
createdAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
const user2: User = {
|
|
117
|
+
id: '2',
|
|
118
|
+
email: 'user2@example.com',
|
|
119
|
+
name: 'User Two',
|
|
120
|
+
createdAt: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
useUserStore.getState().addUser(user1);
|
|
124
|
+
useUserStore.getState().addUser(user2);
|
|
125
|
+
useUserStore.getState().updateUser('2', { name: 'Updated User Two' });
|
|
126
|
+
|
|
127
|
+
const users = useUserStore.getState().users;
|
|
128
|
+
expect(users[0].name).toBe('User One'); // Not updated
|
|
129
|
+
expect(users[1].name).toBe('Updated User Two'); // Updated
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should not clear current user when removing a different user', () => {
|
|
133
|
+
const currentUser: User = {
|
|
134
|
+
id: '1',
|
|
135
|
+
email: 'current@example.com',
|
|
136
|
+
name: 'Current User',
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
const otherUser: User = {
|
|
140
|
+
id: '2',
|
|
141
|
+
email: 'other@example.com',
|
|
142
|
+
name: 'Other User',
|
|
143
|
+
createdAt: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
useUserStore.getState().setUser(currentUser);
|
|
147
|
+
useUserStore.getState().addUser(currentUser);
|
|
148
|
+
useUserStore.getState().addUser(otherUser);
|
|
149
|
+
|
|
150
|
+
// Remove the other user, not the current user
|
|
151
|
+
useUserStore.getState().removeUser('2');
|
|
152
|
+
|
|
153
|
+
// Current user should still be set
|
|
154
|
+
expect(useUserStore.getState().user).toEqual(currentUser);
|
|
155
|
+
expect(useUserStore.getState().users).toHaveLength(1);
|
|
156
|
+
expect(useUserStore.getState().users[0].id).toBe('1');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should clear current user when removing the current user', () => {
|
|
160
|
+
const user: User = {
|
|
161
|
+
id: '1',
|
|
162
|
+
email: 'test@example.com',
|
|
163
|
+
name: 'Test User',
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
useUserStore.getState().setUser(user);
|
|
168
|
+
useUserStore.getState().addUser(user);
|
|
169
|
+
useUserStore.getState().removeUser('1');
|
|
170
|
+
|
|
171
|
+
expect(useUserStore.getState().user).toBeNull();
|
|
172
|
+
expect(useUserStore.getState().users).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should set users array', () => {
|
|
176
|
+
const users: User[] = [
|
|
177
|
+
{ id: '1', email: 'user1@example.com', name: 'User 1', createdAt: new Date().toISOString() },
|
|
178
|
+
{ id: '2', email: 'user2@example.com', name: 'User 2', createdAt: new Date().toISOString() },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
useUserStore.getState().setUsers(users);
|
|
182
|
+
expect(useUserStore.getState().users).toHaveLength(2);
|
|
183
|
+
expect(useUserStore.getState().users).toEqual(users);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createContext, useCallback, type ReactNode } from 'react';
|
|
2
|
+
import { Auth0Provider as Auth0ProviderBase, useAuth0 } from '@auth0/auth0-react';
|
|
3
|
+
import { auth0Config } from '../config/auth0-config';
|
|
4
|
+
import type { AuthContextType, AuthUser } from '{{PACKAGE_SCOPE}}/common-types';
|
|
5
|
+
|
|
6
|
+
export const AuthContext = createContext<AuthContextType | null>(null);
|
|
7
|
+
|
|
8
|
+
interface AuthProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal component that uses Auth0 hooks (must be inside Auth0ProviderBase)
|
|
14
|
+
*/
|
|
15
|
+
function AuthContextProvider({ children }: AuthProviderProps) {
|
|
16
|
+
const {
|
|
17
|
+
user: auth0User,
|
|
18
|
+
isLoading,
|
|
19
|
+
isAuthenticated,
|
|
20
|
+
error: auth0Error,
|
|
21
|
+
loginWithRedirect,
|
|
22
|
+
logout,
|
|
23
|
+
} = useAuth0();
|
|
24
|
+
|
|
25
|
+
// Map Auth0 user to our AuthUser type
|
|
26
|
+
const user: AuthUser | null = auth0User
|
|
27
|
+
? {
|
|
28
|
+
id: auth0User.sub || '',
|
|
29
|
+
email: auth0User.email || '',
|
|
30
|
+
emailVerified: auth0User.email_verified || false,
|
|
31
|
+
}
|
|
32
|
+
: null;
|
|
33
|
+
|
|
34
|
+
const error = auth0Error || null;
|
|
35
|
+
|
|
36
|
+
const signIn = useCallback(async (_email: string, _password: string) => {
|
|
37
|
+
// Auth0 uses redirect-based auth, email/password are handled by Auth0 login page
|
|
38
|
+
await loginWithRedirect();
|
|
39
|
+
}, [loginWithRedirect]);
|
|
40
|
+
|
|
41
|
+
const signUp = useCallback(async (_email: string, _password: string) => {
|
|
42
|
+
// Auth0 handles signup via the same redirect flow with screen_hint
|
|
43
|
+
await loginWithRedirect({
|
|
44
|
+
authorizationParams: {
|
|
45
|
+
screen_hint: 'signup',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}, [loginWithRedirect]);
|
|
49
|
+
|
|
50
|
+
const signOut = useCallback(async () => {
|
|
51
|
+
logout({
|
|
52
|
+
logoutParams: {
|
|
53
|
+
returnTo: window.location.origin,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}, [logout]);
|
|
57
|
+
|
|
58
|
+
const confirmSignUp = useCallback(async (_email: string, _code: string) => {
|
|
59
|
+
throw new Error('confirmSignUp is not supported with Auth0. Email verification is handled automatically by Auth0.');
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const forgotPassword = useCallback(async (_email: string) => {
|
|
63
|
+
throw new Error('forgotPassword is not supported with Auth0. Password reset is handled via the Auth0 Universal Login.');
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const confirmForgotPassword = useCallback(async (_email: string, _code: string, _newPassword: string) => {
|
|
67
|
+
throw new Error('confirmForgotPassword is not supported with Auth0. Password reset is handled via the Auth0 Universal Login.');
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const value: AuthContextType = {
|
|
71
|
+
user,
|
|
72
|
+
isLoading,
|
|
73
|
+
isAuthenticated,
|
|
74
|
+
error,
|
|
75
|
+
signIn,
|
|
76
|
+
signUp,
|
|
77
|
+
signOut,
|
|
78
|
+
confirmSignUp,
|
|
79
|
+
forgotPassword,
|
|
80
|
+
confirmForgotPassword,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Auth0 authentication provider that wraps @auth0/auth0-react
|
|
88
|
+
* and adapts to our AuthContextType interface
|
|
89
|
+
*/
|
|
90
|
+
export function AuthProvider({ children }: AuthProviderProps) {
|
|
91
|
+
return (
|
|
92
|
+
<Auth0ProviderBase
|
|
93
|
+
domain={auth0Config.domain}
|
|
94
|
+
clientId={auth0Config.clientId}
|
|
95
|
+
authorizationParams={{
|
|
96
|
+
redirect_uri: auth0Config.redirectUri,
|
|
97
|
+
...(auth0Config.audience && { audience: auth0Config.audience }),
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<AuthContextProvider>{children}</AuthContextProvider>
|
|
101
|
+
</Auth0ProviderBase>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createContext, useCallback, useEffect, useState, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
signIn as amplifySignIn,
|
|
4
|
+
signUp as amplifySignUp,
|
|
5
|
+
signOut as amplifySignOut,
|
|
6
|
+
confirmSignUp as amplifyConfirmSignUp,
|
|
7
|
+
resetPassword,
|
|
8
|
+
confirmResetPassword,
|
|
9
|
+
getCurrentUser,
|
|
10
|
+
fetchAuthSession,
|
|
11
|
+
} from 'aws-amplify/auth';
|
|
12
|
+
import type { AuthContextType, AuthUser } from '{{PACKAGE_SCOPE}}/common-types';
|
|
13
|
+
|
|
14
|
+
export const AuthContext = createContext<AuthContextType | null>(null);
|
|
15
|
+
|
|
16
|
+
interface AuthProviderProps {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AuthProvider({ children }: AuthProviderProps) {
|
|
21
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
22
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState<Error | null>(null);
|
|
24
|
+
|
|
25
|
+
const isAuthenticated = user !== null;
|
|
26
|
+
|
|
27
|
+
// Check for existing session on mount
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
checkAuthState();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
async function checkAuthState() {
|
|
33
|
+
try {
|
|
34
|
+
const currentUser = await getCurrentUser();
|
|
35
|
+
const session = await fetchAuthSession();
|
|
36
|
+
|
|
37
|
+
setUser({
|
|
38
|
+
id: currentUser.userId,
|
|
39
|
+
email: currentUser.signInDetails?.loginId || '',
|
|
40
|
+
emailVerified: true,
|
|
41
|
+
groups: session.tokens?.accessToken?.payload['cognito:groups'] as string[] | undefined,
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
setUser(null);
|
|
45
|
+
} finally {
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const signIn = useCallback(async (email: string, password: string) => {
|
|
51
|
+
setIsLoading(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
try {
|
|
54
|
+
await amplifySignIn({ username: email, password });
|
|
55
|
+
await checkAuthState();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(err instanceof Error ? err : new Error('Sign in failed'));
|
|
58
|
+
throw err;
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const signUp = useCallback(async (email: string, password: string) => {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
await amplifySignUp({
|
|
69
|
+
username: email,
|
|
70
|
+
password,
|
|
71
|
+
options: { userAttributes: { email } },
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
setError(err instanceof Error ? err : new Error('Sign up failed'));
|
|
75
|
+
throw err;
|
|
76
|
+
} finally {
|
|
77
|
+
setIsLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const signOut = useCallback(async () => {
|
|
82
|
+
setIsLoading(true);
|
|
83
|
+
try {
|
|
84
|
+
await amplifySignOut();
|
|
85
|
+
setUser(null);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setError(err instanceof Error ? err : new Error('Sign out failed'));
|
|
88
|
+
} finally {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const confirmSignUp = useCallback(async (email: string, code: string) => {
|
|
94
|
+
setIsLoading(true);
|
|
95
|
+
try {
|
|
96
|
+
await amplifyConfirmSignUp({ username: email, confirmationCode: code });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
setError(err instanceof Error ? err : new Error('Confirmation failed'));
|
|
99
|
+
throw err;
|
|
100
|
+
} finally {
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const forgotPassword = useCallback(async (email: string) => {
|
|
106
|
+
setIsLoading(true);
|
|
107
|
+
try {
|
|
108
|
+
await resetPassword({ username: email });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setError(err instanceof Error ? err : new Error('Password reset failed'));
|
|
111
|
+
throw err;
|
|
112
|
+
} finally {
|
|
113
|
+
setIsLoading(false);
|
|
114
|
+
}
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const confirmForgotPassword = useCallback(async (email: string, code: string, newPassword: string) => {
|
|
118
|
+
setIsLoading(true);
|
|
119
|
+
try {
|
|
120
|
+
await confirmResetPassword({ username: email, confirmationCode: code, newPassword });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
setError(err instanceof Error ? err : new Error('Password reset confirmation failed'));
|
|
123
|
+
throw err;
|
|
124
|
+
} finally {
|
|
125
|
+
setIsLoading(false);
|
|
126
|
+
}
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const value: AuthContextType = {
|
|
130
|
+
user,
|
|
131
|
+
isLoading,
|
|
132
|
+
isAuthenticated,
|
|
133
|
+
error,
|
|
134
|
+
signIn,
|
|
135
|
+
signUp,
|
|
136
|
+
signOut,
|
|
137
|
+
confirmSignUp,
|
|
138
|
+
forgotPassword,
|
|
139
|
+
confirmForgotPassword,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
143
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
// {{#if AUTH_COGNITO}}
|
|
3
|
+
import { AuthContext } from './cognito-provider';
|
|
4
|
+
// {{/if AUTH_COGNITO}}
|
|
5
|
+
// {{#if AUTH_AUTH0}}
|
|
6
|
+
import { AuthContext } from './auth0-provider';
|
|
7
|
+
// {{/if AUTH_AUTH0}}
|
|
8
|
+
import type { AuthContextType } from '{{PACKAGE_SCOPE}}/common-types';
|
|
9
|
+
|
|
10
|
+
export function useAuth(): AuthContextType {
|
|
11
|
+
const context = useContext(AuthContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|