Skip to content

Commit

Permalink
Fix config leaks (#47)
Browse files Browse the repository at this point in the history
Resolve some issues with environment values being inadvertently shared
into the compiled client by explicitly separating out config values into
"public" (`src/lib/shared/shared.config.ts`) and "private"
(`src/api.config.ts`) sides.

Also includes fixes for:
- Unable to submit response form when email form value returns undefined
  • Loading branch information
mplewis authored Nov 21, 2024
1 parent 2c32698 commit 78258a8
Show file tree
Hide file tree
Showing 79 changed files with 135 additions and 112 deletions.
17 changes: 17 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,22 @@ module.exports = {
'max-len': 'off',
},
},
{
files: ['api/src/lib/shared/**/*'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['*api.config*'],
message:
'API config values are forbidden in library code shared with the web side',
},
],
},
],
},
},
],
}
8 changes: 4 additions & 4 deletions NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@
- [ ] Why are hamburger items missing on Confirm RSVP page?
- [ ] URL builder helpers
- [ ] Form to request resend link(s) for email address
<<<<<<< HEAD
- [ ] # Add Sentry to frontend
- [ ] Resend confirmation if an RSVP enters an existing email
> > > > > > > origin/main
- [ ] Diff dates in `notify` so that they are omitted when unchanged
- [ ] Investigate why some emailed URLs use incorrect hosts for Netlify Deploy Preview
- [ ] Stably sort RSVP list
Expand All @@ -48,7 +45,10 @@
- [ ] Unify email and Discord notifications
- [ ] "Serious mode" for LoadingBuddy for e.g. unsubscribe requests
- [ ] Notification when target email is denylisted on record creation
- [x] Resend confirmation if an RSVP enters an existing email
- [ ] Replace shared API lib dir with Yarn "shared package" workspace
- [ ] Add advanced crash logging
- [ ] Linter for secrets in build
- [x] Resend confirmation if an RSVP enters an existing email
- [x] Captcha
- [x] Hold RSVP locally with cookie
- [x] **Scheduler engine**
Expand Down
13 changes: 12 additions & 1 deletion api/src/api.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ function assert(name: string, value: string | undefined) {
if (!value) throw new Error(`Missing required environment variable ${name}`)
}

// Secret key

export const SECRET_KEY = process.env.SECRET_KEY
if (!SECRET_KEY) throw new Error('SECRET_KEY is required')

// Recaptcha

export const RECAPTCHA_SERVER_KEY = process.env.RECAPTCHA_SERVER_KEY

// Email

assert('SMTP_HOST', process.env.SMTP_HOST)
assert('SMTP_USER', process.env.SMTP_USER)
assert('SMTP_PASS', process.env.SMTP_PASS)
assert('FROM_NAME', process.env.FROM_NAME)
assert('FROM_EMAIL', process.env.FROM_EMAIL)
if (!process.env.SMTP_PORT) process.env.SMTP_PORT = '587'
process.env.SMTP_PORT ||= '587'
const port = parseInt(process.env.SMTP_PORT, 10)

export const MAIL_CONFIG = {
Expand Down
1 change: 0 additions & 1 deletion api/src/app.config.ts

This file was deleted.

2 changes: 1 addition & 1 deletion api/src/directives/requireAuth/requireAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ValidatorDirectiveFunc,
} from '@redwoodjs/graphql-server'

import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
import { requireAuth as applicationRequireAuth } from 'src/lib/backend/auth'

