Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions docker-compose.jobs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ services:
MAILGUN_DOMAIN: "mg.constructive.io"
MAILGUN_FROM: "[email protected]"
MAILGUN_REPLY: "[email protected]"
# Local dashboard port for generated links, used only for
# localhost-style hosts in DRY RUN mode:
# http://localhost:LOCAL_APP_PORT/...
LOCAL_APP_PORT: "3000"
SEND_EMAIL_LINK_DRY_RUN: "${SEND_EMAIL_LINK_DRY_RUN:-true}"
ports:
# Expose function locally (optional)
Expand Down
2 changes: 2 additions & 0 deletions functions/send-email-link/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Recommended / optional:
Bearer token to send as `Authorization` header for GraphQL requests.
- `DEFAULT_DATABASE_ID`
Used if `X-Database-Id` is not provided by the worker. In normal jobs usage, `X-Database-Id` should always be present.
- `LOCAL_APP_PORT`
Optional port suffix for localhost-style hosts (e.g. `3000`). When the resolved hostname is `localhost` / `*.localhost` and `SEND_EMAIL_LINK_DRY_RUN=true`, links are generated as `http://localhost:LOCAL_APP_PORT/...`. Ignored for non-local hostnames and in production.

Email delivery (used by `@launchql/postmaster`):

Expand Down
24 changes: 22 additions & 2 deletions functions/send-email-link/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,28 @@ export const sendEmailLink = async (
const name = company.name;
const primary = theme.primary;

const baseUrl = 'https://' + (subdomain ? [subdomain, domain].join('.') : domain);
const url = new URL(baseUrl);
const hostname = subdomain ? [subdomain, domain].join('.') : domain;

// Treat localhost-style hosts specially so we can generate
// http://localhost[:port]/... links for local dev without
// breaking production URLs.
const isLocalHost =
hostname.startsWith('localhost') ||
hostname.startsWith('0.0.0.0') ||
hostname.endsWith('.localhost');

// Optional: LOCAL_APP_PORT lets you attach a port for local dashboards
// e.g. LOCAL_APP_PORT=3000 -> http://localhost:3000
// It is ignored for non-local hostnames. Only allow on DRY RUNs
const localPort =
isLocalHost && isDryRun && process.env.LOCAL_APP_PORT
? `:${process.env.LOCAL_APP_PORT}`
: '';

// Use http only for local dry-run to avoid browser TLS warnings
// in dev; production stays https.
const protocol = isLocalHost && isDryRun ? 'http' : 'https';
const url = new URL(`${protocol}://${hostname}${localPort}`);

let subject: string;
let subMessage: string;
Expand Down
4 changes: 4 additions & 0 deletions jobs/DEVELOPMENT_JOBS.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ You should then see the job picked up by `knative-job-service` and the email pay
- The app/meta packages deployed in step 3 (`app-svc-local`, `db-meta`)
- A real `database_id` (use `$DBID` above)
- A GraphQL hostname that matches a seeded domain route (step 5)
- For localhost development, the site/domain metadata usually resolves to `localhost`.
In that case, the function will honor the `LOCAL_APP_PORT` env (default `3000` in
`docker-compose.jobs.yml`) and generate links like `http://localhost:3000/...`
when `SEND_EMAIL_LINK_DRY_RUN=true`.

With `SEND_EMAIL_LINK_DRY_RUN=true` (default in `docker-compose.jobs.yml`), enqueue a job:

Expand Down
23 changes: 11 additions & 12 deletions jobs/knative-job-fn/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ type JobContext = {
databaseId: string | undefined;
};

function getHeaders(req: any) {
return {
'x-worker-id': req.get('X-Worker-Id'),
'x-job-id': req.get('X-Job-Id'),
'x-database-id': req.get('X-Database-Id'),
'x-callback-url': req.get('X-Callback-Url')
}
}

const app: any = express();

app.use(bodyParser.json());
Expand All @@ -21,12 +30,7 @@ app.use(bodyParser.json());
app.use((req: any, res: any, next: any) => {
try {
// Log only the headers we care about plus a shallow body snapshot
const headers = {
'x-worker-id': req.get('X-Worker-Id'),
'x-job-id': req.get('X-Job-Id'),
'x-database-id': req.get('X-Database-Id'),
'x-callback-url': req.get('X-Callback-Url')
};
const headers = getHeaders(req)

let body: any;
if (req.body && typeof req.body === 'object') {
Expand Down Expand Up @@ -229,12 +233,7 @@ export default {

// Log the full error context for debugging.
try {
const headers = {
'x-worker-id': req.get('X-Worker-Id'),
'x-job-id': req.get('X-Job-Id'),
'x-database-id': req.get('X-Database-Id'),
'x-callback-url': req.get('X-Callback-Url')
};
const headers = getHeaders(req)

// Some error types (e.g. GraphQL ClientError) expose response info.
const errorDetails: any = {
Expand Down