Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 8 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ env:
# Used by packages/db to build the client; tests that touch DB logic mock the
# module, so this never needs a real Postgres instance in CI.
DATABASE_URL: postgresql://harly:harly@localhost:5432/harly
NEXT_PUBLIC_APP_URL: http://localhost:3000
HARLY_URL: http://localhost:3000
# Keep CI's public-origin variables routable-looking and non-local. The
# production config intentionally rejects localhost/loopback origins.
NEXT_PUBLIC_APP_URL: https://ci.example.invalid
HARLY_URL: https://ci.example.invalid
DOCUSEAL_SECRET_KEY_BASE: ci-docuseal-secret-key-base-do-not-use-in-production
BETTER_AUTH_SECRET: ci-test-secret-do-not-use-in-production
NODE_ENV: test
Expand Down Expand Up @@ -146,8 +148,8 @@ jobs:
env:
NODE_ENV: production
DATABASE_URL: postgresql://harly:harly@localhost:5432/harly
NEXT_PUBLIC_APP_URL: http://localhost:3000
HARLY_URL: http://localhost:3000
NEXT_PUBLIC_APP_URL: https://ci.example.invalid
HARLY_URL: https://ci.example.invalid
BETTER_AUTH_SECRET: ci-test-secret-do-not-use-in-production
AI_ENCRYPTION_KEY: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
STORAGE_UPLOAD_SECRET: cccccccccccccccccccccccccccccccc
Expand Down Expand Up @@ -359,7 +361,7 @@ jobs:
- name: Validate Compose profiles
run: |
export HARLY_IMAGE="$(node -p 'const r=require("./release-manifest.json"); r.image + "@" + r.digest')"
export HARLY_VERSION="$(node -p 'require("./release-manifest.json").version')" HARLY_URL=http://localhost:3000
export HARLY_VERSION="$(node -p 'require("./release-manifest.json").version')" HARLY_URL=https://ci.example.invalid
export POSTGRES_USER=harly POSTGRES_PASSWORD=harly POSTGRES_DB=harly
export BETTER_AUTH_SECRET=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa AI_ENCRYPTION_KEY=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
export STORAGE_UPLOAD_SECRET=cccccccccccccccccccccccccccccccc CRON_SECRET=dddddddddddddddddddddddddddddddd
Expand Down Expand Up @@ -392,7 +394,7 @@ jobs:
COMPOSE_PROJECT_NAME: harly-ci
HARLY_IMAGE: harly:ci
HARLY_VERSION: ci
HARLY_URL: http://127.0.0.1:3099
HARLY_URL: https://ci.example.invalid
HARLY_PORT: 3099
POSTGRES_USER: harly
POSTGRES_PASSWORD: harly-ci-postgres-password
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 \
NODE_OPTIONS=--max-old-space-size=4096 \
HARLY_DISABLE_WEBPACK_BUILD_WORKER=1 \
HARLY_URL=http://localhost:3000 \
HARLY_URL=https://build.invalid \
DATABASE_URL=postgresql://build:build@127.0.0.1:5432/build \
BETTER_AUTH_SECRET=build-only-better-auth-secret-000000000000 \
AI_ENCRYPTION_KEY=build-only-ai-encryption-key-0000000000000 \
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/(dashboard)/settings/developers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
WEBHOOK_EVENTS,
WEBHOOK_EVENT_LABELS,
} from "@/server/webhooks/events";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const dynamic = "force-dynamic";

Expand All @@ -41,9 +42,7 @@ export default async function DevelopersSettingsPage() {
})),
);

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();

return (
<DevelopersSettings
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/(dashboard)/settings/email/replies/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getWorkspaceContext } from "@/features/workspaces/context";
import { requirePagePermission } from "@/features/workspaces/permissions-server";
import { getWorkspaceInboundEmailStatus } from "@/lib/email/config";
import { getMailboxStatus } from "@/lib/mailbox/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const dynamic = "force-dynamic";

Expand Down Expand Up @@ -35,6 +36,7 @@ export default async function ConfigureReplyHandlingPage() {
inboundStatus={inboundStatus}
workspaceId={organization.id}
initialMode={initialMode}
appUrl={getHarlyPublicOrigin()}
/>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/(dashboard)/settings/portal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getWorkspaceContext } from "@/features/workspaces/context";
import { requirePagePermission } from "@/features/workspaces/permissions-server";
import { CandidatePortalCard } from "@/features/workspaces/CandidatePortalCard";
import { getWorkspaceEmailStatus } from "@/lib/email/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const dynamic = "force-dynamic";