export const schema = gql`
"""
Expand Down
6 changes: 3 additions & 3 deletions api/src/functions/downloadIcs/downloadIcs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { APIGatewayEvent } from 'aws-lambda'
import { createEvent, EventAttributes } from 'ics'

import { convertToDateArray } from 'src/lib/date'
import { markdownToText } from 'src/lib/markdown'
import { wrap } from 'src/lib/sentry'
import { convertToDateArray } from 'src/lib/backend/date'
import { wrap } from 'src/lib/backend/sentry'
import { markdownToText } from 'src/lib/shared/markdown'
import { eventBySlug } from 'src/services/events/events'

export interface Event {
Expand Down
4 changes: 2 additions & 2 deletions api/src/functions/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { logger } from 'src/lib/backend/logger'
import { wrap } from 'src/lib/backend/sentry'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
import { wrap } from 'src/lib/sentry'

const baseHandler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
Expand Down
2 changes: 1 addition & 1 deletion api/src/functions/sendReminders/sendReminders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { APIGatewayEvent, Context } from 'aws-lambda'

import { sendOutstandingReminders } from 'src/lib/schedule/reminder'
import { sendOutstandingReminders } from 'src/lib/backend/schedule/reminder'

export const handler = async (_event: APIGatewayEvent, _context: Context) => {
await sendOutstandingReminders()
Expand Down
5 changes: 4 additions & 1 deletion api/src/functions/tidyUnconfirmed/tidyUnconfirmed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { APIGatewayEvent, Context } from 'aws-lambda'

import { tidyUnconfirmedEvents, tidyUnconfirmedResponses } from 'src/lib/tidy'
import {
tidyUnconfirmedEvents,
tidyUnconfirmedResponses,
} from 'src/lib/backend/tidy'

export const handler = async (_event: APIGatewayEvent, _context: Context) => {
await tidyUnconfirmedEvents()
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion api/src/lib/captcha.ts → api/src/lib/backend/captcha.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import querystring from 'querystring'

import { CI, RECAPTCHA_SERVER_KEY } from 'src/app.config'
import { RECAPTCHA_SERVER_KEY } from 'src/api.config'
import { CI } from 'src/lib/shared/shared.config'

import { logger } from './logger'

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InMemoryMailHandler } from '@redwoodjs/mailer-handler-in-memory'

import { db } from '../db'
import { db } from '../../db'
import { mailer } from '../mailer'

import { sendEmail } from './send'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MAIL_SENDER } from 'src/api.config'
import { mailer } from 'src/lib/mailer'
import { mailer } from 'src/lib/backend/mailer'

import { db } from '../db'
import { db } from '../../db'
import { logger } from '../logger'

import { Plain } from './plain'
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { stripIndent } from 'common-tags'
import { Event } from 'types/graphql'

import { SITE_URL } from 'src/app.config'
import { SITE_URL } from 'src/lib/shared/shared.config'

import { sendEmail } from '../send'

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { stripIndent } from 'common-tags'
import { PublicEvent, Response } from 'types/graphql'

import { SITE_URL } from 'src/app.config'

import {
prettyEndWithBetween,
prettyStartWithUntil,
prettyUntil,
} from '../../convert/date'
} from 'src/lib/shared/convert/date'
import { SITE_URL } from 'src/lib/shared/shared.config'

import { sendEmail } from '../send'

/**
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { stripIndent } from 'common-tags'
import { Event, PublicEvent, Response } from 'types/graphql'

import { SITE_URL } from 'src/app.config'
import { SITE_URL } from 'src/lib/shared/shared.config'

import { sendEmail } from '../send'

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SITE_URL } from 'src/app.config'
import { SITE_URL } from 'src/lib/shared/shared.config'

import { sign } from '../sign'

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion api/src/lib/mailer.ts → api/src/lib/backend/mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { NodemailerMailHandler } from '@redwoodjs/mailer-handler-nodemailer'
import { RendererOptions } from '@redwoodjs/mailer-renderer-react-email'

import { MAIL_CONFIG } from 'src/api.config'
import { logger } from 'src/lib/logger'
import { logger } from 'src/lib/backend/logger'

const { port, host, secure, auth } = MAIL_CONFIG

Expand Down
2 changes: 1 addition & 1 deletion api/src/lib/notify.ts → api/src/lib/backend/notify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Embed, Webhook } from '@vermaysha/discord-webhook'

import { CI, LOCALHOST } from 'src/app.config'
import { CI, LOCALHOST } from 'src/lib/shared/shared.config'

import { logger } from './logger'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Event } from 'types/graphql'

import { SITE_URL } from 'src/app.config'
import { SITE_URL } from 'src/lib/shared/shared.config'

import { notify } from '../notify'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Event, Response } from 'types/graphql'

import { SITE_URL } from 'src/app.config'
import { SITE_URL } from 'src/lib/shared/shared.config'

import { notify } from '../notify'

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { db } from '../db'
import { db } from '../../db'

import { sendOutstandingReminders } from './reminder'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { db } from '../db'
import { db } from '../../db'
import { sendReminder } from '../email/template/reminder'
import { logger } from '../logger'

Expand Down
File renamed without changes.
File renamed without changes.
5 changes: 1 addition & 4 deletions api/src/lib/sign.ts → api/src/lib/backend/sign.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as crypto from 'crypto'

import { CI, SECRET_KEY } from 'src/app.config'

const SECRET_KEY_FOR_TESTING = 'secret-key-for-testing'
import { SECRET_KEY } from 'src/api.config'

/** Sign string data with the app's secret key and return a URL-safe signature. */
export function sign(data: string): string {
Expand All @@ -18,7 +16,6 @@ export function verify(data: string, signature: string): boolean {
}

function verifySecretKey() {
if (CI) return SECRET_KEY_FOR_TESTING
if (!SECRET_KEY || SECRET_KEY === '') {
throw new Error('SECRET_KEY is required for signing/validation')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { randomString, randomInteger } from 'remeda'
import { Event, Response } from 'types/graphql'

import { db } from './db'
import { db } from '../db'

import { tidyUnconfirmedEvents, tidyUnconfirmedResponses } from './tidy'

function buildEvent(overrides: Partial<Event> = {}): Omit<Event, 'responses'> {
Expand Down
5 changes: 3 additions & 2 deletions api/src/lib/tidy.ts → api/src/lib/backend/tidy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dayjs from '../lib/dayjs'
import dayjs from 'src/lib/shared/dayjs'

import { db } from '../db'

import { db } from './db'
import { logger } from './logger'

const UNCONFIRMED_EXPIRY = dayjs.duration(24, 'hours')
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion api/src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PrismaClient } from '@prisma/client'

import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger'

import { logger } from './logger'
import { logger } from './backend/logger'

const prismaClient = new PrismaClient({
log: emitLogLevels(['info', 'warn', 'error']),
Expand Down
3 changes: 3 additions & 0 deletions api/src/lib/shared/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Files in this directory are shared with the web side.
Do not access secrets inside files in this directory! They will end up bundled into the client.
Secrets must be accessed from `api.config.ts` which the linter forbids using inside `shared/`.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 10 additions & 6 deletions app.config.ts → api/src/lib/shared/shared.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const CI = process.env.NODE_ENV === 'test'

type NetlifyContext = 'production' | 'deploy-preview' | 'branch-deploy' | 'dev'
const context: NetlifyContext = process.env.CONTEXT as NetlifyContext

const extractAfterHTTPSMatcher = /^https:\/\/(.*)/

Expand All @@ -9,22 +12,23 @@ function extractAfterHTTPS(url?: string) {
return url
}

const context: NetlifyContext = process.env.CONTEXT as NetlifyContext
// Site host and URL

export let SITE_HOST = process.env.SITE_HOST
if (context === 'deploy-preview' || context === 'branch-deploy')
SITE_HOST = extractAfterHTTPS(process.env.DEPLOY_PRIME_URL)

export const SECRET_KEY = process.env.SECRET_KEY

const protocol = SITE_HOST?.startsWith('localhost') ? 'http' : 'https'
if (!SITE_HOST) throw new Error('SITE_HOST is required')
export const SITE_URL = `${protocol}://${SITE_HOST}` as const

