Skip to content

Commit 003ebe9

Browse files
committed
wip Custom Domain form for Territory Edit; fix endpoint typo; upsert customDomain resolver
1 parent 7bb6166 commit 003ebe9

File tree

6 files changed

+139
-25
lines changed

6 files changed

+139
-25
lines changed

api/resolvers/sub.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { whenRange } from '@/lib/time'
2-
import { validateSchema, territorySchema } from '@/lib/validate'
2+
import { validateSchema, validateDomain, territorySchema } from '@/lib/validate'
33
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
44
import { viewGroup } from './growth'
55
import { notifyTerritoryTransfer } from '@/lib/webPush'
@@ -277,6 +277,35 @@ export default {
277277
}
278278

279279
return await performPaidAction('TERRITORY_UNARCHIVE', data, { me, models, lnd })
280+
},
281+
updateCustomDomain: async (parent, { subName, domain }, { me, models }) => {
282+
if (!me) {
283+
throw new GqlAuthenticationError()
284+
}
285+
286+
const sub = await models.sub.findUnique({ where: { name: subName } })
287+
if (!sub) {
288+
throw new GqlInputError('sub not found')
289+
}
290+
291+
if (sub.userId !== me.id) {
292+
throw new GqlInputError('you do not own this sub')
293+
}
294+
domain = domain.trim()
295+
if (domain && !validateDomain(domain)) {
296+
throw new GqlInputError('Invalid domain format')
297+
}
298+
299+
if (domain) {
300+
const existing = await models.customDomain.findUnique({ where: { subName } })
301+
if (existing) {
302+
return await models.customDomain.update({ where: { subName }, data: { domain, verificationState: 'PENDING' } })
303+
} else {
304+
return await models.customDomain.create({ data: { domain, subName, verificationState: 'PENDING' } })
305+
}
306+
} else {
307+
return await models.customDomain.delete({ where: { subName } })
308+
}
280309
}
281310
},
282311
Sub: {

api/typeDefs/sub.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default gql`
3939
replyCost: Int!, postTypes: [String!]!,
4040
billingType: String!, billingAutoRenew: Boolean!,
4141
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
42+
updateCustomDomain(subName: String!, domain: String!): CustomDomain
4243
}
4344
4445
type Sub {

components/territory-domains.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useState } from 'react'
2+
import { Badge } from 'react-bootstrap'
3+
import { Form, Input, SubmitButton } from './form'
4+
import { gql, useMutation } from '@apollo/client'
5+
import Info from './info'
6+
7+
const UPDATE_CUSTOM_DOMAIN = gql`
8+
mutation UpdateCustomDomain($subName: String!, $domain: String!) {
9+
updateCustomDomain(subName: $subName, domain: $domain) {
10+
domain
11+
verificationState
12+
lastVerifiedAt
13+
}
14+
}
15+
`
16+
17+
export default function CustomDomainForm ({ sub }) {
18+
const [updateCustomDomain] = useMutation(UPDATE_CUSTOM_DOMAIN)
19+
const [error, setError] = useState(null)
20+
const [success, setSuccess] = useState(false)
21+
22+
const onSubmit = async ({ domain }) => {
23+
setError(null)
24+
setSuccess(false)
25+
console.log('domain', domain)
26+
27+
const { data } = await updateCustomDomain({
28+
variables: {
29+
subName: sub.name,
30+
domain
31+
}
32+
})
33+
console.log('success', data)
34+
setSuccess(true)
35+
}
36+
37+
const getStatusBadge = (status) => {
38+
switch (status) {
39+
case 'VERIFIED':
40+
return <Badge bg='success'>verified</Badge>
41+
case 'PENDING':
42+
return <Badge bg='warning'>pending</Badge>
43+
case 'FAILED':
44+
return <Badge bg='danger'>failed</Badge>
45+
}
46+
}
47+
48+
return (
49+
<Form
50+
initial={{
51+
domain: sub?.customDomain?.domain || ''
52+
}}
53+
onSubmit={onSubmit}
54+
className='mb-2'
55+
>
56+
<div className='d-flex align-items-center gap-2'>
57+
<Input
58+
label={
59+
<div className='d-flex align-items-center'>
60+
<span>domain</span>
61+
{error && <Info variant='danger'>error</Info>}
62+
{success && <Info variant='success'>Domain settings updated successfully!</Info>}
63+
{sub?.customDomain && (
64+
<div className='d-flex align-items-center gap-2'>
65+
{getStatusBadge(sub.customDomain.verificationState)}
66+
<span className='text-muted'>
67+
{sub.customDomain.lastVerifiedAt &&
68+
` (Last checked: ${new Date(sub.customDomain.lastVerifiedAt).toLocaleString()})`}
69+
</span>
70+
71+
{sub.customDomain.verificationState === 'PENDING' && (
72+
<Info>
73+
<h6>Verify your domain</h6>
74+
<p>Add the following DNS records to verify ownership of your domain:</p>
75+
<pre>
76+
CNAME record:
77+
Host: @
78+
Value: stacker.news
79+
</pre>
80+
</Info>
81+
)}
82+
</div>
83+
)}
84+
</div>
85+
}
86+
name='domain'
87+
placeholder='example.com'
88+
/>
89+
{/* TODO: toaster */}
90+
<SubmitButton variant='primary' className='mt-3'>save</SubmitButton>
91+
</div>
92+
</Form>
93+
)
94+
}

components/territory-form.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { purchasedType } from '@/lib/territory'
1414
import { SUB } from '@/fragments/subs'
1515
import { usePaidMutation } from './use-paid-mutation'
1616
import { UNARCHIVE_TERRITORY, UPSERT_SUB } from '@/fragments/paidAction'
17+
import TerritoryDomains from './territory-domains'
1718

1819
export default function TerritoryForm ({ sub }) {
1920
const router = useRouter()
@@ -276,25 +277,7 @@ export default function TerritoryForm ({ sub }) {
276277
name='nsfw'
277278
groupClassName='ms-1'
278279
/>
279-
<Input
280-
label={
281-
<div className='d-flex align-items-center'>[NOT IMPLEMENTED] custom domain
282-
<Info>
283-
<ol>
284-
<li>TODO Immediate infos on Custom Domains</li>
285-
</ol>
286-
</Info>
287-
</div>
288-
}
289-
name='customDomain'
290-
type='text'
291-
required
292-
append={
293-
<>
294-
<InputGroup.Text className='text-monospace'>{sub?.customDomain?.verificationState || 'not verified'}</InputGroup.Text>
295-
</>
296-
}
297-
/>
280+
<TerritoryDomains sub={sub} />
298281
{sub?.customDomain?.verificationState === 'VERIFIED' &&
299282
<>
300283
<BootstrapForm.Label>[NOT IMPLEMENTED] branding</BootstrapForm.Label>

lib/validate.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,7 @@ export function territorySchema (args) {
323323
.max(100000, 'must be at most 100k'),
324324
postTypes: array().of(string().oneOf(POST_TYPES)).min(1, 'must support at least one post type'),
325325
billingType: string().required('required').oneOf(TERRITORY_BILLING_TYPES, 'required'),
326-
nsfw: boolean(),
327-
customDomain: string().matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
328-
message: 'enter a valid domain name (e.g., example.com)'
329-
}).nullable()
326+
nsfw: boolean()
330327
})
331328
}
332329

@@ -349,6 +346,15 @@ export function territoryTransferSchema ({ me, ...args }) {
349346
})
350347
}
351348

349+
// TODO: validate domain
350+
export function customDomainSchema (args) {
351+
return object({
352+
customDomain: string().matches(/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, {
353+
message: 'enter a valid domain name (e.g., example.com)'
354+
}).nullable()
355+
})
356+
}
357+
352358
export function userSchema (args) {
353359
return object({
354360
name: nameValidator

middleware.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const NO_REWRITE_PATHS = ['/api', '/_next', '/_error', '/404', '/500', '/offline
1717

1818
// fetch custom domain mappings from our API, caching it for 5 minutes
1919
const getDomainMappingsCache = cachedFetcher(async function fetchDomainMappings () {
20-
const url = `${process.env.NEXT_PUBLIC_URL}/api/domains/map`
20+
const url = `${process.env.NEXT_PUBLIC_URL}/api/domains`
2121
try {
2222
const response = await fetch(url)
2323
if (!response.ok) {
@@ -49,6 +49,7 @@ export async function customDomainMiddleware (request, referrerResp) {
4949
console.log('referer', referer)
5050

5151
const domainMapping = await getDomainMappingsCache()
52+
console.log('domainMapping', domainMapping)
5253
const domainInfo = domainMapping?.[host.toLowerCase()]
5354
if (!domainInfo) {
5455
return NextResponse.redirect(new URL(pathname, mainDomain))

0 commit comments

Comments
 (0)