Skip to content

Commit d70e25b

Browse files
authored
fix: handle subdomains & scroll for custom domain dialog (#889)
* only show A record if subdomain * Update VerifyStep.tsx * Update VerifyStep.tsx * Update VerifyStep.tsx * use tldts for domain extraction * biome formatting * style update
1 parent cc5ecbf commit d70e25b

File tree

7 files changed

+83
-45
lines changed

7 files changed

+83
-45
lines changed

apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/VerifyStep.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import clsx from "clsx";
33
import { Check, Copy } from "lucide-react";
44
import { useEffect, useState } from "react";
55
import { toast } from "sonner";
6+
import { parse } from "tldts";
67
import { useDashboardContext } from "@/app/(org)/dashboard/Contexts";
78
import type { DomainConfig, DomainVerification } from "./types";
89

@@ -54,6 +55,25 @@ const VerifyStep = ({
5455
return [];
5556
};
5657

58+
const isSubdomain = (raw: string): boolean => {
59+
// Normalize and extract host (no scheme, path, port, or trailing dot)
60+
const input =
61+
raw
62+
.trim()
63+
.replace(/^https?:\/\//i, "")
64+
.split("/")[0] ?? "";
65+
if (!input) return false;
66+
const host = (input.replace(/\.$/, "").split(":")[0] || "").toLowerCase();
67+
try {
68+
// Prefer PSL-backed parsing for correctness (e.g., co.uk, com.au)
69+
const { subdomain } = parse(host);
70+
return Boolean(subdomain);
71+
} catch {
72+
// Fallback: conservative heuristic
73+
const parts = host.split(".");
74+
return parts.length > 2;
75+
}
76+
};
5777
const recommendedAValues = getRecommendedAValues();
5878

5979
// Check if DNS records are already correctly configured
@@ -63,8 +83,8 @@ const VerifyStep = ({
6383
const cnameConfigured =
6484
recommendedCnames.length > 0 &&
6585
recommendedCnames.some((rec) => currentCnames.includes(rec.value));
66-
67-
const showARecord = recommendedARecord && !aRecordConfigured;
86+
const showARecord =
87+
recommendedAValues.length > 0 && !aRecordConfigured && !isSubdomain(domain);
6888
const showCNAMERecord = hasRecommendedCNAME && !cnameConfigured;
6989
const showTXTRecord = hasTXTVerification && !isVerified;
7090

@@ -124,7 +144,7 @@ const VerifyStep = ({
124144
) : (
125145
!isVerified &&
126146
domainConfig && (
127-
<div className="space-y-4">
147+
<div className="custom-scroll px-1 h-full max-h-[300px] space-y-4">
128148
{/* TXT Record Configuration for Verification */}
129149
{showTXTRecord && (
130150
<div className="overflow-hidden rounded-lg border border-gray-4">

apps/web/app/api/playlist/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class Api extends HttpApi.make("CapWebApi").add(
4141
.addError(HttpApiError.InternalServerError)
4242
.addError(HttpApiError.NotFound),
4343
),
44-
) { }
44+
) {}
4545

4646
const ApiLive = HttpApiBuilder.api(Api).pipe(
4747
Layer.provide(
@@ -198,11 +198,13 @@ const getPlaylistResponse = (
198198
const generatedPlaylist = generateMasterPlaylist(
199199
videoMetadata?.Metadata?.resolution ?? "",
200200
videoMetadata?.Metadata?.bandwidth ?? "",
201-
`${serverEnv().WEB_URL}/api/playlist?userId=${video.ownerId
201+
`${serverEnv().WEB_URL}/api/playlist?userId=${
202+
video.ownerId
202203
}&videoId=${video.id}&videoType=video`,
203204
audioMetadata
204-
? `${serverEnv().WEB_URL}/api/playlist?userId=${video.ownerId
205-
}&videoId=${video.id}&videoType=audio`
205+
? `${serverEnv().WEB_URL}/api/playlist?userId=${
206+
video.ownerId
207+
}&videoId=${video.id}&videoType=audio`
206208
: null,
207209
);
208210

apps/web/app/api/video/delete/route.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
HttpApiEndpoint,
77
HttpApiError,
88
HttpApiGroup,
9-
HttpServerResponse,
109
} from "@effect/platform";
1110
import { Effect, Layer, Schema } from "effect";
1211
import { apiToHandler } from "@/lib/server";
@@ -19,7 +18,7 @@ class Api extends HttpApi.make("Api").add(
1918
.addError(HttpApiError.Forbidden)
2019
.addError(HttpApiError.NotFound),
2120
),
22-
) { }
21+
) {}
2322

2423
const ApiLive = HttpApiBuilder.api(Api).pipe(
2524
Layer.provide(

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"subtitles-parser-vtt": "^0.1.0",
114114
"tailwind-merge": "^2.6.0",
115115
"tailwindcss-animate": "^1.0.7",
116+
"tldts": "^7.0.11",
116117
"uuid": "^9.0.1",
117118
"zod": "^3.25.76"
118119
},

packages/web-backend/src/S3Buckets/index.ts

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,26 @@ export class S3Buckets extends Effect.Service<S3Buckets>()("S3Buckets", {
1313
const repo = yield* S3BucketsRepo;
1414

1515
const defaultConfigs = {
16-
publicEndpoint:
17-
yield* Config.string("S3_PUBLIC_ENDPOINT").pipe(
18-
Config.orElse(() => Config.string("CAP_AWS_ENDPOINT")),
19-
Config.option,
20-
Effect.flatten,
21-
Effect.catchTag("NoSuchElementException", () =>
22-
Effect.dieMessage(
23-
"Neither S3_PUBLIC_ENDPOINT nor CAP_AWS_ENDPOINT provided",
24-
),
16+
publicEndpoint: yield* Config.string("S3_PUBLIC_ENDPOINT").pipe(
17+
Config.orElse(() => Config.string("CAP_AWS_ENDPOINT")),
18+
Config.option,
19+
Effect.flatten,
20+
Effect.catchTag("NoSuchElementException", () =>
21+
Effect.dieMessage(
22+
"Neither S3_PUBLIC_ENDPOINT nor CAP_AWS_ENDPOINT provided",
2523
),
2624
),
27-
internalEndpoint:
28-
yield* Config.string("S3_INTERNAL_ENDPOINT").pipe(
29-
Config.orElse(() => Config.string("CAP_AWS_ENDPOINT")),
30-
Config.option,
31-
Effect.flatten,
32-
Effect.catchTag("NoSuchElementException", () =>
33-
Effect.dieMessage(
34-
"Neither S3_INTERNAL_ENDPOINT nor CAP_AWS_ENDPOINT provided",
35-
),
25+
),
26+
internalEndpoint: yield* Config.string("S3_INTERNAL_ENDPOINT").pipe(
27+
Config.orElse(() => Config.string("CAP_AWS_ENDPOINT")),
28+
Config.option,
29+
Effect.flatten,
30+
Effect.catchTag("NoSuchElementException", () =>
31+
Effect.dieMessage(
32+
"Neither S3_INTERNAL_ENDPOINT nor CAP_AWS_ENDPOINT provided",
3633
),
3734
),
35+
),
3836
region: yield* Config.string("CAP_AWS_REGION"),
3937
accessKey: yield* Config.string("CAP_AWS_ACCESS_KEY"),
4038
secretKey: yield* Config.string("CAP_AWS_SECRET_KEY"),
@@ -181,4 +179,4 @@ export class S3Buckets extends Effect.Service<S3Buckets>()("S3Buckets", {
181179
};
182180
}),
183181
dependencies: [S3BucketsRepo.Default],
184-
}) { }
182+
}) {}

packages/web-backend/src/Videos/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ export class Videos extends Effect.Service<Videos>()("Videos", {
1818
// This is only for external use since it does an access check,
1919
// internal use should prefer the repo directly
2020
getById: (id: Video.VideoId) =>
21-
repo.getById(id).pipe(
22-
Policy.withPublicPolicy(policy.canView(id)),
23-
Effect.withSpan("Videos.getById"),
24-
),
21+
repo
22+
.getById(id)
23+
.pipe(
24+
Policy.withPublicPolicy(policy.canView(id)),
25+
Effect.withSpan("Videos.getById"),
26+
),
2527

2628
/*
2729
* Delete a video. Will fail if the user does not have access.
@@ -104,4 +106,4 @@ export class Videos extends Effect.Service<Videos>()("Videos", {
104106
};
105107
}),
106108
dependencies: [VideosPolicy.Default, VideosRepo.Default, S3Buckets.Default],
107-
}) { }
109+
}) {}

pnpm-lock.yaml

Lines changed: 28 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)