export const CI = process.env.NODE_ENV === 'test'
export const LOCALHOST = SITE_HOST.startsWith('localhost:')

// S3

export const S3_REGION = process.env.S3_REGION
export const S3_BUCKET = process.env.S3_BUCKET
export const S3_NAMESPACE = process.env.S3_NAMESPACE

export const RECAPTCHA_SERVER_KEY = process.env.RECAPTCHA_SERVER_KEY
// Recaptcha

export const RECAPTCHA_CLIENT_KEY = process.env.REDWOOD_ENV_RECAPTCHA_CLIENT_KEY
2 changes: 1 addition & 1 deletion api/src/lib/url.ts → api/src/lib/shared/url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { S3_REGION, S3_BUCKET, S3_NAMESPACE, SITE_URL } from 'src/app.config'
import { S3_REGION, S3_BUCKET, S3_NAMESPACE, SITE_URL } from './shared.config'

/**
* Generate a fully-qualified URL for the given path.
Expand Down
File renamed without changes.
File renamed without changes.
18 changes: 10 additions & 8 deletions api/src/services/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import type {

import { RedwoodError, validate } from '@redwoodjs/api'

import { validateCaptcha } from 'src/lib/captcha'
import { validateCaptcha } from 'src/lib/backend/captcha'
import { sendEventDetails } from 'src/lib/backend/email/template/event'
import {
notifyEventCreated,
notifyEventUpdated,
} from 'src/lib/backend/notify/event'
import { summarize } from 'src/lib/backend/response'
import { generateToken, alphaLower } from 'src/lib/backend/token'
import { db } from 'src/lib/db'
import { sendEventDetails } from 'src/lib/email/template/event'
import { notifyEventCreated, notifyEventUpdated } from 'src/lib/notify/event'
import { summarize } from 'src/lib/response'
import { generateToken, alphaLower } from 'src/lib/token'
import { checkVisibility } from 'src/lib/visibility'

import dayjs from '../../lib/dayjs'
import dayjs from 'src/lib/shared/dayjs'
import { checkVisibility } from 'src/lib/shared/visibility'

import { updateEventPreviewImage } from './preview'

Expand Down
5 changes: 2 additions & 3 deletions api/src/services/events/ogImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import utc from 'dayjs/plugin/utc'
import Handlebars from 'handlebars'
import puppeteer from 'puppeteer-core'

import { markdownToText } from 'src/lib/markdown'

import dayjs from '../../lib/dayjs'
import dayjs from 'src/lib/shared/dayjs'
import { markdownToText } from 'src/lib/shared/markdown'

dayjs.extend(advancedFormat)
dayjs.extend(timezone)
Expand Down
5 changes: 2 additions & 3 deletions api/src/services/events/preview.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { Event } from 'types/graphql'

import { S3_REGION, S3_BUCKET, CI } from 'src/app.config'

import { keyFor } from '../../lib/url'
import { S3_REGION, S3_BUCKET, CI } from 'src/lib/shared/shared.config'
import { keyFor } from 'src/lib/shared/url'

import { renderEventPreview } from './ogImage'

Expand Down
2 changes: 1 addition & 1 deletion api/src/services/ignoredEmails/ignoredEmails.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sign } from 'src/lib/backend/sign'
import { db } from 'src/lib/db'
import { sign } from 'src/lib/sign'

import {
createIgnoredEmail,
Expand Down
Loading

0 comments on commit 78258a8

Please sign in to comment.