diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml
new file mode 100644
index 000000000..b3b8a06ef
--- /dev/null
+++ b/.github/workflows/close-stale-issues.yml
@@ -0,0 +1,23 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: "30 1 * * *" #once a day at 1:30am
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v5
+ with:
+ days-before-issue-stale: 15
+ days-before-issue-close: 15
+ stale-issue-label: "🕰️Stale"
+ exempt-issue-labels: "⏳Postpone"
+ stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
+ close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
+ days-before-pr-stale: -1
+ days-before-pr-close: -1
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index f4b2afd24..7c1c2913b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
👉Looking for Jitsu Classic? Switch to
-classic branch , and read about Jitsu Classic and Jitsu Next differences
+classic branch , and read about Jitsu Classic and Jitsu Next differences
diff --git a/webapps/console/components/ServicesCatalog/ServicesCatalog.tsx b/webapps/console/components/ServicesCatalog/ServicesCatalog.tsx
index a4e077511..b5725eb99 100644
--- a/webapps/console/components/ServicesCatalog/ServicesCatalog.tsx
+++ b/webapps/console/components/ServicesCatalog/ServicesCatalog.tsx
@@ -8,7 +8,7 @@ import { LoadingAnimation } from "../GlobalLoader/GlobalLoader";
import React from "react";
import { ErrorCard } from "../GlobalError/GlobalError";
import { Input } from "antd";
-import { useAppConfig } from "../../lib/context";
+import { useAppConfig, useWorkspace } from "../../lib/context";
function groupByType(sources: SourceType[]): Record {
const groups: Record = {};
@@ -57,6 +57,7 @@ export function getServiceIcon(source: SourceType, icons: Record
export const ServicesCatalog: React.FC<{ onClick: (packageType, packageId: string) => void }> = ({ onClick }) => {
const { data, isLoading, error } = useApi<{ sources: SourceType[] }>(`/api/sources?mode=meta`);
const sourcesIconsLoader = useApi<{ sources: SourceType[] }>(`/api/sources?mode=icons-only`);
+ const workspace = useWorkspace();
const [filter, setFilter] = React.useState("");
const appconfig = useAppConfig();
const sourcesIcons: Record = sourcesIconsLoader.data
@@ -95,6 +96,7 @@ export const ServicesCatalog: React.FC<{ onClick: (packageType, packageId: strin
.filter(
source =>
!appconfig.mitCompliant ||
+ workspace.featuresEnabled.includes("ignore_sources_licenses") ||
source.meta.license?.toLowerCase() === "mit" ||
(source.meta.mitVersions && source.meta.mitVersions.length > 0)
);
diff --git a/webapps/console/lib/api.ts b/webapps/console/lib/api.ts
index 8ee1ab07d..1886b03be 100644
--- a/webapps/console/lib/api.ts
+++ b/webapps/console/lib/api.ts
@@ -76,6 +76,9 @@ function parseIfNeeded(o: any): any {
export function getAuthBearerToken(req: NextApiRequest): string | undefined {
if (req.headers.authorization && req.headers.authorization.toLowerCase().indexOf("bearer ") === 0) {
return req.headers.authorization.substring("bearer ".length);
+ } else if (req.query?.__unsafe_token) {
+ //very unsafe, but some tools we use can't set headers, so we need to allow this
+ return req.query.__unsafe_token as string;
}
return undefined;
}
diff --git a/webapps/console/lib/ee-client.ts b/webapps/console/lib/ee-client.ts
index e76fe0911..06240545d 100644
--- a/webapps/console/lib/ee-client.ts
+++ b/webapps/console/lib/ee-client.ts
@@ -1,5 +1,4 @@
import { get } from "./useApi";
-import { DomainStatus } from "./server/ee";
import * as auth from "firebase/auth";
export type ClassicProjectStatus = {
@@ -11,7 +10,6 @@ export type ClassicProjectStatus = {
};
export interface EeClient {
- attachDomain(domain: string): Promise;
checkClassicProject(): Promise;
createCustomToken(): Promise;
}
@@ -39,15 +37,6 @@ export function getEeClient(host: string, workspaceId: string): EeClient {
return cachedToken;
};
return {
- attachDomain: async domain => {
- cachedToken = await refreshTokenIfNeeded();
- return await get(removeDoubleSlashes(`${host}/api/domain`), {
- query: { domain },
- headers: {
- Authorization: `Bearer ${cachedToken.token}`,
- },
- });
- },
checkClassicProject: async () => {
const fbToken = await auth.getAuth().currentUser?.getIdToken();
return await get(removeDoubleSlashes(`${host}/api/is-active`), {
diff --git a/webapps/console/lib/schema/index.ts b/webapps/console/lib/schema/index.ts
index 56e08c578..d87f0f2b6 100644
--- a/webapps/console/lib/schema/index.ts
+++ b/webapps/console/lib/schema/index.ts
@@ -56,6 +56,7 @@ export const AppConfig = z.object({
//iso date
readOnlyUntil: z.string().optional(),
disableSignup: z.boolean().optional(),
+ customDomainsEnabled: z.boolean().optional(),
ee: z.object({
available: z.boolean(),
host: z.string().optional(),
diff --git a/webapps/console/lib/server/custom-domains.ts b/webapps/console/lib/server/custom-domains.ts
index fae3870ba..40cfcebe9 100644
--- a/webapps/console/lib/server/custom-domains.ts
+++ b/webapps/console/lib/server/custom-domains.ts
@@ -1,8 +1,12 @@
import { db } from "./db";
import { StreamConfig } from "../schema";
+import dns from "dns";
+import { getLog } from "juava";
type DomainAvailability = { available: true; usedInWorkspaces?: never } | { available: false; usedInWorkspace: string };
+export const customDomainCnames = process.env.CUSTOM_DOMAIN_CNAMES?.split(",");
+
/**
* Tells if the given domain is used in other workspaces.
*/
@@ -26,3 +30,38 @@ export async function isDomainAvailable(domain: string, workspaceId: string): Pr
return { available: true };
}
}
+
+function resolveCname(domain: string): Promise {
+ return new Promise((resolve, reject) => {
+ dns.resolveCname(domain, (err, addresses) => {
+ if (err) {
+ reject(err);
+ } else {
+ if (addresses.length === 1) {
+ resolve(addresses[0]);
+ } else if (!addresses || addresses.length === 0) {
+ resolve(undefined);
+ } else {
+ getLog()
+ .atWarn()
+ .log(`Domain ${domain} has multiple CNAME records: ${addresses.join(", ")}. Using first one`);
+ resolve(addresses[0]);
+ }
+ }
+ });
+ });
+}
+
+export async function isCnameValid(domain: string): Promise {
+ if (!customDomainCnames || customDomainCnames.length == 0) {
+ throw new Error(`CUSTOM_DOMAIN_CNAMES is not set. isCnameValid() should not be called`);
+ }
+ let cnameRecord: string | undefined;
+ try {
+ cnameRecord = await resolveCname(domain);
+ } catch (e) {
+ getLog().atError().withCause(e).log(`Domain ${domain} has no CNAME records`);
+ return false;
+ }
+ return !!(cnameRecord && customDomainCnames.includes(cnameRecord.toLowerCase()));
+}
diff --git a/webapps/console/lib/server/ee.ts b/webapps/console/lib/server/ee.ts
index c9a16e3ca..b0d8761b8 100644
--- a/webapps/console/lib/server/ee.ts
+++ b/webapps/console/lib/server/ee.ts
@@ -45,13 +45,3 @@ export function createJwt(
const token = jwt.sign({ userId, email, workspaceId, exp: expiresSecondsTimestamp }, jwtSecret);
return { jwt: token, expiresAt: new Date(expiresSecondsTimestamp * 1000).toISOString() };
}
-
-export type DomainStatus = { error?: string } & (
- | { needsConfiguration: false }
- | { needsConfiguration: true; configurationType: "cname"; cnameValue: string }
- | {
- needsConfiguration: true;
- configurationType: "verification";
- verification: { type: string; domain: string; value: string }[];
- }
-);
diff --git a/webapps/console/lib/shared/domain-check-response.ts b/webapps/console/lib/shared/domain-check-response.ts
new file mode 100644
index 000000000..c12cf86ee
--- /dev/null
+++ b/webapps/console/lib/shared/domain-check-response.ts
@@ -0,0 +1,21 @@
+import { z } from "zod";
+import { Simplify } from "type-fest";
+
+export const DomainCheckResponse = z.union([
+ z.object({
+ ok: z.literal(true),
+ reason: z.never().optional(),
+ }),
+ z.object({
+ ok: z.literal(false),
+ reason: z.union([z.literal("used_by_other_workspace"), z.literal("invalid_domain_name")]),
+ cnameValue: z.never().optional(),
+ }),
+ z.object({
+ ok: z.literal(false),
+ reason: z.literal("requires_cname_configuration"),
+ cnameValue: z.string().optional(),
+ }),
+]);
+
+export type DomainCheckResponse = Simplify>;
diff --git a/webapps/console/pages/[workspaceId]/services.tsx b/webapps/console/pages/[workspaceId]/services.tsx
index ac7d1fbaa..4bea39efa 100644
--- a/webapps/console/pages/[workspaceId]/services.tsx
+++ b/webapps/console/pages/[workspaceId]/services.tsx
@@ -155,7 +155,11 @@ const ServicesList: React.FC<{}> = () => {
`/api/sources/versions?type=${packageType}&package=${encodeURIComponent(packageId)}`
);
const versions = rawVersions.versions
- .filter((v: any) => v.isRelease && (v.isMit || !appconfig.mitCompliant))
+ .filter(
+ (v: any) =>
+ v.isRelease &&
+ (v.isMit || !appconfig.mitCompliant || workspace.featuresEnabled.includes("ignore_sources_licenses"))
+ )
.map((v: any) => v.name);
const sourceType = await rpc(`/api/sources/${packageType}/${encodeURIComponent(packageId)}`);
diff --git a/webapps/console/pages/[workspaceId]/streams.tsx b/webapps/console/pages/[workspaceId]/streams.tsx
index 8829a12cb..61a88ae6f 100644
--- a/webapps/console/pages/[workspaceId]/streams.tsx
+++ b/webapps/console/pages/[workspaceId]/streams.tsx
@@ -15,7 +15,6 @@ import { getEeClient } from "../../lib/ee-client";
import { assertDefined, requireDefined } from "juava";
import { ReloadOutlined } from "@ant-design/icons";
import { confirmOp, feedbackError } from "../../lib/ui";
-import type { DomainStatus } from "../../lib/server/ee";
import { getAntdModal, useAntdModal } from "../../lib/modal";
import { get } from "../../lib/useApi";
import { Activity, AlertTriangle, Check, Globe, Wrench, Zap } from "lucide-react";
@@ -27,6 +26,7 @@ import { useLinksQuery } from "../../lib/queries";
import { toURL } from "../../lib/shared/url";
import JSON5 from "json5";
import { EditorToolbar } from "../../components/EditorToolbar/EditorToolbar";
+import { DomainCheckResponse } from "../../lib/shared/domain-check-response";
const Streams: React.FC = () => {
return (
@@ -77,10 +77,10 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise
);
const [reloadTrigger, setReloadTrigger] = useState(0);
const [deleting, setDeleting] = useState(false);
- const { data, isLoading, error, refetch } = useQuery(
+ const { data, isLoading, error, refetch } = useQuery(
["domain-status", domain.toLowerCase(), reloadTrigger],
async () => {
- return await eeClient.attachDomain(domain);
+ return await get(`/api/${workspace.id}/domain-check?domain=${domain.toLowerCase()}`);
},
{ cacheTime: 0 }
);
@@ -94,9 +94,7 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise
{/**/}
{domain}
@@ -113,14 +111,14 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise
- {data?.needsConfiguration && (
+ {!data?.ok && (
{
- DomainConfigurationInstructions.show({ domain, status: data });
+ DomainConfigurationInstructions.show({ domain, status: data! });
}}
className="border-0"
>
@@ -174,29 +172,29 @@ const CustomDomain: React.FC<{ domain: string; deleteDomain: () => Promise
);
- } else if (error || data?.error) {
+ } else if (error) {
return ERROR ;
- } else if (data?.needsConfiguration) {
+ } else if (!data?.ok) {
return Configuration Required ;
} else {
return OK ;
}
})()}
- {(error || data?.error) && (
+ {error && (
Description:
-
{`${data?.error || "Internal error"}`}
+
{`${"Internal error"}`}
)}
- {data?.needsConfiguration && (
+ {!data?.ok && (
Description:
See{" "}
DomainConfigurationInstructions.show({ domain, status: data })}
+ onClick={() => DomainConfigurationInstructions.show({ domain, status: data! })}
>
configuration instructions
@@ -234,25 +232,16 @@ export const DNSRecordTable: React.FC
= ({ records }) => {
);
};
-export type DomainInstructionsProps = { domain: string; status: DomainStatus };
+export type DomainInstructionsProps = { domain: string; status: DomainCheckResponse };
const DomainConfigurationInstructions: React.FC & {
show: (p: DomainInstructionsProps) => void;
} = ({ domain, status }) => {
- if (status.needsConfiguration && status.configurationType === "cname") {
+ if (status.reason === "requires_cname_configuration") {
return (
Set the following record on your DNS provider to continue
-
-
-
- );
- } else if (status.needsConfiguration && status.configurationType == "verification") {
- return (
-
-
Set the following record on your DNS provider to continue
-
-
+
);
@@ -282,10 +271,24 @@ const DomainsEditor: React.FC> = props => {
const add = async () => {
setAddPending(true);
try {
- const { available } = await get(`/api/${workspace.id}/domain-check?domain=${addValue}`);
- if (!available) {
- feedbackError(`Domain ${addValue} is not available. It is used by other workspace`);
- return;
+ const available: DomainCheckResponse = await get(`/api/${workspace.id}/domain-check?domain=${addValue}`);
+ if (!available.ok) {
+ if (available.reason === "used_by_other_workspace") {
+ feedbackError(
+ <>
+ Domain {addValue}
is not available. It is used by other workspace. Contact{" "}
+ support@jitsu.com
if you think this is a mistake
+ >
+ );
+ return;
+ } else if (available.reason === "invalid_domain_name") {
+ feedbackError(
+ <>
+ Invalid domain name {addValue}
+ >
+ );
+ return;
+ }
}
const newVal = [...domains, addValue as string];
setDomains(newVal);
@@ -544,7 +547,7 @@ const StreamsList: React.FC<{}> = () => {
},
domains: {
editor: DomainsEditor,
- hidden: !appConfig.ee.available,
+ hidden: !appConfig.customDomainsEnabled,
displayName: "Custom Tracking Domains",
documentation: (
<>
diff --git a/webapps/console/pages/api/[workspaceId]/domain-check.ts b/webapps/console/pages/api/[workspaceId]/domain-check.ts
index a8c6ca9c1..22558c2d9 100644
--- a/webapps/console/pages/api/[workspaceId]/domain-check.ts
+++ b/webapps/console/pages/api/[workspaceId]/domain-check.ts
@@ -1,38 +1,41 @@
-import { Api, inferUrl, nextJsApiHandler, verifyAccess } from "../../../lib/api";
import { getServerLog } from "../../../lib/server/log";
import { z } from "zod";
-import { isDomainAvailable } from "../../../lib/server/custom-domains";
+import { customDomainCnames, isCnameValid, isDomainAvailable } from "../../../lib/server/custom-domains";
+import { DomainCheckResponse } from "../../../lib/shared/domain-check-response";
+import { createRoute, verifyAccess } from "../../../lib/api";
const log = getServerLog("custom-domains");
-export const api: Api = {
- url: inferUrl(__filename),
- GET: {
+export default createRoute()
+ .GET({
auth: true,
- types: {
- query: z.object({
- workspaceId: z.string(),
- domain: z.string(),
- }),
- result: z.object({
- available: z.boolean(),
- }),
- },
- handle: async ({ user, query }) => {
- await verifyAccess(user, query.workspaceId);
- const domainAvailability = await isDomainAvailable(query.domain, query.workspaceId);
- if (!domainAvailability.available) {
- log
- .atWarn()
- .log(
- `Domain '${query.domain}' can't be added to workspace ${query.workspaceId}. It is used by ${domainAvailability.usedInWorkspace}`
- );
- return { available: false };
- }
- return { available: true };
- },
- },
-};
+ query: z.object({
+ workspaceId: z.string(),
+ domain: z.string(),
+ }),
+ result: DomainCheckResponse,
+ })
+ .handler(async ({ user, query: { workspaceId, domain } }) => {
+ if (!customDomainCnames || customDomainCnames.length == 0) {
+ throw new Error(`CUSTOM_DOMAIN_CNAMES is not set`);
+ }
+ await verifyAccess(user, workspaceId);
+ const domainAvailability = await isDomainAvailable(domain, workspaceId);
+ if (!domainAvailability.available) {
+ log
+ .atWarn()
+ .log(
+ `Domain '${domain}' can't be added to workspace ${workspaceId}. It is used by ${domainAvailability.usedInWorkspace}`
+ );
+ return { ok: false, reason: "used_by_other_workspace" };
+ }
-export default nextJsApiHandler(api);
+ const cnameValid = await isCnameValid(domain);
+ if (!cnameValid) {
+ log.atWarn().log(`Domain ${domain} is not valid`);
+ return { ok: false, reason: "requires_cname_configuration", cnameValue: customDomainCnames[0] };
+ }
+ return { ok: true };
+ })
+ .toNextApiHandler();
diff --git a/webapps/console/pages/api/admin/domains-report.ts b/webapps/console/pages/api/admin/domains-report.ts
new file mode 100644
index 000000000..14ac86681
--- /dev/null
+++ b/webapps/console/pages/api/admin/domains-report.ts
@@ -0,0 +1,57 @@
+import { createRoute } from "../../../lib/api";
+import { db } from "../../../lib/server/db";
+import { assertDefined, assertTrue } from "juava";
+import { isCnameValid } from "../../../lib/server/custom-domains";
+
+export default createRoute()
+ .GET({
+ auth: true,
+ streaming: true,
+ })
+ .handler(async ({ res, user }) => {
+ const userProfile = await db.prisma().userProfile.findFirst({ where: { id: user.internalId } });
+ assertDefined(userProfile, "User profile not found");
+ assertTrue(userProfile.admin, "Not enough permissions");
+
+ const domains = await db.pgPool().query(`
+ select
+ s.id,
+ s.config ->> 'name' as "streamName",
+ s."updatedAt" as "updatedAt",
+ s.config,
+ w.id as "workspaceId"
+ from "ConfigurationObject" s
+ join "Workspace" w on w.id = s."workspaceId"
+ where s.type = 'stream'
+ and s.config ->> 'domains' <> '[]'
+ `);
+
+ const result: any[] = [];
+ const cache: { [key: string]: boolean } = {};
+ res.writeHead(200, {
+ "Content-Type": "application/json",
+ });
+ res.write("[");
+ let hasPrev: boolean = false;
+ for (const row of domains.rows) {
+ for (const domain of row.config.domains) {
+ const validCname = cache[domain] ?? (cache[domain] = await isCnameValid(domain));
+ const resRow = {
+ configured: validCname,
+ domain,
+ lastValidated: row.updatedAt,
+ misconfigurationReason: validCname ? null : "invalid_cname",
+ sourceId: row.id,
+ workspaceId: row.workspaceId,
+ };
+ res.write(`${hasPrev ? "," : ""}${JSON.stringify(resRow)}\n`);
+ hasPrev = true;
+ }
+ }
+ res.write("]");
+ res.end();
+ })
+ .toNextApiHandler();
+export const config = {
+ maxDuration: 120, //2 mins, mostly becasue of workspace-stat call
+};
diff --git a/webapps/console/pages/api/app-config.ts b/webapps/console/pages/api/app-config.ts
index 36b4b18a3..2d7921229 100644
--- a/webapps/console/pages/api/app-config.ts
+++ b/webapps/console/pages/api/app-config.ts
@@ -9,6 +9,7 @@ import { isTruish } from "../../lib/shared/chores";
import { readOnlyUntil } from "../../lib/server/read-only-mode";
import { productTelemetryEnabled, productTelemetryHost } from "../../lib/server/telemetry";
import { mainDataDomain } from "../../lib/server/data-domains";
+import { customDomainCnames } from "../../lib/server/custom-domains";
export default createRoute()
.GET({ result: AppConfig, auth: false })
@@ -31,6 +32,7 @@ export default createRoute()
}
: undefined,
billingEnabled: isEEAvailable(),
+ customDomainsEnabled: customDomainCnames && customDomainCnames.length > 0,
syncs: {
enabled: isTruish(process.env.SYNCS_ENABLED),
scheduler: {
diff --git a/webapps/ee-api/lib/vercel.ts b/webapps/ee-api/lib/vercel.ts
deleted file mode 100644
index 9296e48b8..000000000
--- a/webapps/ee-api/lib/vercel.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { getLog, requireDefined } from "juava";
-import fetch from "node-fetch-commonjs";
-
-export type DomainInfo = Record;
-
-export const vercelProjectId = requireDefined(
- process.env.DOMAINS_VERCEL_PROJECT_ID,
- `env DOMAINS_VERCEL_PROJECT_ID is not set`
-);
-export const vercelTeamId = requireDefined(process.env.DOMAINS_VERCEL_TEAM_ID, `env DOMAINS_VERCEL_TEAM_ID is not set`);
-export const vercelToken = requireDefined(process.env.DOMAINS_VERCEL_TOKEN, `env DOMAINS_VERCEL_TOKEN is not set`);
-export const vercelCname = process.env.CNAME || "cname.jitsu.com";
-
-const log = getLog("vercel");
-
-export async function vercelRpc(url: string, method?: "GET" | "POST", body?: any): Promise {
- const res = await fetch(url.indexOf("https://") === 0 ? url : `https://api.vercel.com${url}`, {
- method: method ?? "GET",
- body: body ? JSON.stringify(body) : undefined,
- headers: {
- Authorization: `Bearer ${vercelToken}`,
- "Content-Type": "application/json",
- },
- });
- const txt = await res.text();
- if (!res.ok) {
- log.atError().log(`Rpc failed: ${res.status} ${res.statusText}. Response: ${txt}`);
- }
- return JSON.parse(txt);
-}
-
-export async function getExistingDomain(domain: string): Promise {
- const result = await vercelRpc(
- `https://api.vercel.com/v9/projects/${vercelProjectId}/domains/${domain}?teamId=${vercelTeamId}`
- );
- if (result.name) {
- return result;
- } else {
- return undefined;
- }
-}
diff --git a/webapps/ee-api/pages/api/domain.ts b/webapps/ee-api/pages/api/domain.ts
deleted file mode 100644
index d64eb1e46..000000000
--- a/webapps/ee-api/pages/api/domain.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import type { NextApiRequest, NextApiResponse } from "next";
-import { auth } from "../../lib/auth";
-import { getErrorMessage, requireDefined } from "juava";
-import dns from "dns";
-import { withErrorHandler } from "../../lib/error-handler";
-import isValidDomain from "is-valid-domain";
-import { getServerLog } from "../../lib/log";
-import { getExistingDomain, vercelCname, vercelProjectId, vercelRpc, vercelTeamId } from "../../lib/vercel";
-
-const log = getServerLog("/api/domain");
-
-function resolveCname(domain: string): Promise {
- return new Promise((resolve, reject) => {
- dns.resolveCname(domain, (err, addresses) => {
- if (err) {
- reject(err);
- } else {
- resolve(addresses);
- }
- });
- });
-}
-
-const alternativeCname = process.env.CUSTOM_DOMAIN_CNAMES?.split(",");
-
-const handler = async function handler(req: NextApiRequest, res: NextApiResponse) {
- log.atDebug().log(`${req.method} ${req.url} ${JSON.stringify(req.headers)}`);
- res.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*");
- res.setHeader("Access-Control-Allow-Methods", "*");
- res.setHeader("Access-Control-Allow-Headers", "authorization, content-type, baggage, sentry-trace");
- if (req.method === "OPTIONS") {
- //allowing requests from everywhere since our tokens are short-lived
- //and can't be hijacked
- res.status(200).end();
- return;
- }
- if (!(await auth(req, res))) {
- return;
- }
- try {
- const domain = requireDefined(req.query.domain as string, `query param domain is not set`);
- if (!isValidDomain(domain, { subdomain: true, wildcard: false })) {
- throw new Error(`Not a valid domain name`);
- }
-
- if (alternativeCname && alternativeCname.length > 0) {
- try {
- const cnames = await resolveCname(domain);
- if (cnames.length > 0) {
- const configuredCname = cnames.find(cname => alternativeCname.includes(cname));
- if (configuredCname) {
- res.status(200).json({ ok: true, needsConfiguration: false, configuredCname });
- return;
- } else {
- res.status(200).json({
- ok: false,
- needsConfiguration: true,
- configurationType: "cname",
- cnameValue: alternativeCname[0],
- });
- return;
- }
- } else {
- if (vercelProjectId) {
- //continue with vercel
- } else {
- res.status(200).json({
- ok: false,
- needsConfiguration: true,
- configurationType: "cname",
- cnameValue: alternativeCname[0],
- });
- }
- }
- } catch (e) {
- if (vercelProjectId) {
- //continue with vercel
- } else {
- res
- .status(200)
- .json({ ok: false, needsConfiguration: true, configurationType: "cname", cnameValue: alternativeCname[0] });
- }
- }
- }
-
- let domainInfo = await getExistingDomain(domain);
- if (!domainInfo) {
- domainInfo = await vercelRpc(`/v10/projects/${vercelProjectId}/domains?teamId=${vercelTeamId}`, "POST", {
- name: domain,
- });
- }
- if (!domainInfo) {
- throw new Error(`Can't get domainInfo for ${domain}`);
- }
- if (domainInfo.error) {
- return res.status(200).json({ ok: false, error: domainInfo.error.message || domainInfo.error });
- }
- const status = await vercelRpc(`/v6/domains/${domain}/config?teamId=${vercelTeamId}`);
- log.atDebug().log(`Checking status of domain ${domain}: ${JSON.stringify({ status, domainInfo }, null, 2)}`);
- const misconfigured = status.misconfigured;
- const verified = domainInfo.verified;
- if (!misconfigured && verified) {
- res.status(200).json({ ok: true, needsConfiguration: false });
- } else if (misconfigured) {
- res.status(200).json({ ok: true, needsConfiguration: true, configurationType: "cname", cnameValue: vercelCname });
- } else if (!verified) {
- //request Vercel to verify domain
- const verifyInfo = await vercelRpc(
- `/v9/projects/${vercelProjectId}/domains/${domain}/verify?teamId=${vercelTeamId}`,
- "POST"
- );
- if (!verifyInfo) {
- throw new Error(`Can't verify ${domain}`);
- }
- if (!verifyInfo.verified) {
- if (!verifyInfo.verification && !domainInfo.verification) {
- throw new Error(`Domain ${domain} is not verified, and there is no verification info`);
- }
- res.status(200).json({
- ok: true,
- needsConfiguration: true,
- configurationType: "verification",
- verification: verifyInfo.verification || domainInfo.verification,
- });
- } else {
- res.status(200).json({ ok: true, needsConfiguration: false });
- }
- } else {
- throw new Error(`Unexpected state: misconfigured=${misconfigured}, verified=${verified} `);
- }
- } catch (e) {
- log.atError().withCause(e).log(`${req.url} failed`);
- return res.status(200).json({ ok: false, error: getErrorMessage(e) });
- }
-};
-export default withErrorHandler(handler);
diff --git a/webapps/ee-api/pages/api/report/domains.ts b/webapps/ee-api/pages/api/report/domains.ts
deleted file mode 100644
index 250f90ad6..000000000
--- a/webapps/ee-api/pages/api/report/domains.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { NextApiRequest, NextApiResponse } from "next";
-import { auth } from "../../../lib/auth";
-import { assertTrue, getLog } from "juava";
-import { applicationDb } from "../../../lib/services";
-import { withErrorHandler } from "../../../lib/error-handler";
-import { getExistingDomain, vercelRpc, vercelTeamId } from "../../../lib/vercel";
-
-const handler = async function handler(req: NextApiRequest, res: NextApiResponse) {
- const claims = await auth(req, res);
- assertTrue(claims?.type === "admin", "Should be admin");
- const queryResult = await applicationDb.query(`
- select
- s.id,
- s.config ->> 'name' as "streamName",
- s."updatedAt" as "updatedAt",
- s.config,
- w.slug as "workspaceSlug",
- w.name as "workspaceName"
- from newjitsu."ConfigurationObject" s
- join newjitsu."Workspace" w on w.id = s."workspaceId"
- where s.type = 'stream'
- and s.config ->> 'domains' <> '[]'
- `);
- const result: any[] = [];
- res.setHeader("Content-Type", "application/json");
- res.status(200);
- try {
- //we need to do it in streaming mode, otherwise Vercel wll timeout
- res.write("[\n");
- let firstRow = true;
- //some domains can be configured for multiple stream, so a little optimization here
- const localCache: Record = {};
- for (const row of queryResult.rows) {
- const domains = row.config.domains;
- for (const domain of domains) {
- let domainStatus: any;
- let domainInfo: any;
- if (localCache[domain]) {
- getLog().atDebug().log(`Domain ${domain} has been already checked`);
- domainStatus = localCache[domain].domainStatus;
- domainInfo = localCache[domain].domainInfo;
- } else {
- getLog().atDebug().log(`Checking domain ${domain}`);
- domainStatus = await vercelRpc(`/v6/domains/${domain}/config?teamId=${vercelTeamId}`);
- domainInfo = (await getExistingDomain(domain)) || {};
- localCache[domain] = { domainStatus, domainInfo };
- getLog()
- .atDebug()
- .log(`Domain ${domain} info:\n${JSON.stringify({ domainStatus, domainInfo }, null, 2)}`);
- }
- let configured: boolean;
- let misconfigurationReason: any = null;
- if (!domainStatus?.misconfigured && domainInfo?.verified) {
- configured = true;
- } else if (domainStatus?.misconfigured) {
- configured = false;
- misconfigurationReason = "cname";
- } else {
- configured = false;
- misconfigurationReason = "verification_required";
- }
- const workspaceLink = `https://use.jitsu.com/${row.workspaceSlug || row.workspaceId}`;
- const resultRow = {
- domain,
- configured,
- misconfigurationReason,
- lastUpdated: domainStatus.updatedAt ? new Date(domainStatus.updatedAt) : row.updatedAt,
- workspaceId: row.workspaceId,
- sourceId: row.id,
- sourceLink: `${workspaceLink}/streams?id=${row.id}`,
- workspaceLink,
- };
- res.write(`${firstRow ? "" : ",\n"}${JSON.stringify(resultRow)}`);
- getLog()
- .atDebug()
- .log(
- `Domain ${domain} is ${configured ? "configured" : "not configured"}. Full info: ${JSON.stringify(
- resultRow
- )}`
- );
- firstRow = false;
- }
- }
- res.write("\n]");
- } finally {
- //no catch block here, since in streaming it's not hard to display error to the user
- res.end();
- }
-};
-
-export const config = {
- maxDuration: 300,
-};
-
-export default withErrorHandler(handler);