Skip to content

Commit 753e7c6

Browse files
committed
refactor custom domain queries, todo dynamic callback for login
1 parent 23bba32 commit 753e7c6

File tree

10 files changed

+135
-48
lines changed

10 files changed

+135
-48
lines changed

api/resolvers/domain.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { validateSchema, customDomainSchema } from '@/lib/validate'
2+
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
3+
import { randomBytes } from 'node:crypto'
4+
5+
export default {
6+
Query: {
7+
customDomain: async (parent, { subName }, { models }) => {
8+
return models.customDomain.findUnique({ where: { subName } })
9+
}
10+
},
11+
Mutation: {
12+
setCustomDomain: async (parent, { subName, domain }, { me, models }) => {
13+
if (!me) {
14+
throw new GqlAuthenticationError()
15+
}
16+
17+
const sub = await models.sub.findUnique({ where: { name: subName } })
18+
if (!sub) {
19+
throw new GqlInputError('sub not found')
20+
}
21+
22+
if (sub.userId !== me.id) {
23+
throw new GqlInputError('you do not own this sub')
24+
}
25+
domain = domain.trim() // protect against trailing spaces
26+
if (domain && !validateSchema(customDomainSchema, { domain })) {
27+
throw new GqlInputError('Invalid domain format')
28+
}
29+
30+
if (domain) {
31+
const existing = await models.customDomain.findUnique({ where: { subName } })
32+
if (existing) {
33+
if (domain === existing.domain) {
34+
throw new GqlInputError('domain already set')
35+
}
36+
return await models.customDomain.update({
37+
where: { subName },
38+
data: { domain, dnsState: 'PENDING', sslState: 'PENDING' }
39+
})
40+
} else {
41+
return await models.customDomain.create({
42+
data: {
43+
domain,
44+
dnsState: 'PENDING',
45+
verificationTxt: randomBytes(32).toString('base64'), // TODO: explore other options
46+
sub: {
47+
connect: { name: subName }
48+
}
49+
}
50+
})
51+
}
52+
} else {
53+
return await models.customDomain.delete({ where: { subName } })
54+
}
55+
}
56+
}
57+
}

api/resolvers/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { GraphQLScalarType, Kind } from 'graphql'
2020
import { createIntScalar } from 'graphql-scalar'
2121
import paidAction from './paidAction'
2222
import vault from './vault'
23+
import domain from './domain'
2324

