Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Domains for Territories #1958

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bd72179
Middleware for Custom Domains; Sync auth if coming from main domain; …
Soxasora Mar 8, 2025
2acbb44
Middleware tweaks; TODOs
Soxasora Mar 9, 2025
fa3b9a2
Merge branch 'master' into custom_domain
Soxasora Mar 9, 2025
cada0ee
values from customDomain in Territory Edit, domain string validation
Soxasora Mar 9, 2025
7bb6166
Fetch and cache all verified domains; allow only verified domains
Soxasora Mar 9, 2025
003ebe9
wip Custom Domain form for Territory Edit; fix endpoint typo; upsert …
Soxasora Mar 9, 2025
a39e5b0
Merge branch 'master' into custom_domain
Soxasora Mar 10, 2025
c930106
fix Sorts active key; referrer cookies workaround; structure for terr…
Soxasora Mar 10, 2025
5072aa1
don't show territory selector; don't redirect to main domain
Soxasora Mar 10, 2025
04df5d5
consider referrer cookies
Soxasora Mar 10, 2025
f4f37c3
check validity of CNAME and TXT records via worker; clean sub-select;…
Soxasora Mar 11, 2025
cf27645
DNS Resolver env setting, fixes inconsistencies with results
Soxasora Mar 11, 2025
3ac04a6
custom domain form and validation; FAILED verification only if it was…
Soxasora Mar 11, 2025
2a9297d
fix form submit; update Sub onSubmit
Soxasora Mar 13, 2025
f959c7f
wip One-click single sign on
Soxasora Mar 13, 2025
3947ff8
Merge branch 'master' into custom_domain
Soxasora Mar 14, 2025
9552bf8
issue and check SSL with worker; refactor customDomain; early ACM imp…
Soxasora Mar 14, 2025
1d04948
adjust schema and worker validation
Soxasora Mar 15, 2025
d906afa
Merge branch 'master' into custom_domain
Soxasora Mar 16, 2025
25bafc0
switch to AWS localstack; mock ACM integration; add certificate DNS v…
Soxasora Mar 17, 2025
390a15d
Merge branch 'master' into custom_domain
Soxasora Mar 18, 2025
0c79d9f
consequential domain verification flow
Soxasora Mar 18, 2025
23bba32
poll every 30 seconds SSL and DNS verification states
Soxasora Mar 18, 2025
76df54a
refactor custom domain queries, todo dynamic callback for login
Soxasora Mar 19, 2025
6db07b8
fix login flow, temporarily disable auto-auth, fix OAuth login
Soxasora Mar 21, 2025
d4d2a70
Merge branch 'master' into custom_domain
Soxasora Mar 22, 2025
4d6d659
middleware multiAuth support, referrer redirect support
Soxasora Mar 23, 2025
81f1550
refetch and start polling on domain update; domain verification tweak…
Soxasora Mar 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
PERSISTENCE=1
SKIP_SSL_CERT_DOWNLOAD=1
LOCALSTACK_ENDPOINT=http://localhost:4566

# tor proxy
TOR_PROXY=http://tor:7050/
Expand All @@ -183,4 +184,7 @@ CPU_SHARES_IMPORTANT=1024
CPU_SHARES_MODERATE=512
CPU_SHARES_LOW=256

NEXT_TELEMETRY_DISABLED=1
NEXT_TELEMETRY_DISABLED=1

# DNS resolver for custom domain verification
DNS_RESOLVER=1.1.1.1
3 changes: 3 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ DB_APP_CONNECTION_LIMIT=4
DB_WORKER_CONNECTION_LIMIT=2
DB_TRANSACTION_TIMEOUT=10000
NEXT_TELEMETRY_DISABLED=1

# DNS resolver for custom domain verification
DNS_RESOLVER=1.1.1.1
50 changes: 50 additions & 0 deletions api/acm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import AWS from 'aws-sdk'
// TODO: boilerplate

AWS.config.update({
region: 'us-east-1'
})

const config = {
s3ForcePathStyle: process.env.NODE_ENV === 'development'
}

export async function requestCertificate (domain) {
// for local development, we use the LOCALSTACK_ENDPOINT which
// is reachable from the host machine
if (process.env.NODE_ENV === 'development') {
config.endpoint = process.env.LOCALSTACK_ENDPOINT
}

// TODO: Research real values
const acm = new AWS.ACM(config)
const params = {
DomainName: domain,
ValidationMethod: 'DNS',
Tags: [
{
Key: 'ManagedBy',
Value: 'stackernews'
}
]
}

const certificate = await acm.requestCertificate(params).promise()
return certificate.CertificateArn
}