Expand Down Expand Up @@ -46,7 +47,7 @@ export default async function PortalSettingsPage() {
row?.hasLinkedinClientId && row?.hasLinkedinSecret,
)}
linkedinClientId={row?.hasLinkedinClientId ?? ""}
appUrl={process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"}
appUrl={getHarlyPublicOrigin()}
showApplicationStatus={row?.portalShowApplicationStatus ?? true}
showHiringTeam={row?.portalShowHiringTeam ?? false}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { JobActionsMenu } from "@/features/jobs/JobActionsMenu";
import { JobShareButton } from "@/features/jobs/JobShareButton";
import { JobStatusActions } from "@/features/jobs/JobStatusActions";
import { getWorkspaceAiStatus } from "@/lib/ai/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { countCandidatePool } from "@/features/matching/data";
import { getWorkspaceContext } from "@/features/workspaces/context";

Expand Down Expand Up @@ -50,7 +51,7 @@ export default async function DashboardJobPage({
}

const { job } = result;
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
const appUrl = getHarlyPublicOrigin();
const publicUrl = `${appUrl}/jobs/${job.slug}`;

return (
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/integrations/google/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { auth } from "@/lib/auth";
import { encryptSecret } from "@/lib/crypto";
import { createOAuth2Client } from "@/lib/gcal/config";
import { createLogger } from "@/lib/logger";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { requirePermission } from "@/features/workspaces/permissions-server";
import {
verifyAndConsumeOauthStateNonce,
Expand Down Expand Up @@ -121,9 +122,7 @@ export async function GET(req: NextRequest) {
}

function getAppUrl(): string {
return (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
return getHarlyPublicOrigin();
}

function redirectWithError(msg: string) {
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/app/api/integrations/outlook/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { encryptSecret } from "@/lib/crypto";
import { createLogger } from "@/lib/logger";
import { getWorkspaceOutlookCredentials } from "@/lib/outlook/config";
import { getMe } from "@/lib/outlook/client";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { requirePermission } from "@/features/workspaces/permissions-server";
import { verifyAndConsumeOauthStateNonce } from "@/server/oauth-state";

Expand Down Expand Up @@ -70,9 +71,7 @@ export async function GET(req: NextRequest) {
);
}

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/integrations/outlook/callback`;

// Exchange code for tokens
Expand Down Expand Up @@ -146,9 +145,7 @@ export async function GET(req: NextRequest) {
}

function redirectWithError(msg: string) {
const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const url = new URL(`${appUrl}/settings/integrations`);
url.searchParams.set("outlook_error", msg);
return NextResponse.redirect(url.toString());
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/integrations/outlook/install/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse, type NextRequest } from "next/server";

import { auth } from "@/lib/auth";
import { getWorkspaceOutlookCredentials } from "@/lib/outlook/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { requirePermission } from "@/features/workspaces/permissions-server";
import { createInstallState } from "@/server/oauth-state";

Expand Down Expand Up @@ -48,9 +49,7 @@ export async function GET(req: NextRequest) {
);
}

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/integrations/outlook/callback`;

const state = await createInstallState({
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/app/api/integrations/slack/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getWorkspaceSlackCredentials } from "@/lib/slack/config";
import { requirePermission } from "@/features/workspaces/permissions-server";
import { verifyAndConsumeOauthStateNonce } from "@/server/oauth-state";
import { logAuditEvent } from "@/lib/audit-log";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const runtime = "nodejs";

Expand Down Expand Up @@ -65,9 +66,7 @@ export async function GET(req: NextRequest) {
return redirectWithError("Slack credentials not found for this workspace.");
}

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/integrations/slack/callback`;

let tokenRes: Response;
Expand Down Expand Up @@ -160,9 +159,7 @@ export async function GET(req: NextRequest) {
}

function redirectWithError(msg: string) {
const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const url = new URL(`${appUrl}/settings/integrations`);
url.searchParams.set("slack_error", msg);
return NextResponse.redirect(url.toString());
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/integrations/slack/install/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse, type NextRequest } from "next/server";

import { auth } from "@/lib/auth";
import { getWorkspaceSlackCredentials } from "@/lib/slack/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { requirePermission } from "@/features/workspaces/permissions-server";
import { createInstallState } from "@/server/oauth-state";

Expand Down Expand Up @@ -38,9 +39,7 @@ export async function GET(req: NextRequest) {
);
}

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/integrations/slack/callback`;

const state = await createInstallState({
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/app/api/integrations/zoom/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { auth } from "@/lib/auth";
import { encryptSecret } from "@/lib/crypto";
import { createLogger } from "@/lib/logger";
import { getZoomCredentials } from "@/lib/zoom/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { requirePermission } from "@/features/workspaces/permissions-server";
import { verifyAndConsumeOauthStateNonce } from "@/server/oauth-state";

Expand Down Expand Up @@ -84,9 +85,7 @@ export async function GET(req: NextRequest) {
return redirectWithError("Zoom credentials not found for this workspace.");
}

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/integrations/zoom/callback`;

// Exchange code for token
Expand Down Expand Up @@ -151,9 +150,7 @@ export async function GET(req: NextRequest) {
}

function redirectWithError(msg: string) {
const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const url = new URL(`${appUrl}/settings/integrations`);
url.searchParams.set("zoom_error", msg);
return NextResponse.redirect(url.toString());
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/app/api/integrations/zoom/install/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse, type NextRequest } from "next/server";

import { auth } from "@/lib/auth";
import { getZoomCredentials } from "@/lib/zoom/config";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { requirePermission } from "@/features/workspaces/permissions-server";
import { createInstallState } from "@/server/oauth-state";

Expand Down Expand Up @@ -37,9 +38,7 @@ export async function GET(req: NextRequest) {
provider: "zoom",
});

const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();

const authUrl = new URL("https://zoom.us/oauth/authorize");
authUrl.searchParams.set("response_type", "code");
Expand All @@ -52,9 +51,7 @@ export async function GET(req: NextRequest) {
}

function redirectWithError(msg: string) {
const appUrl = (
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
).replace(/\/$/, "");
const appUrl = getHarlyPublicOrigin();
const url = new URL(`${appUrl}/settings/integrations`);
url.searchParams.set("zoom_error", msg);
return NextResponse.redirect(url.toString());
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/api/portal/auth/callback/github/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isPortalEnabled,
} from "@/lib/portal-auth";
import { PORTAL_OAUTH_STATE_COOKIE, verifyPortalOAuthState } from "@/lib/portal-oauth-state";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const runtime = "nodejs";

Expand All @@ -27,7 +28,7 @@ export async function GET(request: NextRequest) {
}

try {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/portal/auth/callback/github`;
const workspaceId = oauthState!.workspaceId;
if (!(await isPortalEnabled(workspaceId))) redirect("/portal/login?error=no_workspace" as Route);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/api/portal/auth/callback/google/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isPortalEnabled,
} from "@/lib/portal-auth";
import { PORTAL_OAUTH_STATE_COOKIE, verifyPortalOAuthState } from "@/lib/portal-oauth-state";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const runtime = "nodejs";

Expand All @@ -24,7 +25,7 @@ export async function GET(request: NextRequest) {

if (!code) redirect("/portal/login?error=oauth_denied" as Route);

const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/portal/auth/callback/google`;

try {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/api/portal/auth/callback/linkedin/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isPortalEnabled,
} from "@/lib/portal-auth";
import { PORTAL_OAUTH_STATE_COOKIE, verifyPortalOAuthState } from "@/lib/portal-oauth-state";
import { getHarlyPublicOrigin } from "@/lib/public-origin";

export const runtime = "nodejs";

Expand All @@ -24,7 +25,7 @@ export async function GET(request: NextRequest) {

if (!code) redirect("/portal/login?error=oauth_denied" as Route);

const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
const appUrl = getHarlyPublicOrigin();
const redirectUri = `${appUrl}/api/portal/auth/callback/linkedin`;

try {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/api/portal/auth/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getSinglePortalWorkspace,
} from "@/lib/portal-auth";
import { createLogger } from "@/lib/logger";
import { getHarlyPublicOrigin } from "@/lib/public-origin";
import { createPortalOAuthState, PORTAL_OAUTH_STATE_COOKIE } from "@/lib/portal-oauth-state";

const log = createLogger("api-portal-auth");
Expand All @@ -25,7 +26,7 @@ export async function GET(request: NextRequest) {

const state = createPortalOAuthState(next, workspace.id);

const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
const appUrl = getHarlyPublicOrigin();

try {
if (provider === "google") {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/api/public/v1/image/presign/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@/lib/storage-validation";
import { storage, storageProvider } from "@/lib/storage";
import { appendStorageUploadIntent, createStorageUploadIntent } from "@/lib/storage-upload-intent";
import { toHarlyPublicUrl } from "@/lib/public-origin";
import { resolvePublicWorkspace } from "@/server/api/public";
import { clientIp, enforceRateLimit } from "@/server/api/ratelimit";
import { apiOk, corsPreflight, withApi } from "@/server/api/respond";
Expand Down Expand Up @@ -39,7 +40,7 @@ export const POST = withApi(async (request) => {
});

const intent = createStorageUploadIntent({ workspaceId: workspace.workspaceId, key, contentType: parsed.data.contentType, contentLength: parsed.data.contentLength, expiresAt: Date.now() + 10 * 60_000 });
return apiOk({ ...result, uploadUrl: storageProvider === "local" ? new URL(appendStorageUploadIntent(result.uploadUrl, intent), request.url).toString() : result.uploadUrl, key }, { cors: true });
return apiOk({ ...result, uploadUrl: storageProvider === "local" ? toHarlyPublicUrl(appendStorageUploadIntent(result.uploadUrl, intent)) : result.uploadUrl, key }, { cors: true });
}, { cors: true });

export function OPTIONS() {
Expand Down
Loading
Loading