From b392b1f02e77af62b2faf2aceb57900564e2951a Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 20 Dec 2024 15:51:22 +0000 Subject: [PATCH] Use Effect for an environment variable Refs #1834 --- integration/base.ts | 10 +++++----- src/Context.ts | 5 ++++- src/ExpressServer.ts | 7 ++++--- src/WebApp.ts | 9 +++++---- src/env.ts | 1 - src/index.ts | 3 ++- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/integration/base.ts b/integration/base.ts index 69e30f475..d0e8ae442 100644 --- a/integration/base.ts +++ b/integration/base.ts @@ -2,15 +2,15 @@ import { FetchHttpClient } from '@effect/platform' import { NodeHttpServer } from '@effect/platform-node' import { LibsqlClient } from '@effect/sql-libsql' import { + test as baseTest, + expect, type Fixtures, type PlaywrightTestArgs, type PlaywrightTestOptions, - test as baseTest, - expect, } from '@playwright/test' import { SystemClock } from 'clock-ts' import { Doi } from 'doi-ts' -import { Effect, Logger as EffectLogger, Fiber, Layer, Option, pipe } from 'effect' +import { Effect, Logger as EffectLogger, Fiber, Layer, Option, pipe, Redacted } from 'effect' import fetchMock from 'fetch-mock' import * as fs from 'fs/promises' import http from 'http' @@ -39,7 +39,7 @@ import { UnverifiedContactEmailAddress, VerifiedContactEmailAddress, } from '../src/contact-email-address.js' -import { DeprecatedLoggerEnv, ExpressConfig } from '../src/Context.js' +import { DeprecatedLoggerEnv, ExpressConfig, SessionSecret } from '../src/Context.js' import { DeprecatedLogger } from '../src/DeprecatedServices.js' import { createAuthorInviteEmail } from '../src/email.js' import type { @@ -1297,7 +1297,6 @@ const appFixtures: Fixtures, PlaywrightTestArg researchInterestsStore, reviewRequestStore, scietyListToken: 'secret' as NonEmptyString, - secret: '', sessionCookie: 'session', sessionStore: new Keyv(), slackApiToken: '', @@ -1325,6 +1324,7 @@ const appFixtures: Fixtures, PlaywrightTestArg Effect.provideService(GhostApi, { key: 'key' }), Effect.provide(Nodemailer.layer(nodemailer)), Effect.provideService(PublicUrl, new URL(`http://localhost:${port}`)), + Effect.provideService(SessionSecret, Redacted.make('')), Effect.provideService(FetchHttpClient.Fetch, fetch as unknown as typeof globalThis.fetch), Effect.provide(LibsqlClient.layer({ url: `file:${testInfo.outputPath('database.db')}` })), Effect.provide(TemplatePage.optionsLayer({ fathomId: Option.none(), environmentLabel: Option.none() })), diff --git a/src/Context.ts b/src/Context.ts index a4dc54ffc..889a76be2 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -1,4 +1,4 @@ -import { Context } from 'effect' +import { Context, type Redacted } from 'effect' import type { LoggerEnv } from 'logger-fp-ts' import type { app, ConfigEnv } from './app.js' import type { EnvVars } from './env.js' @@ -25,6 +25,7 @@ export class ExpressConfig extends Context.Tag('ExpressConfig')< | 'ghostApi' | 'nodemailer' | 'publicUrl' + | 'secret' | 'sleep' | 'templatePage' | 'useCrowdinInContext' @@ -34,3 +35,5 @@ export class ExpressConfig extends Context.Tag('ExpressConfig')< export class Locale extends Context.Tag('Locale')() {} export class FlashMessage extends Context.Tag('CurrentFlashMessage')() {} + +export class SessionSecret extends Context.Tag('SessionSecret')() {} diff --git a/src/ExpressServer.ts b/src/ExpressServer.ts index f0d728312..0fcfca1ae 100644 --- a/src/ExpressServer.ts +++ b/src/ExpressServer.ts @@ -1,9 +1,9 @@ import { FetchHttpClient } from '@effect/platform' import KeyvRedis from '@keyv/redis' -import { Effect } from 'effect' +import { Effect, Redacted } from 'effect' import Keyv from 'keyv' import { app } from './app.js' -import { DeprecatedEnvVars, DeprecatedLoggerEnv, DeprecatedSleepEnv, ExpressConfig } from './Context.js' +import { DeprecatedEnvVars, DeprecatedLoggerEnv, DeprecatedSleepEnv, ExpressConfig, SessionSecret } from './Context.js' import * as EffectToFpts from './EffectToFpts.js' import { CanWriteComments, UseCrowdinInContext } from './feature-flags.js' import { GhostApi } from './ghost.js' @@ -25,6 +25,7 @@ export const expressServer = Effect.gen(function* () { const templatePage = yield* TemplatePage const useCrowdinInContext = yield* UseCrowdinInContext const ghostApi = yield* GhostApi + const secret = yield* SessionSecret return app({ canWriteComments, @@ -34,6 +35,7 @@ export const expressServer = Effect.gen(function* () { ghostApi, nodemailer, publicUrl, + secret: Redacted.value(secret), ...sleep, templatePage, useCrowdinInContext, @@ -95,7 +97,6 @@ export const ExpressConfigLive = Effect.gen(function* () { researchInterestsStore: new Keyv({ emitErrors: false, namespace: 'research-interests', store: createKeyvStore() }), reviewRequestStore: new Keyv({ emitErrors: false, namespace: 'review-request', store: createKeyvStore() }), scietyListToken: env.SCIETY_LIST_TOKEN, - secret: env.SECRET, sessionCookie: 'session', sessionStore: new Keyv({ emitErrors: false, diff --git a/src/WebApp.ts b/src/WebApp.ts index 2226925d0..20d7eb6cc 100644 --- a/src/WebApp.ts +++ b/src/WebApp.ts @@ -10,9 +10,9 @@ import { Path, } from '@effect/platform' import cookieSignature from 'cookie-signature' -import { Cause, Config, Effect, flow, Layer, Option, pipe, Schema } from 'effect' +import { Cause, Config, Effect, flow, Layer, Option, pipe, Redacted, Schema } from 'effect' import { StatusCodes } from 'http-status-codes' -import { Express, ExpressConfig, FlashMessage, Locale } from './Context.js' +import { Express, ExpressConfig, FlashMessage, Locale, SessionSecret } from './Context.js' import { ExpressHttpApp } from './ExpressHttpApp.js' import { expressServer } from './ExpressServer.js' import { CanChooseLocale, UseCrowdinInContext } from './feature-flags.js' @@ -143,13 +143,14 @@ const getFlashMessage = HttpMiddleware.make(app => const getLoggedInUser = HttpMiddleware.make(app => Effect.gen(function* () { - const { secret, sessionCookie, sessionStore } = yield* ExpressConfig + const secret = yield* SessionSecret + const { sessionCookie, sessionStore } = yield* ExpressConfig const session = yield* pipe( HttpServerRequest.schemaCookies( Schema.Struct({ session: pipe(Schema.propertySignature(Schema.String), Schema.fromKey(sessionCookie)) }), ), - Effect.andThen(({ session }) => cookieSignature.unsign(session, secret)), + Effect.andThen(({ session }) => cookieSignature.unsign(session, Redacted.value(secret))), Effect.andThen(Schema.decodeUnknown(Uuid.UuidSchema)), Effect.andThen(sessionId => sessionStore.get(sessionId)), Effect.andThen(Schema.decodeUnknown(Schema.Struct({ user: UserSchema }))), diff --git a/src/env.ts b/src/env.ts index 253ce506e..2002677b2 100644 --- a/src/env.ts +++ b/src/env.ts @@ -70,7 +70,6 @@ const EnvD = pipe( ORCID_API_URL: withDefault(UrlD, new URL('https://pub.orcid.org/')), REMOVED_PREREVIEWS: withDefault(CommaSeparatedListD(IntD), []), SCIETY_LIST_TOKEN: withDefault(NonEmptyStringC, v4()() as unknown as NonEmptyString), - SECRET: D.string, SLACK_API_TOKEN: D.string, SLACK_CLIENT_ID: D.string, SLACK_CLIENT_SECRET: D.string, diff --git a/src/index.ts b/src/index.ts index ede62ca2f..6066017c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { Config, Effect, Function, Layer, Logger, LogLevel, Schema } from 'effec import { pipe } from 'fp-ts/lib/function.js' import { createServer } from 'http' import fetch from 'make-fetch-happen' -import { DeprecatedEnvVars, DeprecatedLoggerEnv, ExpressConfig } from './Context.js' +import { DeprecatedEnvVars, DeprecatedLoggerEnv, ExpressConfig, SessionSecret } from './Context.js' import { DeprecatedLogger, makeDeprecatedEnvVars, makeDeprecatedLoggerEnv } from './DeprecatedServices.js' import { ExpressConfigLive } from './ExpressServer.js' import * as FeatureFlags from './feature-flags.js' @@ -68,6 +68,7 @@ pipe( }), ), Effect.provideServiceEffect(PublicUrl, Config.url('PUBLIC_URL')), + Effect.provideServiceEffect(SessionSecret, Config.redacted('SECRET')), Logger.withMinimumLogLevel(LogLevel.Debug), Effect.provide(Logger.replaceEffect(Logger.defaultLogger, DeprecatedLogger)), Effect.provideServiceEffect(DeprecatedLoggerEnv, makeDeprecatedLoggerEnv),