import cdk = require('@aws-cdk/core'); import lambda = require('@aws-cdk/aws-lambda'); import apigw = require('@aws-cdk/aws-apigateway'); import cognito = require('@aws-cdk/aws-cognito'); import * as S3 from '@aws-cdk/aws-s3'; import * as IAM from '@aws-cdk/aws-iam'; import * as route53 from '@aws-cdk/aws-route53'; import { CloudFrontWebDistribution, OriginAccessIdentity, CloudFrontAllowedMethods, SSLMethod, CloudFrontWebDistributionProps } from '@aws-cdk/aws-cloudfront'; import { VerificationEmailStyle, OAuthScope, CfnUserPool } from '@aws-cdk/aws-cognito'; import { StackOutput } from './stackoutput'; import { Duration } from '@aws-cdk/core'; import { Config } from '../../../scripts/config'; export class BaseStack extends cdk.Stack { bucket: S3.Bucket; distribution: CloudFrontWebDistribution; endpoint: apigw.LambdaRestApi; userPool: cognito.UserPool; userPoolClient: cognito.UserPoolClient; apiFunction: lambda.Function; constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); this.cognito(); this.backend(); this.frontend(); this.outputs(); } frontend() { this.bucket = new S3.Bucket(this, Config.instance.appEnv + '-hosting-bucket', { bucketName: Config.instance.appEnv + '-hosting-bucket', websiteIndexDocument: 'index.html', websiteErrorDocument: 'index.html', removalPolicy: cdk.RemovalPolicy.DESTROY, }); let oai = new OriginAccessIdentity(this, Config.instance.appEnv + '-oai'); let distributionProps: CloudFrontWebDistributionProps = { comment: Config.instance.appEnv + ' distribution', aliasConfiguration: Config.instance.domain ? { names: [Config.instance.subdomain], acmCertRef: Config.instance.certificateArn, sslMethod: SSLMethod.SNI } : undefined, originConfigs: [{ s3OriginSource: { s3BucketSource: this.bucket, originAccessIdentity: oai, }, behaviors: [{ isDefaultBehavior: true }], }, { customOriginSource: { domainName: `${this.endpoint.restApiId}.execute-api.${this.region}.${this.urlSuffix}` }, originPath: `/${this.endpoint.deploymentStage.stageName}`, behaviors: [{ pathPattern: '/api/*', allowedMethods: CloudFrontAllowedMethods.ALL, defaultTtl: Duration.millis(0), maxTtl: Duration.millis(0), minTtl: Duration.millis(0), forwardedValues: { queryString: true, cookies: { forward: 'all' }, // This doesn't work - bug in CDK? // headers: ['*'], } }] }] } this.distribution = new CloudFrontWebDistribution(this, Config.instance.appEnv + '-distribution', distributionProps); if (Config.instance.domain){ new route53.CfnRecordSet(this, Config.instance.appEnv + '-recordset', { hostedZoneName: Config.instance.domain + '.', name: Config.instance.subdomain, type: 'A', aliasTarget: { hostedZoneId: 'Z2FDTNDATAQYW2', // cloudfront.net dnsName: this.distribution.domainName } }) } this.bucket.addToResourcePolicy(new IAM.PolicyStatement({ effect: IAM.Effect.ALLOW, resources: [ this.bucket.bucketArn, this.bucket.bucketArn + '/*' ], actions: [ 's3:GetBucket*', 's3:GetObject*', 's3:List*', ], principals: [ new IAM.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId) ] })); } backend() { this.apiFunction = new lambda.Function(this, Config.instance.appEnv + '-api', { functionName: Config.instance.appEnv + '-api', code: lambda.Code.asset('../api/dist'), runtime: lambda.Runtime.NODEJS_10_X, handler: 'generic/handler.handler', environment: { } }); this.apiFunction.addToRolePolicy(new IAM.PolicyStatement({ actions: ['cognito-idp:*'], resources: [this.userPool.userPoolArn] })); this.endpoint = new apigw.LambdaRestApi(this, Config.instance.appEnv + '-endpoint', { restApiName: Config.instance.appEnv + '-endpoint', handler: this.apiFunction, proxy: true }) } cognito(){ this.userPool = new cognito.UserPool(this, Config.instance.appEnv + '-user-pool', { userPoolName: Config.instance.appEnv + '-userpool', selfSignUpEnabled: true, signInAliases: { email: true }, autoVerify: { email: true }, userVerification: { emailSubject: 'Verify your email for ' + Config.instance.app, emailBody: `Your verification code is: <h1>{####}</h1>`, emailStyle: VerificationEmailStyle.CODE, }, passwordPolicy: { minLength: 8, requireDigits: true, requireLowercase: true, requireUppercase: true, requireSymbols: false }, }); if (Config.instance.cognitoEmailArn){ const cfnUserPool = this.userPool.node.defaultChild as CfnUserPool; cfnUserPool.emailConfiguration = { emailSendingAccount: 'DEVELOPER', replyToEmailAddress: 'noreply@' + Config.instance.domain, from: 'noreply@' + Config.instance.domain, sourceArn: Config.instance.cognitoEmailArn }; } this.userPoolClient = new cognito.UserPoolClient(this, Config.instance.appEnv + 'user-pool-client', { userPoolClientName: Config.instance.appEnv + 'user-pool-client', authFlows: { adminUserPassword: true, refreshToken: true }, generateSecret: false, userPool: this.userPool, oAuth: { flows: { authorizationCodeGrant: true, implicitCodeGrant: true }, scopes: [OAuthScope.OPENID, OAuthScope.COGNITO_ADMIN] } }) const cfnUserPoolClient = this.userPoolClient.node.defaultChild as cognito.CfnUserPoolClient; cfnUserPoolClient.supportedIdentityProviders = ['COGNITO']; new cognito.UserPoolDomain(this, Config.instance.appEnv + '-user-pool-domain', { userPool: this.userPool, cognitoDomain: { domainPrefix: Config.instance.appEnv } }); } outputs() { let outputs: StackOutput = { DistributionUri: 'https://' + this.distribution.domainName, DistributionId: this.distribution.distributionId, HostingBucket: 's3://' + this.bucket.bucketName, UserPoolId: this.userPool.userPoolId, UserPoolClientId: this.userPoolClient.userPoolClientId, FunctionName: this.apiFunction.functionName, EndpointUrl: this.endpoint.url, } Object.keys(outputs).forEach((k) => new cdk.CfnOutput(this, k, {value: outputs[k as keyof StackOutput]})); } } const app = new cdk.App(); new BaseStack(app, Config.instance.appEnv);