export async function describeCertificate (certificateArn) {
// for local development, we use the LOCALSTACK_ENDPOINT which
// is reachable from the host machine
if (process.env.NODE_ENV === 'development') {
config.endpoint = process.env.LOCALSTACK_ENDPOINT
}
const acm = new AWS.ACM(config)
const certificate = await acm.describeCertificate({ CertificateArn: certificateArn }).promise()
return certificate
}

export async function getCertificateStatus (certificateArn) {
const certificate = await describeCertificate(certificateArn)
return certificate.Certificate.Status
}
62 changes: 62 additions & 0 deletions api/resolvers/domain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { validateSchema, customDomainSchema } from '@/lib/validate'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { randomBytes } from 'node:crypto'

export default {
Query: {
customDomain: async (parent, { subName }, { models }) => {
return models.customDomain.findUnique({ where: { subName } })
}
},
Mutation: {
setCustomDomain: async (parent, { subName, domain }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}

const sub = await models.sub.findUnique({ where: { name: subName } })
if (!sub) {
throw new GqlInputError('sub not found')
}

if (sub.userId !== me.id) {
throw new GqlInputError('you do not own this sub')
}
domain = domain.trim() // protect against trailing spaces
if (domain && !validateSchema(customDomainSchema, { domain })) {
throw new GqlInputError('Invalid domain format')
}

if (domain) {
const existing = await models.customDomain.findUnique({ where: { subName } })
if (existing && existing.domain === domain) {
throw new GqlInputError('domain already set')
}
return await models.customDomain.upsert({
where: { subName },
update: {
domain,
dnsState: 'PENDING',
sslState: 'WAITING',
certificateArn: null
},
create: {
domain,
dnsState: 'PENDING',
verificationTxt: randomBytes(32).toString('base64'),
sub: {
connect: { name: subName }
}
}
})
} else {
try {
return await models.customDomain.delete({ where: { subName } })
} catch (error) {
console.error(error)
throw new GqlInputError('failed to delete domain')
}
}
}
}
}
3 changes: 2 additions & 1 deletion api/resolvers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { GraphQLScalarType, Kind } from 'graphql'
import { createIntScalar } from 'graphql-scalar'
import paidAction from './paidAction'
import vault from './vault'
import domain from './domain'

const date = new GraphQLScalarType({
name: 'Date',
Expand Down Expand Up @@ -56,4 +57,4 @@ const limit = createIntScalar({

export default [user, item, message, wallet, lnurl, notifications, invite, sub,
upload, search, growth, rewards, referrals, price, admin, blockHeight, chainFee,
{ JSONObject }, { Date: date }, { Limit: limit }, paidAction, vault]
domain, { JSONObject }, { Date: date }, { Limit: limit }, paidAction, vault]
3 changes: 3 additions & 0 deletions api/resolvers/sub.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ export default {

return sub.SubSubscription?.length > 0
},
customDomain: async (sub, args, { models }) => {
return models.customDomain.findUnique({ where: { subName: sub.name } })
},
createdAt: sub => sub.createdAt || sub.created_at
}
}
Expand Down
25 changes: 25 additions & 0 deletions api/typeDefs/domain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { gql } from 'graphql-tag'

export default gql`
extend type Query {
customDomain(subName: String!): CustomDomain
}
extend type Mutation {
setCustomDomain(subName: String!, domain: String!): CustomDomain
}
type CustomDomain {
createdAt: Date!
updatedAt: Date!
domain: String!
subName: String!
dnsState: String
sslState: String
certificateArn: String
lastVerifiedAt: Date
verificationCname: String
verificationCnameValue: String
verificationTxt: String
}
`
3 changes: 2 additions & 1 deletion api/typeDefs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import blockHeight from './blockHeight'
import chainFee from './chainFee'
import paidAction from './paidAction'
import vault from './vault'
import domain from './domain'

const common = gql`
type Query {
Expand All @@ -39,4 +40,4 @@ const common = gql`
`

export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, paidAction, vault]
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, domain, paidAction, vault]
2 changes: 1 addition & 1 deletion api/typeDefs/sub.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default gql`
nposts(when: String, from: String, to: String): Int!
ncomments(when: String, from: String, to: String): Int!
meSubscription: Boolean!

customDomain: CustomDomain
optional: SubOptional!
}

