From d9622ea451b2216741a0a28536c6071a7fbb2778 Mon Sep 17 00:00:00 2001 From: lucalise Date: Wed, 21 Jan 2026 23:03:22 -0800 Subject: [PATCH] feat: deploy to vercel & use ALB on ecs --- .gitignore | 1 + .vercelignore | 6 +++ api/Cargo.lock | 28 ++++++++++ api/Cargo.toml | 2 +- api/src/endpoints/proxy_file.rs | 4 +- api/src/main.rs | 2 +- api/src/user.rs | 2 +- cdk/cdk.context.json | 6 ++- cdk/lib/cdk-stack.ts | 90 +++++++++++++-------------------- src/lib/auth-client.ts | 2 +- vercel.json | 8 +++ 11 files changed, 89 insertions(+), 62 deletions(-) create mode 100644 .vercelignore create mode 100644 vercel.json diff --git a/.gitignore b/.gitignore index 20ffa1d..b7ff52a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* +.env*.local diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..80b3b5e --- /dev/null +++ b/.vercelignore @@ -0,0 +1,6 @@ +node_modules +.svelte-kit +.git +.vercel +api +cdk diff --git a/api/Cargo.lock b/api/Cargo.lock index b8ddfa7..a362485 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -1396,6 +1396,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1416,6 +1427,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -2932,6 +2944,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", @@ -2954,12 +2967,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls 0.26.4", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -4395,6 +4410,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.83" diff --git a/api/Cargo.toml b/api/Cargo.toml index 9f54317..ae766b5 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -11,7 +11,7 @@ aws-sdk-dynamodb = "1.102.0" chrono = { version = "0.4.43", features = ["serde"] } jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] } mime_guess = "2.0.5" -reqwest = { version = "0.13.1", features = ["json", "query"] } +reqwest = { version = "0.13.1", features = ["json", "query", "stream"] } sentry = { version = "0.46.1", features = ["actix", "tracing"] } serde = "1.0.228" serde_dynamo = { version = "4.3.0", features = ["aws-sdk-dynamodb+1"] } diff --git a/api/src/endpoints/proxy_file.rs b/api/src/endpoints/proxy_file.rs index 5516410..572adfe 100644 --- a/api/src/endpoints/proxy_file.rs +++ b/api/src/endpoints/proxy_file.rs @@ -37,7 +37,7 @@ pub async fn proxy_file( .error_for_status_ref() .map_err(|_| crate::error::Error::NotFound)?; - let bytes = response.bytes().await?; + let stream = response.bytes_stream(); let mime = mime_guess::from_path(&path.file) .first_or_octet_stream() .to_string(); @@ -45,5 +45,5 @@ pub async fn proxy_file( Ok(HttpResponse::Ok() .content_type(mime) .insert_header(("Cache-Control", "public, max-age=3600")) - .body(bytes)) + .streaming(stream)) } diff --git a/api/src/main.rs b/api/src/main.rs index cec5305..7b9a640 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -98,7 +98,7 @@ async fn run() -> std::io::Result<()> { ), ) }) - .bind(("0.0.0.0", 8080))? + .bind((env::var("ADDRESS").unwrap_or("0.0.0.0".to_string()), 8080))? .run() .await } diff --git a/api/src/user.rs b/api/src/user.rs index 66427af..a39f408 100644 --- a/api/src/user.rs +++ b/api/src/user.rs @@ -169,7 +169,7 @@ impl UserRepository { .item("gsi1pk", AttributeValue::S("REPOS".into())) .item("gsi1sk", AttributeValue::S(now.clone())) .item("imported_at", AttributeValue::S(now)) - .item("approved", AttributeValue::Bool(false)) + .item("approved", AttributeValue::Bool(true)) .item("owner_id", AttributeValue::S(user_id.into())) .item("description", AttributeValue::S(repo.description.clone())) .send() diff --git a/cdk/cdk.context.json b/cdk/cdk.context.json index 72f7b8a..5a45953 100644 --- a/cdk/cdk.context.json +++ b/cdk/cdk.context.json @@ -3,5 +3,9 @@ "ca-central-1a", "ca-central-1b", "ca-central-1d" - ] + ], + "hosted-zone:account=585061171043:domainName=lucalise.ca:region=ca-central-1": { + "Id": "/hostedzone/Z0948300LINP3SX1WI4O", + "Name": "lucalise.ca." + } } diff --git a/cdk/lib/cdk-stack.ts b/cdk/lib/cdk-stack.ts index c8a0056..1ee0450 100644 --- a/cdk/lib/cdk-stack.ts +++ b/cdk/lib/cdk-stack.ts @@ -4,8 +4,10 @@ import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import * as ecs from "aws-cdk-lib/aws-ecs"; import * as ssm from "aws-cdk-lib/aws-ssm" -import * as servicediscovery from "aws-cdk-lib/aws-servicediscovery"; -import * as apigatewayv2 from "aws-cdk-lib/aws-apigatewayv2"; +import * as acm from "aws-cdk-lib/aws-certificatemanager" +import * as route53 from "aws-cdk-lib/aws-route53" +import * as route53Targets from "aws-cdk-lib/aws-route53-targets" +import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2" import * as path from "path"; interface StackProps extends cdk.StackProps { @@ -26,6 +28,9 @@ export class GhostV2Stack extends cdk.Stack { partitionKey: { name: "gsi1pk", type: dynamodb.AttributeType.STRING }, sortKey: { name: "gsi1sk", type: dynamodb.AttributeType.STRING }, }); + if (props.environment !== "prod") { + return; + } const vpc = new ec2.Vpc(this, "ghostv2 vpc", { maxAzs: 2, @@ -45,22 +50,24 @@ export class GhostV2Stack extends cdk.Stack { allowAllOutbound: true, }); ecsSecurityGroup.addIngressRule( - ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Peer.anyIpv4(), + ec2.Port.tcp(80), + "Allow HTTP" + ); + ecsSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(443), + "Allow HTTPS" + ); + ecsSecurityGroup.addIngressRule( + ec2.Peer.anyIpv4(), ec2.Port.tcp(8080), - "Allow traffic from VPC" + "Allow API" ); const cluster = new ecs.Cluster(this, "ghostv2 cluster", { vpc, }); - const namespace = new servicediscovery.PrivateDnsNamespace( - this, - "ghostv2 namespace", - { - name: "ghostv2.local", - vpc, - } - ); const taskDefinition = new ecs.FargateTaskDefinition( this, "ghostv2 api", @@ -73,9 +80,7 @@ export class GhostV2Stack extends cdk.Stack { }, } ); - table.grantReadWriteData(taskDefinition.taskRole); - - const secretImports = ["DATABASE_URL", "GH_CLIENT_ID", "GH_CLIENT_SECRET", "REPOS_TABLE_NAME", "SENTRY_DSN"]; + const secretImports = ["DATABASE_URL", "GH_CLIENT_ID", "GH_CLIENT_SECRET", "REPOS_TABLE_NAME", "SENTRY_DSN", "FRONTEND_BASE_URL"]; const secrets = secretImports.reduce>((acc, name) => { acc[name] = ecs.Secret.fromSsmParameter(ssm.StringParameter.fromSecureStringParameterAttributes(this, `param${name}`, { parameterName: `/ghostv2/${props.environment}/${name}` @@ -95,7 +100,7 @@ export class GhostV2Stack extends cdk.Stack { ], secrets }); - + table.grantReadWriteData(taskDefinition.taskRole); const service = new ecs.FargateService(this, "ghostv2 service", { cluster, taskDefinition, @@ -111,46 +116,21 @@ export class GhostV2Stack extends cdk.Stack { weight: 1, }, ], - cloudMapOptions: { - name: "api", - cloudMapNamespace: namespace, - dnsRecordType: servicediscovery.DnsRecordType.SRV, - containerPort: 8080, - dnsTtl: cdk.Duration.seconds(10), - }, }); - - const vpcLink = new apigatewayv2.CfnVpcLink(this, "ghostv2 vpc link", { - name: "ghostv2-vpc-link", - subnetIds: vpc.publicSubnets.map((s) => s.subnetId), - securityGroupIds: [ecsSecurityGroup.securityGroupId], - }); - - const httpApi = new apigatewayv2.CfnApi(this, "ghostv2 http-api", { - name: "ghostv2-api", - protocolType: "HTTP", - }); - - const integration = new apigatewayv2.CfnIntegration(this, "Integration", { - apiId: httpApi.ref, - integrationType: "HTTP_PROXY", - integrationMethod: "ANY", - integrationUri: service.cloudMapService!.serviceArn, - connectionType: "VPC_LINK", - connectionId: vpcLink.ref, - payloadFormatVersion: "1.0", - }); - - new apigatewayv2.CfnRoute(this, "DefaultRoute", { - apiId: httpApi.ref, - routeKey: "$default", - target: `integrations/${integration.ref}`, - }); - - new apigatewayv2.CfnStage(this, "Stage", { - apiId: httpApi.ref, - stageName: "$default", - autoDeploy: true, + const hostedZone = route53.HostedZone.fromLookup(this, "hosted zone", { domainName: "lucalise.ca" }) + const certificate = new acm.Certificate(this, "ghostv2 api cert", { + domainName: "api.ghostv2.lucalise.ca", + validation: acm.CertificateValidation.fromDns(hostedZone) + }) + const alb = new elbv2.ApplicationLoadBalancer(this, "ghostv2 alb", { + vpc, + internetFacing: true, + securityGroup: ecsSecurityGroup, }); + alb.setAttribute("idle_timeout.timeout_seconds", "120") + alb.addRedirect({ sourcePort: 80, sourceProtocol: elbv2.ApplicationProtocol.HTTP, targetPort: 443, targetProtocol: elbv2.ApplicationProtocol.HTTPS }) + const listener = alb.addListener("https listener", { port: 443, open: true, certificates: [certificate] }) + listener.addTargets("ECSTargets", { port: 8080, protocol: elbv2.ApplicationProtocol.HTTP, targets: [service], healthCheck: { path: "/", port: "8080" } }) + new route53.ARecord(this, "alb record", { zone: hostedZone, recordName: "api.ghostv2.lucalise.ca", target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)) }) } } diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts index 4ad6455..4c518f8 100644 --- a/src/lib/auth-client.ts +++ b/src/lib/auth-client.ts @@ -2,6 +2,6 @@ import { jwtClient, adminClient } from 'better-auth/client/plugins'; import { createAuthClient } from 'better-auth/svelte'; export const authClient = createAuthClient({ - baseURL: 'http://localhost:5173', + baseURL: process.env.BETTER_AUTH_URL!, plugins: [jwtClient(), adminClient()] }); diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..49b539e --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/api/v0/:path*", + "destination": "https://s8do6ipb07.execute-api.ca-central-1.amazonaws.com/api/v0/:path*" + } + ] +}