Skip to content

Commit cd46aeb

Browse files
committed
Domains refactor, Domain Verification normalization
- CustomDomain -> Domain - DomainVerification table - CNAME, TXT, SSL verification types - WIP DomainVerification upsert
1 parent c50509a commit cd46aeb

File tree

15 files changed

+315
-227
lines changed

15 files changed

+315
-227
lines changed

api/resolvers/domain.js

+65-34
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import { getDomainMapping } from '@/lib/domains'
55

66
export default {
77
Query: {
8-
customDomain: async (parent, { subName }, { models }) => {
9-
return models.customDomain.findUnique({ where: { subName } })
8+
domain: async (parent, { subName }, { models }) => {
9+
return models.domain.findUnique({ where: { subName }, include: { verifications: true } })
1010
},
11-
domainMapping: async (parent, { domain }, { models }) => {
12-
const mapping = await getDomainMapping(domain)
11+
domainMapping: async (parent, { domainName }, { models }) => {
12+
const mapping = await getDomainMapping(domainName)
1313
return mapping
1414
}
1515
},
1616
Mutation: {
17-
setCustomDomain: async (parent, { subName, domain }, { me, models }) => {
17+
setDomain: async (parent, { subName, domainName }, { me, models }) => {
1818
if (!me) {
1919
throw new GqlAuthenticationError()
2020
}
@@ -28,45 +28,27 @@ export default {
2828
throw new GqlInputError('you do not own this sub')
2929
}
3030

31-
domain = domain.trim() // protect against trailing spaces
32-
if (domain && !validateSchema(customDomainSchema, { domain })) {
31+
domainName = domainName.trim() // protect against trailing spaces
32+
if (domainName && !validateSchema(customDomainSchema, { domainName })) {
3333
throw new GqlInputError('invalid domain format')
3434
}
3535

36-
const existing = await models.customDomain.findUnique({ where: { subName } })
36+
const existing = await models.domain.findUnique({ where: { subName } })
3737

38-
if (domain) {
39-
if (existing && existing.domain === domain && existing.status !== 'HOLD') {
38+
if (domainName) {
39+
if (existing && existing.domainName === domainName && existing.status !== 'HOLD') {
4040
throw new GqlInputError('domain already set')
4141
}
4242

4343
const initializeDomain = {
44-
domain,
45-
createdAt: new Date(),
46-
status: 'PENDING',
47-
verification: {
48-
dns: {
49-
state: 'PENDING',
50-
cname: 'stacker.news',
51-
// generate a random txt record only if it's a new domain
52-
txt: existing?.domain === domain && existing.verification.dns.txt
53-
? existing.verification.dns.txt
54-
: randomBytes(32).toString('base64')
55-
},
56-
ssl: {
57-
state: 'WAITING',
58-
arn: null,
59-
cname: null,
60-
value: null
61-
}
62-
}
44+
domainName,
45+
updatedAt: new Date(),
46+
status: 'PENDING'
6347
}
6448

65-
const updatedDomain = await models.customDomain.upsert({
49+
const updatedDomain = await models.domain.upsert({
6650
where: { subName },
67-
update: {
68-
...initializeDomain
69-
},
51+
update: initializeDomain,
7052
create: {
7153
...initializeDomain,
7254
sub: {
@@ -75,6 +57,55 @@ export default {
7557
}
7658
})
7759

60+
const existingVerifications = await models.domainVerification.findMany({
61+
where: { domainId: updatedDomain.id }
62+
})
63+
64+
const existingVerificationMap = Object.fromEntries(existingVerifications.map(v => [v.type, v]))
65+
66+
const verifications = {
67+
CNAME: {
68+
domainId: updatedDomain.id,
69+
type: 'CNAME',
70+
state: 'PENDING',
71+
host: domainName,
72+
value: 'stacker.news'
73+
},
74+
TXT: {
75+
domainId: updatedDomain.id,
76+
type: 'TXT',
77+
state: 'PENDING',
78+
host: '_snverify.' + domainName,
79+
value: existing.status === 'HOLD' ? existingVerificationMap.TXT?.value : randomBytes(32).toString('base64')
80+
},
81+
SSL: {
82+
domainId: updatedDomain.id,
83+
type: 'SSL',
84+
state: 'WAITING',
85+
host: null,
86+
value: null,
87+
sslArn: null
88+
}
89+
}
90+
91+
const initializeVerifications = Object.entries(verifications).map(([type, verification]) =>
92+
models.domainVerification.upsert({
93+
where: {
94+
domainId_type: {
95+
domainId: updatedDomain.id,
96+
type
97+
}
98+
},
99+
update: verification,
100+
create: {
101+
...verification,
102+
domain: { connect: { id: updatedDomain.id } }
103+
}
104+
})
105+
)
106+
107+
await Promise.all(initializeVerifications)
108+
78109
// schedule domain verification in 30 seconds
79110
await models.$executeRaw`
80111
INSERT INTO pgboss.job (name, data, retrylimit, retrydelay, startafter, keepuntil)
@@ -94,7 +125,7 @@ export default {
94125
WHERE name = 'domainVerification'
95126
AND data->>'domainId' = ${existing.id}::TEXT`
96127

97-
return await models.customDomain.delete({ where: { subName } })
128+
return await models.domain.delete({ where: { subName } })
98129
} catch (error) {
99130
console.error(error)
100131
throw new GqlInputError('failed to delete domain')

api/resolvers/sub.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,8 @@ export default {
310310

311311
return sub.SubSubscription?.length > 0
312312
},
313-
customDomain: async (sub, args, { models }) => {
314-
return models.customDomain.findUnique({ where: { subName: sub.name } })
313+
domain: async (sub, args, { models }) => {
314+
return models.domain.findUnique({ where: { subName: sub.name } })
315315
},
316316
createdAt: sub => sub.createdAt || sub.created_at
317317
}

api/ssrApollo.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,10 @@ export function getGetServerSideProps (
154154

155155
const isCustomDomain = req.headers.host !== process.env.NEXT_PUBLIC_URL.replace(/^https?:\/\//, '')
156156
const subName = req.headers['x-stacker-news-subname'] || null
157-
let customDomain = null
157+
let domain = null
158158
if (isCustomDomain && subName) {
159-
customDomain = {
160-
domain: req.headers.host,
159+
domain = {
160+
domainName: req.headers.host,
161161
subName
162162
// TODO: custom branding
163163
}
@@ -227,7 +227,7 @@ export function getGetServerSideProps (
227227
return {
228228
props: {
229229
...props,
230-
customDomain,
230+
domain,
231231
me,
232232
price,
233233
blockHeight,

api/typeDefs/domain.js

+28-24
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,44 @@ import { gql } from 'graphql-tag'
22

33
export default gql`
44
extend type Query {
5-
customDomain(subName: String!): CustomDomain
6-
domainMapping(domain: String!): DomainMapping
5+
domain(subName: String!): Domain
6+
domainMapping(domainName: String!): DomainMapping
77
}
8+
89
extend type Mutation {
9-
setCustomDomain(subName: String!, domain: String!): CustomDomain
10+
setDomain(subName: String!, domainName: String!): Domain
1011
}
11-
type CustomDomain {
12+
13+
type Domain {
1214
createdAt: Date!
1315
updatedAt: Date!
14-
domain: String!
16+
domainName: String!
1517
subName: String!
16-
lastVerifiedAt: Date
17-
failedAttempts: Int
1818
status: String
19-
verification: CustomDomainVerification
19+
verifications: [DomainVerification]
20+
}
21+
22+
type DomainVerification {
23+
createdAt: Date!
24+
updatedAt: Date!
25+
domainId: Int!
26+
type: DomainVerificationType
27+
state: String
28+
host: String
29+
value: String
30+
sslArn: String
31+
result: String
32+
lastCheckedAt: Date
33+
}
34+
35+
enum DomainVerificationType {
36+
TXT
37+
CNAME
38+
SSL
2039
}
2140
2241
type DomainMapping {
23-
domain: String!
42+
domainName: String!
2443
subName: String!
2544
}
26-
type CustomDomainVerification {
27-
dns: CustomDomainVerificationDNS
28-
ssl: CustomDomainVerificationSSL
29-
}
30-
type CustomDomainVerificationDNS {
31-
state: String
32-
cname: String
33-
txt: String
34-
}
35-
type CustomDomainVerificationSSL {
36-
state: String
37-
arn: String
38-
cname: String
39-
value: String
40-
}
4145
`

api/typeDefs/sub.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default gql`
5555
nposts(when: String, from: String, to: String): Int!
5656
ncomments(when: String, from: String, to: String): Int!
5757
meSubscription: Boolean!
58-
customDomain: CustomDomain
58+
domain: Domain
5959
optional: SubOptional!
6060
}
6161

0 commit comments

Comments
 (0)