import * as pulumi from '@pulumi/pulumi' import * as aws from '@pulumi/aws' import * as awsx from '@pulumi/awsx' import * as cloud from '@pulumi/cloud' import { getType } from 'mime' import { readdirSync, lstatSync } from 'fs' import { join } from 'path' import { handle } from '@hcengineering/dev-account' const siteBucket = new aws.s3.Bucket('anticrm-app', { acl: "public-read", website: { indexDocument: 'index.html' } }) const uploadBucket = new aws.s3.Bucket('anticrm-upload', { acl: "public-read" }) const buildDir = "../../dev/prod" function createObjects(root: string, path: string): void { const dir = join(root, path) for (const file of readdirSync(dir)) { const relative = join(path, file) const absolute = join(root, relative) if (lstatSync(absolute).isDirectory()) { createObjects(root, relative) } else { new aws.s3.BucketObject(relative, { acl: 'public-read', bucket: siteBucket, source: new pulumi.asset.FileAsset(absolute), // use FileAsset to point to a file contentType: getType(absolute) || undefined, // set the MIME type of the file }) } } } createObjects(buildDir + '/public', '') createObjects(buildDir + '/dist', '') export const bucketName = siteBucket.bucket // create a stack export for bucket name export const websiteUrl = siteBucket.websiteEndpoint export const uploadBucketName = uploadBucket.bucket // // Create an S3 Bucket Policy to allow public read of all objects in bucket // // This reusable function can be pulled out into its own module // function publicReadPolicyForBucket(bucketName: string) { // return JSON.stringify({ // Version: "2012-10-17", // Statement: [{ // Effect: "Allow", // Principal: "*", // Action: [ // "s3:GetObject" // ], // Resource: [ // `arn:aws:s3:::${bucketName}/*` // policy refers to bucket name explicitly // ] // }] // }) // } // // Set the access policy for the bucket so all objects are readable // new aws.s3.BucketPolicy('bucketPolicy', { // bucket: siteBucket.bucket, // depends on siteBucket -- see explanation below // policy: siteBucket.bucket.apply(publicReadPolicyForBucket) // // transform the siteBucket.bucket output property -- see explanation below // }); // D O C K E R // const service = new cloud.Service("dev-server", { // containers: { // server: { // build: "./dev-server", // memory: 128, // ports: [{ port: 3333 }], // }, // }, // replicas: 1, // }) // export const serverEndpoint = service.defaultEndpoint.hostname // // L O G I N // // Define a new GET endpoint that just returns a 200 and "hello" in the body. const api = new awsx.apigateway.API("login", { routes: [ { path: "/", method: "POST", eventHandler: async (event) => { let body = event.body; if (event.isBase64Encoded) { body = Buffer.from(body as string, 'base64').toString() } // const result = handle(body, 'wss://pacific-refuge-43514.herokuapp.com/') const result = handle(body, 'wss://transactor.hc.engineering/') return { statusCode: result.statusCode, headers: { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST" }, body: result.body } }, }, { path: "/", method: "OPTIONS", eventHandler: async (event) => { console.log('OPTIONS call') return { statusCode: 200, headers: { "Access-Control-Allow-Headers" : "Content-Type", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "OPTIONS,POST" }, body: '' } }, }, ], }) // Export the auto-generated API Gateway base URL. export const url = api.url // // D N S // // Get the hosted zone by domain name const zoneId = aws.route53.getZone({ name: "hc.engineering." }).then(zone => zone.zoneId) const tenMinutes = 60 * 10 // Per AWS, ACM certificate must be in the us-east-1 region. const eastRegion = new aws.Provider("east", { profile: aws.config.profile, region: "us-east-1", }) const certificate = new aws.acm.Certificate( "certificate", { domainName: "app.hc.engineering", validationMethod: "DNS", }, { provider: eastRegion } ); const certificateValidationDomain = new aws.route53.Record( "app.hc.engineering-validation", { name: certificate.domainValidationOptions[0].resourceRecordName, zoneId: zoneId, type: certificate.domainValidationOptions[0].resourceRecordType, records: [certificate.domainValidationOptions[0].resourceRecordValue], ttl: tenMinutes, } ); const certificateValidation = new aws.acm.CertificateValidation( "certificateValidation", { certificateArn: certificate.arn, validationRecordFqdns: [certificateValidationDomain.fqdn], }, { provider: eastRegion } ) // distributionArgs configures the CloudFront distribution. Relevant documentation: // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html // https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html const distributionArgs: aws.cloudfront.DistributionArgs = { enabled: true, // Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net. // Required if you want to access the distribution via config.targetDomain as well. aliases: ["app.hc.engineering"], // We only specify one origin for this distribution, the S3 content bucket. origins: [ { originId: siteBucket.arn, domainName: siteBucket.websiteEndpoint, customOriginConfig: { // Amazon S3 doesn't support HTTPS connections when using an S3 bucket configured as a website endpoint. // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy originProtocolPolicy: "http-only", httpPort: 80, httpsPort: 443, originSslProtocols: ["TLSv1.2"], }, }, ], defaultRootObject: "index.html", // A CloudFront distribution can configure different cache behaviors based on the request path. // Here we just specify a single, default cache behavior which is just read-only requests to S3. defaultCacheBehavior: { targetOriginId: siteBucket.arn, viewerProtocolPolicy: "redirect-to-https", allowedMethods: ["GET", "HEAD", "OPTIONS"], cachedMethods: ["GET", "HEAD", "OPTIONS"], forwardedValues: { cookies: { forward: "none" }, queryString: false, }, minTtl: 0, defaultTtl: tenMinutes, maxTtl: tenMinutes, }, // "All" is the most broad distribution, and also the most expensive. // "100" is the least broad, and also the least expensive. priceClass: "PriceClass_100", // You can customize error responses. When CloudFront receives an error from the origin (e.g. S3 or some other // web service) it can return a different error code, and return the response for a different resource. customErrorResponses: [ { errorCode: 404, responseCode: 200, responsePagePath: "/index.html" }, ], restrictions: { geoRestriction: { restrictionType: "none", }, }, viewerCertificate: { acmCertificateArn: certificateValidation.certificateArn, // Per AWS, ACM certificate must be in the us-east-1 region. sslSupportMethod: "sni-only", }, } const cdn = new aws.cloudfront.Distribution("cdn", distributionArgs) // Create a Route53 A-record new aws.route53.Record("targetDomain", { name: "app.hc.engineering", zoneId: zoneId, type: "A", aliases: [{ zoneId: cdn.hostedZoneId, name: cdn.domainName, evaluateTargetHealth: true, }], }) new aws.route53.Record("databaseRecord", { name: "db.hc.engineering", zoneId: zoneId, type: "CNAME", ttl: 300, records: [ "xored.com" ] }) new aws.route53.Record("frontRecord", { name: "front.hc.engineering", zoneId: zoneId, type: "A", ttl: 300, records: [ "107.191.38.121" ] }) new aws.route53.Record("transactorRecord", { name: "transactor.hc.engineering", zoneId: zoneId, type: "A", ttl: 300, records: [ "107.191.38.121" ] }) new aws.route53.Record("uploadRecord", { name: "upload.hc.engineering", zoneId: zoneId, type: "A", ttl: 300, records: [ "107.191.38.121" ] }) new aws.route53.Record("accountRecord", { name: "account.hc.engineering", zoneId: zoneId, type: "A", ttl: 300, records: [ "107.191.38.121" ] }) new aws.route53.Record("telegramRecord", { name: "telegram.hc.engineering", zoneId: zoneId, type: "A", ttl: 300, records: [ "107.191.38.121" ] }) new aws.route53.Record("rekoniRecord", { name: "rekoni.hc.engineering", zoneId: zoneId, type: "A", ttl: 300, records: [ "107.191.38.121" ] })