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,90 @@
|
|
|
1
|
+
import { CognitoJwtVerifier } from 'aws-jwt-verify';
|
|
2
|
+
import type { CognitoJwtPayload } from 'aws-jwt-verify/jwt-model';
|
|
3
|
+
import { errorResponse } from '../utils/response';
|
|
4
|
+
import { ERROR_CODES, HTTP_STATUS } from '{{PACKAGE_SCOPE}}/common-types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Authenticated user information extracted from JWT token
|
|
8
|
+
*/
|
|
9
|
+
export interface AuthUser {
|
|
10
|
+
/** Cognito user ID (subject claim) */
|
|
11
|
+
sub: string;
|
|
12
|
+
/** User's email address (if available in token) */
|
|
13
|
+
email?: string;
|
|
14
|
+
/** Cognito groups the user belongs to */
|
|
15
|
+
groups?: string[];
|
|
16
|
+
/** Full JWT payload for additional claims access */
|
|
17
|
+
tokenPayload: CognitoJwtPayload;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Verifier instantiated outside handler for JWKS caching across Lambda invocations
|
|
21
|
+
const verifier = CognitoJwtVerifier.create({
|
|
22
|
+
userPoolId: process.env['COGNITO_USER_POOL_ID']!,
|
|
23
|
+
tokenUse: 'access',
|
|
24
|
+
clientId: process.env['COGNITO_CLIENT_ID']!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Verify a JWT access token from the Authorization header
|
|
29
|
+
*
|
|
30
|
+
* @param authHeader - The Authorization header value (e.g., "Bearer <token>")
|
|
31
|
+
* @returns The authenticated user if token is valid, null otherwise
|
|
32
|
+
*/
|
|
33
|
+
export async function verifyToken(authHeader: string | undefined): Promise<AuthUser | null> {
|
|
34
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const token = authHeader.slice(7); // Remove 'Bearer ' prefix
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const payload = await verifier.verify(token);
|
|
42
|
+
return {
|
|
43
|
+
sub: payload.sub,
|
|
44
|
+
email: payload.email as string | undefined,
|
|
45
|
+
groups: payload['cognito:groups'] as string[] | undefined,
|
|
46
|
+
tokenPayload: payload,
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Token verification failed:', error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Middleware wrapper that requires authentication for a handler
|
|
56
|
+
*
|
|
57
|
+
* Wraps a handler function to automatically verify the JWT token
|
|
58
|
+
* and pass the authenticated user to the handler.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* export const handler = requireAuth(async (event, user) => {
|
|
63
|
+
* console.log('User ID:', user.sub);
|
|
64
|
+
* return successResponse({ userId: user.sub });
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @param handler - Handler function that receives the event and authenticated user
|
|
69
|
+
* @returns Wrapped handler that validates authentication first
|
|
70
|
+
*/
|
|
71
|
+
export function requireAuth<T>(
|
|
72
|
+
handler: (event: T, user: AuthUser) => Promise<unknown>
|
|
73
|
+
): (event: T) => Promise<unknown> {
|
|
74
|
+
return async (event: T) => {
|
|
75
|
+
const authHeader = (event as Record<string, Record<string, string>>).headers?.authorization ||
|
|
76
|
+
(event as Record<string, Record<string, string>>).headers?.Authorization;
|
|
77
|
+
|
|
78
|
+
const user = await verifyToken(authHeader);
|
|
79
|
+
|
|
80
|
+
if (!user) {
|
|
81
|
+
return errorResponse(
|
|
82
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
83
|
+
'Invalid or missing authentication token',
|
|
84
|
+
HTTP_STATUS.UNAUTHORIZED
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return handler(event, user);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DynamoModel, BaseModel, generateUUID } from '../lib/dynamo';
|
|
2
|
+
import type { User as UserType } from '{{PACKAGE_SCOPE}}/common-types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User interface extending BaseModel for DynamoDB
|
|
6
|
+
*/
|
|
7
|
+
export interface UserModel extends BaseModel {
|
|
8
|
+
email: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* User Model for DynamoDB operations
|
|
14
|
+
* Extends DynamoModel to provide user-specific functionality
|
|
15
|
+
*/
|
|
16
|
+
export class UserDynamoModel extends DynamoModel<UserModel> {
|
|
17
|
+
constructor(tableName?: string, region?: string) {
|
|
18
|
+
super(
|
|
19
|
+
tableName || process.env['DYNAMODB_TABLE'] || '{{PROJECT_NAME}}-table',
|
|
20
|
+
region || process.env['AWS_REGION'] || 'us-east-1'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get entity name for logging
|
|
26
|
+
*/
|
|
27
|
+
protected getEntityName(): string {
|
|
28
|
+
return 'User';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate unique ID for new users
|
|
33
|
+
*/
|
|
34
|
+
protected async generateId(data: UserModel): Promise<string> {
|
|
35
|
+
return generateUUID();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set GSI keys for user queries
|
|
40
|
+
* GSI1: Query users by email
|
|
41
|
+
* GSI2: Query users by creation date
|
|
42
|
+
*/
|
|
43
|
+
protected setGSIKeys(entity: UserModel, now: string): void {
|
|
44
|
+
// GSI1: For email lookups
|
|
45
|
+
entity.pk1 = 'USER';
|
|
46
|
+
entity.sk1 = `EMAIL#${entity.email}`;
|
|
47
|
+
|
|
48
|
+
// GSI2: For time-based queries
|
|
49
|
+
entity.pk2 = 'USER';
|
|
50
|
+
entity.sk2 = `CREATED#${now}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Transform UserModel to User type for API responses
|
|
55
|
+
*/
|
|
56
|
+
toUserType(userModel: UserModel): UserType {
|
|
57
|
+
return {
|
|
58
|
+
id: userModel.id,
|
|
59
|
+
email: userModel.email,
|
|
60
|
+
name: userModel.name,
|
|
61
|
+
createdAt: userModel.createdAt,
|
|
62
|
+
updatedAt: userModel.updatedAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get user by email
|
|
68
|
+
*/
|
|
69
|
+
async getByEmail(email: string): Promise<UserModel | null> {
|
|
70
|
+
try {
|
|
71
|
+
this.logger.info(`Getting user by email: ${email}`);
|
|
72
|
+
|
|
73
|
+
const results = await this.queryByGSI(
|
|
74
|
+
'GSI1',
|
|
75
|
+
'pk1 = :pk AND sk1 = :sk',
|
|
76
|
+
{
|
|
77
|
+
':pk': 'USER',
|
|
78
|
+
':sk': `EMAIL#${email}`,
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return results.length > 0 ? results[0] : null;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logger.error('Error getting user by email', { email, error });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get users created after a specific date
|
|
91
|
+
*/
|
|
92
|
+
async getByCreatedAfter(date: string): Promise<UserModel[]> {
|
|
93
|
+
try {
|
|
94
|
+
this.logger.info(`Getting users created after: ${date}`);
|
|
95
|
+
|
|
96
|
+
return await this.queryByGSI(
|
|
97
|
+
'GSI2',
|
|
98
|
+
'pk2 = :pk AND sk2 > :sk',
|
|
99
|
+
{
|
|
100
|
+
':pk': 'USER',
|
|
101
|
+
':sk': `CREATED#${date}`,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.logger.error('Error getting users by created date', { date, error });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { JSONSchemaType } from 'ajv';
|
|
2
|
+
import type { CreateUserRequest, UpdateUserRequest } from '{{PACKAGE_SCOPE}}/common-types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JSON Schema for CreateUserRequest
|
|
6
|
+
*/
|
|
7
|
+
export const createUserSchema: JSONSchemaType<CreateUserRequest> = {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
email: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
format: 'email',
|
|
13
|
+
minLength: 3,
|
|
14
|
+
maxLength: 255,
|
|
15
|
+
},
|
|
16
|
+
name: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
minLength: 1,
|
|
19
|
+
maxLength: 255,
|
|
20
|
+
pattern: '^(?!\\s*$)(?!.*\\s$)(?!^\\s).*$', // No leading/trailing whitespace, not empty
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ['email', 'name'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSON Schema for UpdateUserRequest
|
|
29
|
+
*/
|
|
30
|
+
export const updateUserSchema: JSONSchemaType<UpdateUserRequest> = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
name: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
minLength: 1,
|
|
36
|
+
maxLength: 255,
|
|
37
|
+
pattern: '^(?!\\s*$)(?!.*\\s$)(?!^\\s).*$', // No leading/trailing whitespace, not empty
|
|
38
|
+
nullable: true,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: [],
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
minProperties: 1, // At least one property must be present
|
|
44
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { User, CreateUserRequest, UpdateUserRequest } from '{{PACKAGE_SCOPE}}/common-types';
|
|
2
|
+
import { UserDynamoModel } from '../models/UserModel';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User Service
|
|
6
|
+
* Handles all business logic for user management with DynamoDB backend
|
|
7
|
+
*/
|
|
8
|
+
export class UserService {
|
|
9
|
+
private userModel: UserDynamoModel;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.userModel = new UserDynamoModel();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get all users
|
|
17
|
+
*/
|
|
18
|
+
async getAllUsers(): Promise<User[]> {
|
|
19
|
+
const users = await this.userModel.scanAll();
|
|
20
|
+
return users.map(user => this.userModel.toUserType(user));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get user by ID
|
|
25
|
+
*/
|
|
26
|
+
async getUserById(id: string): Promise<User | null> {
|
|
27
|
+
const user = await this.userModel.getById(id);
|
|
28
|
+
return user ? this.userModel.toUserType(user) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get user by email
|
|
33
|
+
*/
|
|
34
|
+
async getUserByEmail(email: string): Promise<User | null> {
|
|
35
|
+
const user = await this.userModel.getByEmail(email);
|
|
36
|
+
return user ? this.userModel.toUserType(user) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a new user
|
|
41
|
+
*/
|
|
42
|
+
async createUser(request: CreateUserRequest): Promise<User> {
|
|
43
|
+
// Check if user with email already exists
|
|
44
|
+
const existingUser = await this.userModel.getByEmail(request.email);
|
|
45
|
+
if (existingUser) {
|
|
46
|
+
throw new Error(`User with email ${request.email} already exists`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const user = await this.userModel.create({
|
|
50
|
+
email: request.email,
|
|
51
|
+
name: request.name,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return this.userModel.toUserType(user);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update an existing user
|
|
59
|
+
*/
|
|
60
|
+
async updateUser(id: string, request: UpdateUserRequest): Promise<User | null> {
|
|
61
|
+
const updatedUser = await this.userModel.update(id, {
|
|
62
|
+
...(request.name && { name: request.name }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return updatedUser ? this.userModel.toUserType(updatedUser) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Delete a user
|
|
70
|
+
*/
|
|
71
|
+
async deleteUser(id: string): Promise<boolean> {
|
|
72
|
+
return await this.userModel.delete(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delete multiple users
|
|
77
|
+
*/
|
|
78
|
+
async batchDeleteUsers(ids: string[]): Promise<{ success: string[]; failed: Array<{ id: string; error: string }> }> {
|
|
79
|
+
return await this.userModel.batchDelete(ids);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if user exists
|
|
84
|
+
*/
|
|
85
|
+
async userExists(id: string): Promise<boolean> {
|
|
86
|
+
const user = await this.userModel.getById(id);
|
|
87
|
+
return user !== null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get user count
|
|
92
|
+
*/
|
|
93
|
+
async getUserCount(): Promise<number> {
|
|
94
|
+
const users = await this.userModel.scanAll();
|
|
95
|
+
return users.length;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get users created after a specific date
|
|
100
|
+
*/
|
|
101
|
+
async getUsersCreatedAfter(date: string): Promise<User[]> {
|
|
102
|
+
const users = await this.userModel.getByCreatedAfter(date);
|
|
103
|
+
return users.map(user => this.userModel.toUserType(user));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Export singleton instance
|
|
108
|
+
export const userService = new UserService();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent } from 'aws-lambda';
|
|
2
|
+
import type { AuthUser } from '../middleware/cognito-auth';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get auth user from request context
|
|
6
|
+
* Used when auth is handled by API Gateway authorizer instead of middleware
|
|
7
|
+
*
|
|
8
|
+
* @param event - API Gateway proxy event with authorizer claims
|
|
9
|
+
* @returns AuthUser if claims present in context, null otherwise
|
|
10
|
+
*/
|
|
11
|
+
export function getAuthUserFromContext(event: APIGatewayProxyEvent): AuthUser | null {
|
|
12
|
+
const claims = event.requestContext?.authorizer?.claims;
|
|
13
|
+
|
|
14
|
+
if (!claims) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
sub: claims.sub,
|
|
20
|
+
email: claims.email,
|
|
21
|
+
groups: claims['cognito:groups']?.split(','),
|
|
22
|
+
tokenPayload: claims as AuthUser['tokenPayload'],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if user has required group membership
|
|
28
|
+
*
|
|
29
|
+
* @param user - Authenticated user
|
|
30
|
+
* @param groupName - Name of the Cognito group to check
|
|
31
|
+
* @returns true if user belongs to the specified group
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* if (hasGroup(user, 'admins')) {
|
|
36
|
+
* // User is an admin
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function hasGroup(user: AuthUser, groupName: string): boolean {
|
|
41
|
+
return user.groups?.includes(groupName) ?? false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if user is the owner of a resource
|
|
46
|
+
*
|
|
47
|
+
* @param user - Authenticated user
|
|
48
|
+
* @param resourceOwnerId - The owner ID stored on the resource
|
|
49
|
+
* @returns true if the user's sub matches the resource owner ID
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* if (!isOwner(user, document.ownerId)) {
|
|
54
|
+
* return errorResponse(ERROR_CODES.FORBIDDEN, 'Not authorized');
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function isOwner(user: AuthUser, resourceOwnerId: string): boolean {
|
|
59
|
+
return user.sub === resourceOwnerId;
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Remove GSI (Global Secondary Index) fields from an entity
|
|
7
|
+
* Useful for cleaning data before returning to clients
|
|
8
|
+
*/
|
|
9
|
+
export function removeGSIFields<T extends Record<string, any>>(entity: T): Omit<T, 'pk1' | 'sk1' | 'pk2' | 'sk2' | 'pk3' | 'sk3' | 'pk4' | 'sk4' | 'pk5' | 'sk5' | 'pk6' | 'sk6'> {
|
|
10
|
+
const { pk1, sk1, pk2, sk2, pk3, sk3, pk4, sk4, pk5, sk5, pk6, sk6, ...rest } = entity;
|
|
11
|
+
return rest as Omit<T, 'pk1' | 'sk1' | 'pk2' | 'sk2' | 'pk3' | 'sk3' | 'pk4' | 'sk4' | 'pk5' | 'sk5' | 'pk6' | 'sk6'>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a unique ID
|
|
16
|
+
*/
|
|
17
|
+
export function generateUUID(): string {
|
|
18
|
+
return crypto.randomUUID();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get current ISO timestamp
|
|
23
|
+
*/
|
|
24
|
+
export function getCurrentTimestamp(): string {
|
|
25
|
+
return new Date().toISOString();
|
|
26
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiGatewayProxyEvent,
|
|
3
|
+
ApiGatewayProxyResult,
|
|
4
|
+
} from '{{PACKAGE_SCOPE}}/common-types';
|
|
5
|
+
import { HTTP_STATUS, ERROR_CODES } from '{{PACKAGE_SCOPE}}/common-types';
|
|
6
|
+
import { errorResponse } from './response';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parsed request data from API Gateway event
|
|
10
|
+
*/
|
|
11
|
+
export interface ParsedRequest<TBody = unknown> {
|
|
12
|
+
pathParameters: Record<string, string>;
|
|
13
|
+
queryParameters: Record<string, string>;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
body: TBody | null;
|
|
16
|
+
rawBody: string | null;
|
|
17
|
+
httpMethod: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handler function type that receives parsed request data
|
|
23
|
+
*/
|
|
24
|
+
export type HandlerFunction<TBody = unknown, TResponse = unknown> = (
|
|
25
|
+
request: ParsedRequest<TBody>
|
|
26
|
+
) => Promise<TResponse>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse the API Gateway event into a more convenient format
|
|
30
|
+
*/
|
|
31
|
+
export function parseRequest<TBody = unknown>(
|
|
32
|
+
event: ApiGatewayProxyEvent
|
|
33
|
+
): ParsedRequest<TBody> {
|
|
34
|
+
const pathParameters = event.pathParameters || {};
|
|
35
|
+
const queryParameters = event.queryStringParameters || {};
|
|
36
|
+
const headers = event.headers || {};
|
|
37
|
+
const rawBody = event.body;
|
|
38
|
+
|
|
39
|
+
let body: TBody | null = null;
|
|
40
|
+
if (rawBody) {
|
|
41
|
+
try {
|
|
42
|
+
body = JSON.parse(rawBody) as TBody;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Body parsing will be handled by validation in handlers
|
|
45
|
+
body = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
pathParameters,
|
|
51
|
+
queryParameters,
|
|
52
|
+
headers,
|
|
53
|
+
body,
|
|
54
|
+
rawBody,
|
|
55
|
+
httpMethod: event.httpMethod,
|
|
56
|
+
path: event.path,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate that required path parameters are present
|
|
62
|
+
*/
|
|
63
|
+
export function validatePathParameters(
|
|
64
|
+
pathParameters: Record<string, string>,
|
|
65
|
+
required: string[]
|
|
66
|
+
): { valid: boolean; missing?: string[] } {
|
|
67
|
+
const missing = required.filter(param => !pathParameters[param]);
|
|
68
|
+
|
|
69
|
+
if (missing.length > 0) {
|
|
70
|
+
return { valid: false, missing };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { valid: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate that request body is present
|
|
78
|
+
*/
|
|
79
|
+
export function validateBodyPresent(
|
|
80
|
+
body: unknown,
|
|
81
|
+
rawBody: string | null
|
|
82
|
+
): { valid: boolean; error?: string } {
|
|
83
|
+
if (!rawBody) {
|
|
84
|
+
return { valid: false, error: 'Request body is required' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!body) {
|
|
88
|
+
return { valid: false, error: 'Invalid JSON in request body' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { valid: true };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wrapper for Lambda handlers that handles common request parsing and error handling
|
|
96
|
+
*
|
|
97
|
+
* @param handlerFn - The handler function to execute
|
|
98
|
+
* @param handlerName - Name of the handler for logging purposes
|
|
99
|
+
*/
|
|
100
|
+
export function createLambdaHandler<TBody = unknown, TResponse = unknown>(
|
|
101
|
+
handlerFn: HandlerFunction<TBody, TResponse>,
|
|
102
|
+
handlerName: string
|
|
103
|
+
) {
|
|
104
|
+
return async (event: ApiGatewayProxyEvent): Promise<ApiGatewayProxyResult> => {
|
|
105
|
+
console.log(`[${handlerName}] Event:`, JSON.stringify(event, null, 2));
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const parsedRequest = parseRequest<TBody>(event);
|
|
109
|
+
|
|
110
|
+
console.log(`[${handlerName}] Parsed request:`, {
|
|
111
|
+
pathParameters: parsedRequest.pathParameters,
|
|
112
|
+
queryParameters: parsedRequest.queryParameters,
|
|
113
|
+
httpMethod: parsedRequest.httpMethod,
|
|
114
|
+
path: parsedRequest.path,
|
|
115
|
+
hasBody: !!parsedRequest.body,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = await handlerFn(parsedRequest);
|
|
119
|
+
return result as ApiGatewayProxyResult;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`[${handlerName}] Error:`, error);
|
|
122
|
+
|
|
123
|
+
// If error is already an ApiGatewayProxyResult, return it
|
|
124
|
+
if (error && typeof error === 'object' && 'statusCode' in error && 'body' in error) {
|
|
125
|
+
return error as ApiGatewayProxyResult;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Otherwise, return a generic error
|
|
129
|
+
return errorResponse(
|
|
130
|
+
ERROR_CODES.INTERNAL_ERROR,
|
|
131
|
+
`Failed to process request in ${handlerName}`,
|
|
132
|
+
HTTP_STATUS.INTERNAL_SERVER_ERROR
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create an error result that can be thrown and caught by the handler wrapper
|
|
140
|
+
*/
|
|
141
|
+
export function createErrorResult(
|
|
142
|
+
code: string,
|
|
143
|
+
message: string,
|
|
144
|
+
statusCode: (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS],
|
|
145
|
+
details?: Record<string, unknown>
|
|
146
|
+
): ApiGatewayProxyResult {
|
|
147
|
+
return errorResponse(code, message, statusCode, details);
|
|
148
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiGatewayProxyResult,
|
|
3
|
+
ApiResponse,
|
|
4
|
+
HTTP_STATUS,
|
|
5
|
+
} from '{{PACKAGE_SCOPE}}/common-types';
|
|
6
|
+
|
|
7
|
+
const defaultHeaders = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
'Access-Control-Allow-Origin': process.env['ALLOWED_ORIGINS'] || '*',
|
|
10
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
11
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function successResponse<T>(
|
|
15
|
+
data: T,
|
|
16
|
+
statusCode: (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS] = 200,
|
|
17
|
+
message?: string
|
|
18
|
+
): ApiGatewayProxyResult {
|
|
19
|
+
const response: ApiResponse<T> = {
|
|
20
|
+
success: true,
|
|
21
|
+
data,
|
|
22
|
+
message,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
statusCode,
|
|
27
|
+
headers: defaultHeaders,
|
|
28
|
+
body: JSON.stringify(response),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function errorResponse(
|
|
33
|
+
code: string,
|
|
34
|
+
message: string,
|
|
35
|
+
statusCode: (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS] = 500,
|
|
36
|
+
details?: Record<string, unknown>
|
|
37
|
+
): ApiGatewayProxyResult {
|
|
38
|
+
const response: ApiResponse = {
|
|
39
|
+
success: false,
|
|
40
|
+
error: {
|
|
41
|
+
code,
|
|
42
|
+
message,
|
|
43
|
+
details,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
statusCode,
|
|
49
|
+
headers: defaultHeaders,
|
|
50
|
+
body: JSON.stringify(response),
|
|
51
|
+
};
|
|
52
|
+
}
|