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 && (