2425
const date = new GraphQLScalarType({
2526
name: 'Date',
@@ -56,4 +57,4 @@ const limit = createIntScalar({
5657

5758
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
5859
upload, search, growth, rewards, referrals, price, admin, blockHeight, chainFee,
59-
{ JSONObject }, { Date: date }, { Limit: limit }, paidAction, vault]
60+
domain, { JSONObject }, { Date: date }, { Limit: limit }, paidAction, vault]

api/typeDefs/domain.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { gql } from 'graphql-tag'
2+
3+
export default gql`
4+
extend type Query {
5+
customDomain(subName: String!): CustomDomain
6+
}
7+
8+
extend type Mutation {
9+
setCustomDomain(subName: String!, domain: String!): CustomDomain
10+
}
11+
12+
type CustomDomain {
13+
createdAt: Date!
14+
updatedAt: Date!
15+
domain: String!
16+
subName: String!
17+
dnsState: String
18+
sslState: String
19+
certificateArn: String
20+
lastVerifiedAt: Date
21+
verificationCname: String
22+
verificationCnameValue: String
23+
verificationTxt: String
24+
}
25+
`

api/typeDefs/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import blockHeight from './blockHeight'
1919
import chainFee from './chainFee'
2020
import paidAction from './paidAction'
2121
import vault from './vault'
22+
import domain from './domain'
2223

2324
const common = gql`
2425
type Query {
@@ -39,4 +40,4 @@ const common = gql`
3940
`
4041

4142
export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
42-
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, paidAction, vault]
43+
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, domain, paidAction, vault]

api/typeDefs/sub.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,6 @@ export default gql`
77
subs: [Sub!]!
88
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
99
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
10-
customDomain(subName: String!): CustomDomain
11-
}
12-
13-
type CustomDomain {
14-
createdAt: Date!
15-
updatedAt: Date!
16-
domain: String!
17-
subName: String!
18-
dnsState: String
19-
sslState: String
20-
certificateArn: String
21-
lastVerifiedAt: Date
22-
verificationCname: String
23-
verificationCnameValue: String
24-
verificationTxt: String
2510
}
2611
2712
type Subs {
@@ -43,7 +28,6 @@ export default gql`
4328
replyCost: Int!, postTypes: [String!]!,
4429
billingType: String!, billingAutoRenew: Boolean!,
4530
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
46-
setCustomDomain(subName: String!, domain: String!): CustomDomain
4731
}
4832
4933
type Sub {

components/territory-domains.js

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,11 @@
11
import { Badge } from 'react-bootstrap'
22
import { Form, Input, SubmitButton } from './form'
3-
import { gql, useMutation, useQuery } from '@apollo/client'
3+
import { useMutation, useQuery } from '@apollo/client'
44
import { customDomainSchema } from '@/lib/validate'
55
import ActionTooltip from './action-tooltip'
66
import { useToast } from '@/components/toast'
77
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
8-
9-
const SET_CUSTOM_DOMAIN = gql`
10-
mutation SetCustomDomain($subName: String!, $domain: String!) {
11-
setCustomDomain(subName: $subName, domain: $domain) {
12-
domain
13-
dnsState
14-
sslState
15-
}
16-
}
17-
`
18-
19-
const GET_CUSTOM_DOMAIN = gql`
20-
query CustomDomain($subName: String!) {
21-
customDomain(subName: $subName) {
22-
domain
23-
dnsState
24-
sslState
25-
verificationCname
26-
verificationCnameValue
27-
verificationTxt
28-
lastVerifiedAt
29-
}
30-
}
31-
`
8+
import { GET_CUSTOM_DOMAIN, SET_CUSTOM_DOMAIN } from '@/fragments/domains'
329

3310
// TODO: clean this up
3411
export default function CustomDomainForm ({ sub }) {

fragments/domains.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { gql } from 'graphql-tag'
2+
3+
export const GET_CUSTOM_DOMAIN = gql`
4+
query CustomDomain($subName: String!) {
5+
customDomain(subName: $subName) {
6+
domain
7+
dnsState
8+
sslState
9+
verificationCname
10+
verificationCnameValue
11+
verificationTxt
12+
lastVerifiedAt
13+
}
14+
}
15+
`
16+
17+
export const GET_CUSTOM_DOMAIN_FULL = gql`
18+
${GET_CUSTOM_DOMAIN}
19+
fragment CustomDomainFull on CustomDomain {
20+
...CustomDomainFields
21+
certificateArn
22+
}
23+
`
24+
25+
export const SET_CUSTOM_DOMAIN = gql`
26+
mutation SetCustomDomain($subName: String!, $domain: String!) {
27+
setCustomDomain(subName: $subName, domain: $domain) {
28+
domain
29+
dnsState
30+
sslState
31+
}
32+
}
33+
`

lib/domains.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export async function certDetails (certificateArn) {
4646
}
4747
}
4848

49+
// TODO: Test with real values, localstack don't have this info until the certificate is issued
4950
export async function getValidationValues (certificateArn) {
5051
const certificate = await certDetails(certificateArn)
5152
console.log(certificate.DomainValidationOptions)

middleware.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse, URLPattern } from 'next/server'
22
import { cachedFetcher } from '@/lib/fetch'
3+
34
const referrerPattern = new URLPattern({ pathname: ':pathname(*)/r/:referrer([\\w_]+)' })
45
const itemPattern = new URLPattern({ pathname: '/items/:id(\\d+){/:other(\\w+)}?' })
56
const profilePattern = new URLPattern({ pathname: '/:name([\\w_]+){/:type(\\w+)}?' })
@@ -13,10 +14,11 @@ const SN_REFERRER_NONCE = 'sn_referrer_nonce'
1314
const SN_REFEREE_LANDING = 'sn_referee_landing'
1415

1516
const TERRITORY_PATHS = ['/~', '/recent', '/random', '/top', '/post', '/edit']
16-
const NO_REWRITE_PATHS = ['/api', '/_next', '/_error', '/404', '/500', '/offline', '/static']
17+
const NO_REWRITE_PATHS = ['/api', '/_next', '/_error', '/404', '/500', '/offline', '/static', '/signup', '/login', '/logout']
1718

19+
// TODO: move this to a separate file
1820
// fetch custom domain mappings from our API, caching it for 5 minutes
19-
const getDomainMappingsCache = cachedFetcher(async function fetchDomainMappings () {
21+
export const getDomainMappingsCache = cachedFetcher(async function fetchDomainMappings () {
2022
const url = `${process.env.NEXT_PUBLIC_URL}/api/domains`
2123
try {
2224
const response = await fetch(url)
@@ -37,6 +39,12 @@ const getDomainMappingsCache = cachedFetcher(async function fetchDomainMappings
3739
keyGenerator: () => 'domain_mappings'
3840
})
3941

42+
// get a domain mapping from the cache
43+
export async function getDomainMapping (domain) {
44+
const domainMappings = await getDomainMappingsCache()
45+
return domainMappings?.[domain]
46+
}
47+
4048
export async function customDomainMiddleware (request, referrerResp) {
4149
const host = request.headers.get('host')
4250
const referer = request.headers.get('referer')
@@ -48,14 +56,13 @@ export async function customDomainMiddleware (request, referrerResp) {
4856

4957
console.log('referer', referer)
5058

51-
const domainMapping = await getDomainMappingsCache()
52-
console.log('domainMapping', domainMapping)
53-
const domainInfo = domainMapping?.[host.toLowerCase()]
59+
const domainInfo = await getDomainMapping(host?.toLowerCase())
5460
if (!domainInfo) {
5561
console.log('Redirecting to main domain')
5662
return NextResponse.redirect(new URL(pathname, mainDomain))
5763
}
5864

65+
// todo: obviously this is not the best way to do this
5966
if (NO_REWRITE_PATHS.some(p => pathname.startsWith(p)) || pathname.includes('.')) {
6067
return NextResponse.next()
6168
}

pages/login.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, mult
2525
console.error('error decoding callback:', callbackUrl, err)
2626
}
2727

28+
// TODO: custom domain mapping
2829
if (external) {
2930
callbackUrl = '/'
3031
}

0 commit comments

Comments
 (0)