Expand Down
2 changes: 2 additions & 0 deletions components/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export default function Login ({ providers, callbackUrl, multiAuth, error, text,
text={`${text || 'Login'} with`}
/>
)
case 'Sync': // TODO: remove this
return null
default:
return (
<OverlayTrigger
Expand Down
31 changes: 24 additions & 7 deletions components/nav/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import NoteIcon from '../../svgs/notification-4-fill.svg'
import { useMe } from '../me'
import { abbrNum } from '../../lib/format'
import { useServiceWorker } from '../serviceworker'
import { signOut } from 'next-auth/react'
import { signIn, signOut } from 'next-auth/react'
import Badges from '../badge'
import { randInRange } from '../../lib/rand'
import { useLightning } from '../lightning'
Expand Down Expand Up @@ -252,18 +252,30 @@ export function SignUpButton ({ className = 'py-0', width }) {

export default function LoginButton () {
const router = useRouter()
const handleLogin = useCallback(async pathname => await router.push({
pathname,
query: { callbackUrl: window.location.origin + router.asPath }
}), [router])

// TODO: alternative to this, for test only
useEffect(() => {
console.log(router.query)
if (router.query.type === 'sync') {
signIn('sync', { token: router.query.token, callbackUrl: router.query.callbackUrl, redirect: false })
}
}, [router.query])

const handleLogin = useCallback(async () => {
// normal login on main domain
await router.push({
pathname: '/login',
query: { callbackUrl: window.location.origin + router.asPath }
})
}, [router])

return (
<Button
className='align-items-center px-3 py-1'
id='login'
style={{ borderWidth: '2px', width: SWITCH_ACCOUNT_BUTTON_WIDTH }}
variant='outline-grey-darkmode'
onClick={() => handleLogin('/login')}
onClick={handleLogin}
>
login
</Button>
Expand All @@ -275,6 +287,11 @@ function LogoutObstacle ({ onClose }) {
const { removeLocalWallets } = useWallets()
const { nextAccount } = useAccounts()
const router = useRouter()
const [isCustomDomain, setIsCustomDomain] = useState(false)

useEffect(() => {
setIsCustomDomain(router.host !== process.env.NEXT_PUBLIC_URL.replace(/^https?:\/\//, ''))
}, [router.host])

return (
<div className='d-flex m-auto flex-column w-fit-content'>
Expand Down Expand Up @@ -306,7 +323,7 @@ function LogoutObstacle ({ onClose }) {

removeLocalWallets()

await signOut({ callbackUrl: '/' })
await signOut({ callbackUrl: '/', redirect: !isCustomDomain })
}}
>
logout
Expand Down
8 changes: 5 additions & 3 deletions components/nav/desktop/second-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { NavSelect, PostItem, Sorts, hasNavSelect } from '../common'
import styles from '../../header.module.css'

export default function SecondBar (props) {
const { prefix, topNavKey, sub } = props
const { prefix, topNavKey, isCustomDomain, sub } = props
if (!hasNavSelect(props)) return null
return (
<Navbar className='pt-0 pb-2'>
<Nav
className={styles.navbarNav}
activeKey={topNavKey}
>
<NavSelect sub={sub} size='medium' className='me-1' />
<div className='ms-2 d-flex'><Sorts {...props} className='ms-1' /></div>
{!isCustomDomain && <NavSelect sub={sub} size='medium' className='me-1' />}
<div className={`${!isCustomDomain ? 'ms-2' : ''} d-flex`}>
<Sorts {...props} className={`${!isCustomDomain ? 'ms-1' : ''}`} />
</div>
<PostItem className='ms-auto me-0 d-none d-md-flex' prefix={prefix} />
</Nav>
</Navbar>
Expand Down
11 changes: 9 additions & 2 deletions components/nav/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ import { PriceCarouselProvider } from './price-carousel'
export default function Navigation ({ sub }) {
const router = useRouter()
const path = router.asPath.split('?')[0]
// TODO: this works but it can be better
const isCustomDomain = sub && !path.includes(`/~${sub}`)
const props = {
prefix: sub ? `/~${sub}` : '',
path,
pathname: router.pathname,
topNavKey: path.split('/')[sub ? 2 : 1] ?? '',
dropNavKey: path.split('/').slice(sub ? 2 : 1).join('/'),
topNavKey: isCustomDomain
? path.split('/')[1] ?? ''
: path.split('/')[sub ? 2 : 1] ?? '',
dropNavKey: isCustomDomain
? path.split('/').slice(1).join('/')
: path.split('/').slice(sub ? 2 : 1).join('/'),
isCustomDomain,
sub
}

Expand Down
3 changes: 2 additions & 1 deletion components/nav/mobile/top-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import styles from '../../header.module.css'
import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, hasNavSelect } from '../common'
import { useMe } from '@/components/me'

export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey, isCustomDomain }) {
const { me } = useMe()
if (hasNavSelect({ path, pathname }) && isCustomDomain) return null
return (
<Navbar>
<Nav
Expand Down
Loading