Skip to content

Commit a900a8d

Browse files
committed
custom domain form and validation; FAILED verification only if it was VERIFIED
1 parent ac95acd commit a900a8d

File tree

9 files changed

+73
-56
lines changed

9 files changed

+73
-56
lines changed

api/resolvers/sub.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { whenRange } from '@/lib/time'
2-
import { validateSchema, validateDomain, territorySchema } from '@/lib/validate'
2+
import { validateSchema, customDomainSchema, territorySchema } from '@/lib/validate'
33
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
44
import { viewGroup } from './growth'
55
import { notifyTerritoryTransfer } from '@/lib/webPush'
66
import performPaidAction from '../paidAction'
77
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
8+
import { randomBytes } from 'node:crypto'
89

910
export async function getSub (parent, { name }, { models, me }) {
1011
if (!name) return null
@@ -292,16 +293,32 @@ export default {
292293
throw new GqlInputError('you do not own this sub')
293294
}
294295
domain = domain.trim()
295-
if (domain && !validateDomain(domain)) {
296+
if (domain && !validateSchema(customDomainSchema, { domain })) {
296297
throw new GqlInputError('Invalid domain format')
297298
}
298299

300+
console.log('domain', domain)
301+
console.log('sub.customDomain?.domain', sub.customDomain?.domain)
302+
299303
if (domain) {
300304
const existing = await models.customDomain.findUnique({ where: { subName } })
301305
if (existing) {
306+
if (domain === existing.domain) {
307+
throw new GqlInputError('domain already set')
308+
}
302309
return await models.customDomain.update({ where: { subName }, data: { domain, verificationState: 'PENDING' } })
303310
} else {
304-
return await models.customDomain.create({ data: { domain, subName, verificationState: 'PENDING' } })
311+
return await models.customDomain.create({
312+
data: {
313+
domain,
314+
verificationState: 'PENDING',
315+
cname: 'parallel.soxa.dev',
316+
verificationTxt: randomBytes(32).toString('base64'),
317+
sub: {
318+
connect: { name: subName }
319+
}
320+
}
321+
})
305322
}
306323
} else {
307324
return await models.customDomain.delete({ where: { subName } })

components/territory-domains.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ import { Badge } from 'react-bootstrap'
33
import { Form, Input, SubmitButton } from './form'
44
import { gql, useMutation } from '@apollo/client'
55
import Info from './info'
6+
import { customDomainSchema } from '@/lib/validate'
7+
import ActionTooltip from './action-tooltip'
68

79
const UPDATE_CUSTOM_DOMAIN = gql`
810
mutation UpdateCustomDomain($subName: String!, $domain: String!) {
911
updateCustomDomain(subName: $subName, domain: $domain) {
1012
domain
1113
verificationState
12-
lastVerifiedAt
1314
}
1415
}
1516
`
1617

18+
// TODO: verification states should refresh
1719
export default function CustomDomainForm ({ sub }) {
1820
const [updateCustomDomain] = useMutation(UPDATE_CUSTOM_DOMAIN)
1921
const [error, setError] = useState(null)
@@ -59,9 +61,11 @@ export default function CustomDomainForm ({ sub }) {
5961
initial={{
6062
domain: sub?.customDomain?.domain || ''
6163
}}
64+
schema={customDomainSchema}
6265
onSubmit={onSubmit}
6366
className='mb-2'
6467
>
68+
{/* todo: too many flexes */}
6569
<div className='d-flex align-items-center gap-2'>
6670
<Input
6771
label={
@@ -72,12 +76,10 @@ export default function CustomDomainForm ({ sub }) {
7276
{sub?.customDomain && (
7377
<>
7478
<div className='d-flex align-items-center gap-2'>
75-
{getStatusBadge(sub.customDomain.verificationState)}
76-
<span className='text-muted'>
77-
{sub.customDomain.lastVerifiedAt &&
78-
` (Last checked: ${new Date(sub.customDomain.lastVerifiedAt).toLocaleString()})`}
79-
</span>
80-
79+
<ActionTooltip overlayText={new Date(sub.customDomain.lastVerifiedAt).toUTCString()}>
80+
{getStatusBadge(sub.customDomain.verificationState)}
81+
</ActionTooltip>
82+
{getSSLStatusBadge(sub.customDomain.sslEnabled)}
8183
{sub.customDomain.verificationState === 'PENDING' && (
8284
<Info>
8385
<h6>Verify your domain</h6>
@@ -90,7 +92,6 @@ export default function CustomDomainForm ({ sub }) {
9092
</Info>
9193
)}
9294
</div>
93-
{getSSLStatusBadge(sub.customDomain.sslEnabled)}
9495
</>
9596
)}
9697
</div>

components/territory-form.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -280,21 +280,6 @@ export default function TerritoryForm ({ sub }) {
280280
</>
281281
}
282282
/>
283-
<AccordianItem
284-
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>personalization</div>}
285-
body={
286-
<>
287-
<TerritoryDomains sub={sub} />
288-
{sub?.customDomain?.verificationState === 'VERIFIED' &&
289-
<>
290-
<BootstrapForm.Label>[NOT IMPLEMENTED] branding</BootstrapForm.Label>
291-
<div className='mb-3'>WIP</div>
292-
<BootstrapForm.Label>[NOT IMPLEMENTED] color scheme</BootstrapForm.Label>
293-
<div className='mb-3'>WIP</div>
294-
</>}
295-
</>
296-
}
297-
/>
298283
<div className='mt-3 d-flex justify-content-end'>
299284
<FeeButton
300285
text={sub ? 'save' : 'found it'}
@@ -303,6 +288,21 @@ export default function TerritoryForm ({ sub }) {
303288
/>
304289
</div>
305290
</Form>
291+
<AccordianItem
292+
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>personalization</div>}
293+
body={
294+
<>
295+
<TerritoryDomains sub={sub} />
296+
{sub?.customDomain?.verificationState === 'VERIFIED' &&
297+
<>
298+
<BootstrapForm.Label>[NOT IMPLEMENTED] branding</BootstrapForm.Label>
299+
<div className='mb-3'>WIP</div>
300+
<BootstrapForm.Label>[NOT IMPLEMENTED] color scheme</BootstrapForm.Label>
301+
<div className='mb-3'>WIP</div>
302+
</>}
303+
</>
304+
}
305+
/>
306306
</FeeButtonProvider>
307307
)
308308
}

lib/validate.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ export function territoryTransferSchema ({ me, ...args }) {
349349
// TODO: validate domain
350350
export function customDomainSchema (args) {
351351
return object({
352-
customDomain: string().matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
352+
domain: string().matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
353353
message: 'enter a valid domain name (e.g., example.com)'
354354
}).nullable()
355355
})

middleware.js

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export async function customDomainMiddleware (request, referrerResp) {
5252
console.log('domainMapping', domainMapping)
5353
const domainInfo = domainMapping?.[host.toLowerCase()]
5454
if (!domainInfo) {
55+
console.log('Redirecting to main domain')
5556
return NextResponse.redirect(new URL(pathname, mainDomain))
5657
}
5758

prisma/migrations/20250304121322_custom_domains/migration.sql

+20
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ CREATE TABLE "CustomDomain" (
99
"sslCertExpiry" TIMESTAMP(3),
1010
"verificationState" TEXT,
1111
"lastVerifiedAt" TIMESTAMP(3),
12+
"cname" TEXT NOT NULL,
13+
"verificationTxt" TEXT NOT NULL,
1214

1315
CONSTRAINT "CustomDomain_pkey" PRIMARY KEY ("id")
1416
);
@@ -27,3 +29,21 @@ CREATE INDEX "CustomDomain_created_at_idx" ON "CustomDomain"("created_at");
2729

2830
-- AddForeignKey
2931
ALTER TABLE "CustomDomain" ADD CONSTRAINT "CustomDomain_subName_fkey" FOREIGN KEY ("subName") REFERENCES "Sub"("name") ON DELETE CASCADE ON UPDATE CASCADE;
32+
33+
CREATE OR REPLACE FUNCTION schedule_domain_verification_job()
34+
RETURNS INTEGER
35+
LANGUAGE plpgsql
36+
AS $$
37+
DECLARE
38+
BEGIN
39+
-- every 5 minutes
40+
INSERT INTO pgboss.schedule (name, cron, timezone)
41+
VALUES ('domainVerification', '*/5 * * * *', 'America/Chicago') ON CONFLICT DO NOTHING;
42+
return 0;
43+
EXCEPTION WHEN OTHERS THEN
44+
return 0;
45+
END;
46+
$$;
47+
48+
SELECT schedule_domain_verification_job();
49+
DROP FUNCTION IF EXISTS schedule_domain_verification_job;

prisma/migrations/20250311105915_verify_dns_records/migration.sql

-10
This file was deleted.

prisma/migrations/20250311112522_domain_verification_job/migration.sql

-17
This file was deleted.

worker/domainVerification.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ export async function domainVerification () {
1515
console.log(`${domainName}: TXT ${txtValid ? 'valid' : 'invalid'}, CNAME ${cnameValid ? 'valid' : 'invalid'}`)
1616

1717
// verificationState is based on the results of the TXT and CNAME checks
18-
const verificationState = (txtValid && cnameValid) ? 'VERIFIED' : 'FAILED'
18+
const verificationResult = txtValid && cnameValid
19+
const verificationState = verificationResult // TODO: clean this up, working proof of concept
20+
? 'VERIFIED'
21+
: domain.verificationState === 'PENDING' && !verificationResult
22+
? 'PENDING'
23+
: 'FAILED'
1924
await models.customDomain.update({
2025
where: { id },
2126
data: { verificationState, lastVerifiedAt: new Date() }
@@ -31,7 +36,7 @@ export async function domainVerification () {
3136
// Update to FAILED on any error
3237
await models.customDomain.update({
3338
where: { id },
34-
data: { verificationState: 'NOT_VERIFIED', lastVerifiedAt: new Date() }
39+
data: { verificationState: 'FAILED', lastVerifiedAt: new Date() }
3540
})
3641
}
3742
}

0 commit comments

Comments
 (0)