Skip to content

Commit 25bafc0

Browse files
committed
switch to AWS localstack; mock ACM integration; add certificate DNS values; show DNS configs on territory edit
1 parent d906afa commit 25bafc0

File tree

15 files changed

+125
-55
lines changed

15 files changed

+125
-55
lines changed

Diff for: .env.development

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
170170
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
171171
PERSISTENCE=1
172172
SKIP_SSL_CERT_DOWNLOAD=1
173+
LOCALSTACK_ENDPOINT=http://localhost:4566
173174

174175
# tor proxy
175176
TOR_PROXY=http://tor:7050/

Diff for: api/acm/index.js

+28-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
import { ACM } from 'aws-sdk'
2-
// TODO: skeleton
1+
import AWS from 'aws-sdk'
2+
// TODO: boilerplate
33

4-
const region = 'us-east-1' // cloudfront ACM is in us-east-1
5-
const acm = new ACM({ region })
4+
AWS.config.update({
5+
region: 'us-east-1'
6+
})
7+
8+
const config = {
9+
s3ForcePathStyle: process.env.NODE_ENV === 'development'
10+
}
611

712
export async function requestCertificate (domain) {
13+
// for local development, we use the LOCALSTACK_ENDPOINT which
14+
// is reachable from the host machine
15+
if (process.env.NODE_ENV === 'development') {
16+
config.endpoint = process.env.LOCALSTACK_ENDPOINT
17+
}
18+
19+
const acm = new AWS.ACM(config)
820
const params = {
921
DomainName: domain,
1022
ValidationMethod: 'DNS',
@@ -20,7 +32,18 @@ export async function requestCertificate (domain) {
2032
return certificate.CertificateArn
2133
}
2234

23-
export async function getCertificateStatus (certificateArn) {
35+
export async function describeCertificate (certificateArn) {
36+
// for local development, we use the LOCALSTACK_ENDPOINT which
37+
// is reachable from the host machine
38+
if (process.env.NODE_ENV === 'development') {
39+
config.endpoint = process.env.LOCALSTACK_ENDPOINT
40+
}
41+
const acm = new AWS.ACM(config)
2442
const certificate = await acm.describeCertificate({ CertificateArn: certificateArn }).promise()
43+
return certificate
44+
}
45+
46+
export async function getCertificateStatus (certificateArn) {
47+
const certificate = await describeCertificate(certificateArn)
2548
return certificate.Certificate.Status
2649
}

Diff for: api/resolvers/sub.js

-1
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,6 @@ export default {
312312
data: {
313313
domain,
314314
dnsState: 'PENDING',
315-
cname: 'todo', // TODO: explore other options
316315
verificationTxt: randomBytes(32).toString('base64'), // TODO: explore other options
317316
sub: {
318317
connect: { name: subName }

Diff for: api/typeDefs/sub.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export default gql`
1818
sslState: String
1919
certificateArn: String
2020
lastVerifiedAt: Date
21-
cname: String
21+
verificationCname: String
22+
verificationCnameValue: String
2223
verificationTxt: String
2324
}
2425

Diff for: components/territory-domains.js

+32-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Badge } from 'react-bootstrap'
22
import { Form, Input, SubmitButton } from './form'
33
import { gql, useMutation } from '@apollo/client'
4-
import Info from './info'
54
import { customDomainSchema } from '@/lib/validate'
65
import ActionTooltip from './action-tooltip'
76
import { useToast } from '@/components/toast'
@@ -77,23 +76,6 @@ export default function CustomDomainForm ({ sub }) {
7776
{getStatusBadge(sub.customDomain.dnsState)}
7877
</ActionTooltip>
7978
{getSSLStatusBadge(sub.customDomain.sslState)}
80-
{sub.customDomain.dnsState === 'PENDING' && (
81-
<Info>
82-
<h6>Verify your domain</h6>
83-
<p>Add the following DNS records to verify ownership of your domain:</p>
84-
<pre>
85-
CNAME record:
86-
Host: @
87-
Value: stacker.news
88-
</pre>
89-
</Info>
90-
)}
91-
{sub.customDomain.sslState === 'PENDING' && (
92-
<Info>
93-
<h6>SSL certificate pending</h6>
94-
<p>Our system will issue an SSL certificate for your domain.</p>
95-
</Info>
96-
)}
9779
</div>
9880
</>
9981
)}
@@ -105,6 +87,38 @@ export default function CustomDomainForm ({ sub }) {
10587
{/* TODO: toaster */}
10688
<SubmitButton variant='primary' className='mt-3'>save</SubmitButton>
10789
</div>
90+
{(sub.customDomain.dnsState === 'PENDING' || sub.customDomain.dnsState === 'FAILED') && (
91+
<>
92+
<h6>Verify your domain</h6>
93+
<p>Add the following DNS records to verify ownership of your domain:</p>
94+
<pre>
95+
CNAME:
96+
Host: @
97+
Value: stacker.news
98+
</pre>
99+
<pre>
100+
TXT:
101+
Host: @
102+
Value: ${sub.customDomain.verificationTxt}
103+
</pre>
104+
</>
105+
)}
106+
{sub.customDomain.sslState === 'PENDING' && (
107+
<>
108+
<h6>SSL verification pending</h6>
109+
<p>We issued an SSL certificate for your domain.</p>
110+
<pre>
111+
CNAME:
112+
Host: ${sub.customDomain.verificationCname}
113+
Value: ${sub.customDomain.verificationCnameValue}
114+
</pre>
115+
<pre>
116+
TXT:
117+
Host: @
118+
Value: ${sub.customDomain.verificationTxt}
119+
</pre>
120+
</>
121+
)}
108122
</Form>
109123
)
110124
}

Diff for: docker-compose.yml

+7-7
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ services:
136136
labels:
137137
- "CONNECT=localhost:3001"
138138
cpu_shares: "${CPU_SHARES_LOW}"
139-
s3:
140-
container_name: s3
141-
image: localstack/localstack:s3-latest
139+
aws:
140+
container_name: aws
141+
image: localstack/localstack:latest
142142
# healthcheck:
143143
# test: ["CMD-SHELL", "awslocal", "s3", "ls", "s3://uploads"]
144144
# interval: 10s
@@ -156,9 +156,9 @@ services:
156156
expose:
157157
- "4566"
158158
volumes:
159-
- 's3:/var/lib/localstack'
160-
- './docker/s3/init-s3.sh:/etc/localstack/init/ready.d/init-s3.sh'
161-
- './docker/s3/cors.json:/etc/localstack/init/ready.d/cors.json'
159+
- 'aws:/var/lib/localstack'
160+
- './docker/aws/s3/init-s3.sh:/etc/localstack/init/ready.d/init-s3.sh'
161+
- './docker/aws/s3/cors.json:/etc/localstack/init/ready.d/cors.json'
162162
labels:
163163
- "CONNECT=localhost:4566"
164164
cpu_shares: "${CPU_SHARES_LOW}"
@@ -829,7 +829,7 @@ volumes:
829829
lnd:
830830
cln:
831831
router_lnd:
832-
s3:
832+
aws:
833833
nwc_send:
834834
nwc_recv:
835835
tordata:
File renamed without changes.
File renamed without changes.

Diff for: fragments/subs.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export const SUB_FIELDS = gql`
4444
sslState
4545
certificateArn
4646
lastVerifiedAt
47-
cname
47+
verificationCname
48+
verificationCnameValue
4849
verificationTxt
4950
}
5051
}`

Diff for: lib/domains.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { requestCertificate, getCertificateStatus } from '@/api/acm'
1+
import { requestCertificate, getCertificateStatus, describeCertificate } from '@/api/acm'
22
import { promises as dnsPromises } from 'node:dns'
33

44
// TODO: skeleton
@@ -25,7 +25,7 @@ export async function checkCertificateStatus (certificateArn) {
2525
// map ACM statuses
2626
switch (certStatus) {
2727
case 'ISSUED':
28-
return 'ISSUED'
28+
return 'VERIFIED'
2929
case 'PENDING_VALIDATION':
3030
return 'PENDING'
3131
case 'VALIDATION_TIMED_OUT':
@@ -36,7 +36,26 @@ export async function checkCertificateStatus (certificateArn) {
3636
}
3737
}
3838

39-
export async function verifyDomainDNS (domainName, verificationTxt, cname) {
39+
export async function certDetails (certificateArn) {
40+
try {
41+
const certificate = await describeCertificate(certificateArn)
42+
return certificate
43+
} catch (error) {
44+
console.error(`Certificate description failed: ${error.message}`)
45+
return null
46+
}
47+
}
48+
49+
export async function getValidationValues (certificateArn) {
50+
const certificate = await certDetails(certificateArn)
51+
console.log(certificate.DomainValidationOptions)
52+
return {
53+
cname: certificate.DomainValidationOptions[0].ResourceRecord.Name,
54+
value: certificate.DomainValidationOptions[0].ResourceRecord.Value
55+
}
56+
}
57+
58+
export async function verifyDomainDNS (domainName, verificationTxt, cname = 'stacker.news') {
4059
const result = {
4160
txtValid: false,
4261
cnameValid: false,

Diff for: middleware.js

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export async function customDomainMiddleware (request, referrerResp) {
9494

9595
// TODO: dirty of previous iterations, refactor
9696
// UNSAFE UNSAFE UNSAFE tokens are visible in the URL
97+
// Redirect to Auth Sync if user is not logged in or has no multi_auth sessions
9798
export function customDomainAuthMiddleware (request, url) {
9899
const host = request.headers.get('host')
99100
const mainDomain = process.env.NEXT_PUBLIC_URL

Diff for: pages/api/domains/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default async function handler (req, res) {
1212
}
1313

1414
try {
15-
// fetch all custom domains from the database
15+
// fetch all VERIFIED custom domains from the database
1616
const domains = await prisma.customDomain.findMany({
1717
select: {
1818
domain: true,

Diff for: prisma/migrations/20250304121322_custom_domains/migration.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ CREATE TABLE "CustomDomain" (
99
"sslState" TEXT,
1010
"certificateArn" TEXT,
1111
"lastVerifiedAt" TIMESTAMP(3),
12-
"cname" TEXT,
12+
"verificationCname" TEXT,
13+
"verificationCnameValue" TEXT,
1314
"verificationTxt" TEXT,
1415

1516
CONSTRAINT "CustomDomain_pkey" PRIMARY KEY ("id")

Diff for: prisma/schema.prisma

+13-12
Original file line numberDiff line numberDiff line change
@@ -1221,18 +1221,19 @@ model Reminder {
12211221
}
12221222

12231223
model CustomDomain {
1224-
id Int @id @default(autoincrement())
1225-
createdAt DateTime @default(now()) @map("created_at")
1226-
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
1227-
domain String @unique
1228-
subName String @unique @db.Citext
1229-
dnsState String?
1230-
sslState String?
1231-
certificateArn String?
1232-
lastVerifiedAt DateTime?
1233-
cname String?
1234-
verificationTxt String?
1235-
sub Sub @relation(fields: [subName], references: [name], onDelete: Cascade, onUpdate: Cascade)
1224+
id Int @id @default(autoincrement())
1225+
createdAt DateTime @default(now()) @map("created_at")
1226+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
1227+
domain String @unique
1228+
subName String @unique @db.Citext
1229+
dnsState String?
1230+
sslState String?
1231+
certificateArn String?
1232+
lastVerifiedAt DateTime?
1233+
verificationCname String?
1234+
verificationCnameValue String?
1235+
verificationTxt String?
1236+
sub Sub @relation(fields: [subName], references: [name], onDelete: Cascade, onUpdate: Cascade)
12361237
12371238
@@index([domain])
12381239
@@index([createdAt])

Diff for: worker/domainVerification.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import createPrisma from '@/lib/create-prisma'
2-
import { verifyDomainDNS, issueDomainCertificate, checkCertificateStatus } from '@/lib/domains'
2+
import { verifyDomainDNS, issueDomainCertificate, checkCertificateStatus, getValidationValues } from '@/lib/domains'
33

44
// TODO: Add comments
55
export async function domainVerification () {
@@ -9,23 +9,32 @@ export async function domainVerification () {
99
const domains = await models.customDomain.findMany()
1010

1111
for (const domain of domains) {
12-
const { domain: domainName, dnsState, sslState, certificateArn, verificationTxt, cname, id } = domain
12+
const { domain: domainName, dnsState, sslState, certificateArn, verificationTxt, id } = domain
1313
try {
1414
const data = { lastVerifiedAt: new Date() }
1515
// DNS verification
1616
if (dnsState === 'PENDING' || dnsState === 'FAILED') {
17-
const { txtValid, cnameValid } = await verifyDomainDNS(domainName, verificationTxt, cname)
17+
const { txtValid, cnameValid } = await verifyDomainDNS(domainName, verificationTxt)
1818
console.log(`${domainName}: TXT ${txtValid ? 'valid' : 'invalid'}, CNAME ${cnameValid ? 'valid' : 'invalid'}`)
1919
data.dnsState = txtValid && cnameValid ? 'VERIFIED' : 'FAILED'
2020
}
21-
21+
// TODO: make this consequential, don't wait for the next cron to issue the certificate
2222
// SSL issuing
2323
if (dnsState === 'VERIFIED' && (!certificateArn || sslState === 'FAILED')) {
2424
const certificateArn = await issueDomainCertificate(domainName)
2525
console.log(`${domainName}: Certificate issued: ${certificateArn}`)
2626
if (certificateArn) {
2727
const sslState = await checkCertificateStatus(certificateArn)
2828
console.log(`${domainName}: Issued certificate status: ${sslState}`)
29+
if (sslState === 'PENDING') {
30+
try {
31+
const { cname, value } = await getValidationValues(certificateArn)
32+
data.verificationCname = cname
33+
data.verificationCnameValue = value
34+
} catch (error) {
35+
console.error(`Failed to get validation values for domain ${domainName}:`, error)
36+
}
37+
}
2938
if (sslState) data.sslState = sslState
3039
data.certificateArn = certificateArn
3140
} else {
@@ -42,7 +51,7 @@ export async function domainVerification () {
4251

4352
await models.customDomain.update({ where: { id }, data })
4453
} catch (error) {
45-
// TODO: this considers only DNS verification errors, we should also consider SSL verification errors
54+
// TODO: this declares any error as a DNS verification error, we should also consider SSL verification errors
4655
console.error(`Failed to verify domain ${domainName}:`, error)
4756

4857
// TODO: DNS inconcistencies can happen, we should retry at least 3 times before marking it as FAILED

0 commit comments

Comments
 (0)