Define env vars validation. Use validate env vars.

This commit is contained in:
Mihovil Ilakovac 2024-10-29 14:59:50 +01:00
parent 9d618ead9d
commit 7333b259af
35 changed files with 333 additions and 210 deletions

View File

@ -58,12 +58,12 @@ COPY --from=server-builder /app/node_modules ./node_modules
# Copying the SDK because 'validate-env.mjs' executes independent of the bundle
# and references the 'wasp' package.
COPY --from=server-builder /app/.wasp/out/sdk .wasp/out/sdk
# Copying 'server/node_modules' because 'validate-env.mjs' executes independent
# of the bundle and references the dotenv package.
# Copying 'server/node_modules' because we require dotenv package to
# load environment variables
# TODO: replace dotenv with native Node.js environment variable loading
COPY --from=server-builder /app/.wasp/build/server/node_modules .wasp/build/server/node_modules
COPY --from=server-builder /app/.wasp/build/server/bundle .wasp/build/server/bundle
COPY --from=server-builder /app/.wasp/build/server/package*.json .wasp/build/server/
COPY --from=server-builder /app/.wasp/build/server/scripts .wasp/build/server/scripts
COPY db/ .wasp/build/db/
EXPOSE ${PORT}
WORKDIR /app/.wasp/build/server

View File

@ -1,7 +1,8 @@
{{={= =}=}}
import { stripTrailingSlash } from '../universal/url.js'
import { env } from './env.js'
const apiUrl = stripTrailingSlash(import.meta.env.REACT_APP_API_URL) || '{= defaultServerUrl =}';
const apiUrl = stripTrailingSlash(env.REACT_APP_API_URL)
// PUBLIC API
export type ClientConfig = {

View File

@ -0,0 +1,14 @@
{{={= =}=}}
import * as z from 'zod'
import { ensureEnvSchema } from '../env/index.js'
const clientEnvSchema = z.object({
REACT_APP_API_URL: z
.string({
required_error: 'REACT_APP_API_URL is required',
})
.default('{= defaultServerUrl =}')
})
export const env = ensureEnvSchema(import.meta.env, clientEnvSchema)

View File

@ -1,11 +0,0 @@
import * as z from 'zod'
import { ensureEnvSchema } from '../../env/index.js'
const serverEnvSchema = z.object({
REACT_APP_API_URL: z.string({
required_error: 'REACT_APP_API_URL is required',
}),
})
export const env = ensureEnvSchema(import.meta.env, serverEnvSchema)

View File

@ -12,3 +12,6 @@ export type Route = { method: HttpMethod; path: string }
// PUBLIC API
export { config, ClientConfig } from './config'
// PUBLIC API
export { env } from './env.js'

View File

@ -1,17 +1,28 @@
import * as z from 'zod'
const redColor = '\x1b[31m'
export function ensureEnvSchema<Schema extends z.ZodTypeAny>(
data: unknown,
schema: Schema,
data: unknown,
schema: Schema
): z.infer<Schema> {
try {
return schema.parse(data)
} catch (e) {
try {
return schema.parse(data)
} catch (e) {
// TODO: figure out how to output the error message in a better way
if (e instanceof z.ZodError) {
throw new Error(e.errors.map((error) => error.message).join('\n'))
} else {
throw e
}
}
if (e instanceof z.ZodError) {
console.error(redColor, '╔═════════════════════════════╗');
console.error(redColor, '║ Env vars validation failed ║');
console.error(redColor, '╚═════════════════════════════╝');
console.error()
for (const error of e.errors) {
console.error(`- ${error.message}`)
}
console.error()
console.error(redColor, '═══════════════════════════════');
throw new Error('Error parsing environment variables')
} else {
throw e
}
}
}

View File

@ -109,8 +109,6 @@
"./client/test": "./dist/client/test/index.js",
"./client": "./dist/client/index.js",
"./dev": "./dist/dev/index.js",
"./client/env": "./dist/client/env/index.js",
"./server/env": "./dist/server/env/index.js",
{=! todo(filip): Fixes below are for type errors in 0.13.1, remove ASAP =}
{=! Used by our code (SDK for full-stack type safety), uncodumented (but accessible) for users. =}

View File

@ -0,0 +1,7 @@
// Used for internal Wasp development only, not copied to generated app.
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
}

View File

@ -1,15 +0,0 @@
// PRIVATE API (SDK)
export function ensureEnvVarsForProvider<EnvVarName extends string>(
envVarNames: EnvVarName[],
providerName: string,
): Record<EnvVarName, string> {
const result: Record<string, string> = {};
for (const envVarName of envVarNames) {
const value = process.env[envVarName];
if (!value) {
throw new Error(`${envVarName} env variable is required when using the ${providerName} auth provider.`);
}
result[envVarName] = value;
}
return result as Record<EnvVarName, string>;
}

View File

@ -1,23 +1,19 @@
import { OAuth2Provider, OAuth2ProviderWithPKCE } from "arctic";
export function defineProvider<
OAuthClient extends OAuth2Provider | OAuth2ProviderWithPKCE,
Env extends Record<string, string>
OAuthClient extends OAuth2Provider | OAuth2ProviderWithPKCE
>({
id,
displayName,
env,
oAuthClient,
}: {
id: string;
displayName: string;
env: Env;
oAuthClient: OAuthClient;
}) {
return {
id,
displayName,
env,
oAuthClient,
};
}

View File

@ -1,17 +1,12 @@
{{={= =}=}}
import { Discord } from "arctic";
import { Discord } from 'arctic';
import { defineProvider } from "../provider.js";
import { ensureEnvVarsForProvider } from "../env.js";
import { getRedirectUriForCallback } from "../redirect.js";
import { defineProvider } from '../provider.js';
import { getRedirectUriForCallback } from '../redirect.js';
import { env } from '../../../env.js';
const id = "{= providerId =}";
const displayName = "{= displayName =}";
const env = ensureEnvVarsForProvider(
["DISCORD_CLIENT_ID", "DISCORD_CLIENT_SECRET"],
displayName
);
const id = '{= providerId =}';
const displayName = '{= displayName =}';
const oAuthClient = new Discord(
env.DISCORD_CLIENT_ID,
@ -23,6 +18,5 @@ const oAuthClient = new Discord(
export const discord = defineProvider({
id,
displayName,
env,
oAuthClient,
});

View File

@ -1,16 +1,11 @@
{{={= =}=}}
import { GitHub } from "arctic";
import { GitHub } from 'arctic';
import { ensureEnvVarsForProvider } from "../env.js";
import { defineProvider } from "../provider.js";
import { defineProvider } from '../provider.js';
import { env } from '../../../env.js';
const id = "{= providerId =}";
const displayName = "{= displayName =}";
const env = ensureEnvVarsForProvider(
["GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET"],
displayName
);
const id = '{= providerId =}';
const displayName = '{= displayName =}';
const oAuthClient = new GitHub(
env.GITHUB_CLIENT_ID,
@ -21,6 +16,5 @@ const oAuthClient = new GitHub(
export const github = defineProvider({
id,
displayName,
env,
oAuthClient,
});

View File

@ -1,17 +1,12 @@
{{={= =}=}}
import { Google } from "arctic";
import { Google } from 'arctic';
import { ensureEnvVarsForProvider } from "../env.js";
import { getRedirectUriForCallback } from "../redirect.js";
import { defineProvider } from "../provider.js";
import { getRedirectUriForCallback } from '../redirect.js';
import { defineProvider } from '../provider.js';
import { env } from '../../../env.js';
const id = "{= providerId =}";
const displayName = "{= displayName =}";
const env = ensureEnvVarsForProvider(
["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"],
displayName,
);
const id = '{= providerId =}';
const displayName = '{= displayName =}';
const oAuthClient = new Google(
env.GOOGLE_CLIENT_ID,
@ -23,6 +18,5 @@ const oAuthClient = new Google(
export const google = defineProvider({
id,
displayName,
env,
oAuthClient,
});

View File

@ -1,17 +1,12 @@
{{={= =}=}}
import { Keycloak } from "arctic";
import { Keycloak } from 'arctic';
import { ensureEnvVarsForProvider } from "../env.js";
import { getRedirectUriForCallback } from "../redirect.js";
import { defineProvider } from "../provider.js";
import { getRedirectUriForCallback } from '../redirect.js';
import { defineProvider } from '../provider.js';
import { env } from '../../../env.js';
const id = "{= providerId =}";
const displayName = "{= displayName =}";
const env = ensureEnvVarsForProvider(
["KEYCLOAK_REALM_URL", "KEYCLOAK_CLIENT_ID", "KEYCLOAK_CLIENT_SECRET"],
displayName,
);
const id = '{= providerId =}';
const displayName = '{= displayName =}';
const oAuthClient = new Keycloak(
env.KEYCLOAK_REALM_URL,
@ -24,6 +19,5 @@ const oAuthClient = new Keycloak(
export const keycloak = defineProvider({
id,
displayName,
env,
oAuthClient,
});

View File

@ -1,9 +1,8 @@
{{={= =}=}}
import merge from 'lodash.merge'
import { stripTrailingSlash } from "../universal/url.js";
const nodeEnv = process.env.NODE_ENV ?? 'development'
import { env } from './env.js'
import { stripTrailingSlash } from '../universal/url.js'
// TODO:
// - Use dotenv library to consume env vars from a file.
@ -12,11 +11,6 @@ const nodeEnv = process.env.NODE_ENV ?? 'development'
type BaseConfig = {
allowedCORSOrigins: string | string[];
{=# isAuthEnabled =}
auth: {
jwtSecret: string | undefined;
}
{=/ isAuthEnabled =}
}
type CommonConfig = BaseConfig & {
@ -24,6 +18,11 @@ type CommonConfig = BaseConfig & {
isDevelopment: boolean;
port: number;
databaseUrl: string | undefined;
{=# isAuthEnabled =}
auth: {
jwtSecret: string | undefined;
}
{=/ isAuthEnabled =}
}
type EnvConfig = BaseConfig & {
@ -39,14 +38,14 @@ const config: {
production: EnvConfig,
} = {
all: {
env: nodeEnv,
isDevelopment: nodeEnv === 'development',
port: process.env.PORT ? parseInt(process.env.PORT) : {= defaultServerPort =},
databaseUrl: process.env.{= databaseUrlEnvVarName =},
env: env.NODE_ENV,
isDevelopment: env.NODE_ENV === 'development',
port: env.PORT,
databaseUrl: env.{= databaseUrlEnvVarName =},
allowedCORSOrigins: [],
{=# isAuthEnabled =}
auth: {
jwtSecret: undefined
jwtSecret: env.JWT_SECRET
}
{=/ isAuthEnabled =}
},
@ -54,41 +53,26 @@ const config: {
production: getProductionConfig(),
}
const resolvedConfig: Config = merge(config.all, config[nodeEnv])
const resolvedConfig: Config = merge(config.all, config[env.NODE_ENV])
// PUBLIC API
export default resolvedConfig
function getDevelopmentConfig(): EnvConfig {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL ?? '{= defaultClientUrl =}');
const serverUrl = stripTrailingSlash(process.env.WASP_SERVER_URL ?? '{= defaultServerUrl =}');
const frontendUrl = stripTrailingSlash(env.WASP_WEB_CLIENT_URL);
const serverUrl = stripTrailingSlash(env.WASP_SERVER_URL);
return {
// @ts-ignore
frontendUrl,
// @ts-ignore
serverUrl,
allowedCORSOrigins: '*',
{=# isAuthEnabled =}
auth: {
jwtSecret: 'DEVJWTSECRET'
}
{=/ isAuthEnabled =}
}
}
function getProductionConfig(): EnvConfig {
const frontendUrl = stripTrailingSlash(process.env.WASP_WEB_CLIENT_URL);
const serverUrl = stripTrailingSlash(process.env.WASP_SERVER_URL);
const frontendUrl = stripTrailingSlash(env.WASP_WEB_CLIENT_URL);
const serverUrl = stripTrailingSlash(env.WASP_SERVER_URL);
return {
// @ts-ignore
frontendUrl,
// @ts-ignore
serverUrl,
// @ts-ignore
allowedCORSOrigins: [frontendUrl],
{=# isAuthEnabled =}
auth: {
jwtSecret: process.env.JWT_SECRET
}
{=/ isAuthEnabled =}
}
}

View File

@ -1,4 +1,5 @@
{{={= =}=}}
import { env } from '../env.js';
import { initEmailSender } from "./core/index.js";
import { EmailSender } from "./core/types.js";
@ -7,25 +8,24 @@ import { EmailSender } from "./core/types.js";
{=# isSmtpProviderUsed =}
const emailProvider = {
type: "smtp",
host: process.env.SMTP_HOST!,
// @ts-ignore
port: parseInt(process.env.SMTP_PORT, 10),
username: process.env.SMTP_USERNAME!,
password: process.env.SMTP_PASSWORD!,
host: env.SMTP_HOST,
port: env.SMTP_PORT,
username: env.SMTP_USERNAME,
password: env.SMTP_PASSWORD,
} as const;
{=/ isSmtpProviderUsed =}
{=# isSendGridProviderUsed =}
const emailProvider = {
type: "sendgrid",
apiKey: process.env.SENDGRID_API_KEY!,
apiKey: env.SENDGRID_API_KEY,
} as const;
{=/ isSendGridProviderUsed =}
{=# isMailgunProviderUsed =}
const emailProvider = {
type: "mailgun",
apiKey: process.env.MAILGUN_API_KEY!,
domain: process.env.MAILGUN_DOMAIN!,
apiUrl: process.env.MAILGUN_API_URL!,
apiKey: env.MAILGUN_API_KEY,
domain: env.MAILGUN_DOMAIN,
apiUrl: env.MAILGUN_API_URL,
} as const;
{=/ isMailgunProviderUsed =}
{=# isDummyProviderUsed =}

View File

@ -0,0 +1,136 @@
{{={= =}=}}
import * as z from 'zod'
import { ensureEnvSchema } from '../env/index.js'
const serverCommonSchema = z.object({
PORT: z.coerce.number().default({= defaultServerPort =}),
{= databaseUrlEnvVarName =}: z.string({
required_error: '{= databaseUrlEnvVarName =} is required',
}),
PG_BOSS_NEW_OPTIONS: z.string().optional(),
{=# isEmailSenderUsed =}
{=# enabledEmailSenders.isSmtpProviderUsed =}
SMTP_HOST: z.string({
required_error: 'SMTP_HOST is required',
}),
SMTP_PORT: z.coerce.number({
required_error: 'SMTP_PORT is required',
invalid_type_error: 'SMTP_PORT must be a number',
}),
SMTP_USERNAME: z.string({
required_error: 'SMTP_USERNAME is required',
}),
SMTP_PASSWORD: z.string({
required_error: 'SMTP_PASSWORD is required',
}),
{=/ enabledEmailSenders.isSmtpProviderUsed =}
{=# enabledEmailSenders.isSendGridProviderUsed =}
SENDGRID_API_KEY: z.string({
required_error: 'SENDGRID_API_KEY is required',
}),
{=/ enabledEmailSenders.isSendGridProviderUsed =}
{=# enabledEmailSenders.isMailgunProviderUsed =}
MAILGUN_API_KEY: z.string({
required_error: 'MAILGUN_API_KEY is required',
}),
MAILGUN_DOMAIN: z.string({
required_error: 'MAILGUN_DOMAIN is required',
}),
MAILGUN_API_URL: z.string().optional(),
{=/ enabledEmailSenders.isMailgunProviderUsed =}
{=/ isEmailSenderUsed =}
SKIP_EMAIL_VERIFICATION_IN_DEV: z.boolean().default(false),
{=# isAuthEnabled =}
{=# enabledAuthProviders.isGoogleAuthEnabled =}
GOOGLE_CLIENT_ID: z.string({
required_error: 'GOOGLE_CLIENT_ID is required',
}),
GOOGLE_CLIENT_SECRET: z.string({
required_error: 'GOOGLE_CLIENT_SECRET is required',
}),
{=/ enabledAuthProviders.isGoogleAuthEnabled =}
{=# enabledAuthProviders.isGitHubAuthEnabled =}
GITHUB_CLIENT_ID: z.string({
required_error: 'GITHUB_CLIENT_ID is required',
}),
GITHUB_CLIENT_SECRET: z.string({
required_error: 'GITHUB_CLIENT_SECRET is required',
}),
{=/ enabledAuthProviders.isGitHubAuthEnabled =}
{=# enabledAuthProviders.isDiscordAuthEnabled =}
DISCORD_CLIENT_ID: z.string({
required_error: 'DISCORD_CLIENT_ID is required',
}),
DISCORD_CLIENT_SECRET: z.string({
required_error: 'DISCORD_CLIENT_SECRET is required',
}),
{=/ enabledAuthProviders.isDiscordAuthEnabled =}
{=# enabledAuthProviders.isKeycloakAuthEnabled =}
KEYCLOAK_CLIENT_ID: z.string({
required_error: 'KEYCLOAK_CLIENT_ID is required',
}),
KEYCLOAK_CLIENT_SECRET: z.string({
required_error: 'KEYCLOAK_CLIENT_SECRET is required',
}),
KEYCLOAK_REALM_URL: z
.string({
required_error: 'KEYCLOAK_REALM_URL is required',
})
.url({
message: 'KEYCLOAK_REALM_URL must be a valid URL',
}),
{=/ enabledAuthProviders.isKeycloakAuthEnabled =}
{=/ isAuthEnabled =}
})
const serverUrlSchema = z
.string({
required_error: 'WASP_SERVER_URL is required',
})
.url({
message: 'WASP_SERVER_URL must be a valid URL',
})
const clientUrlSchema = z
.string({
required_error: 'WASP_WEB_CLIENT_URL is required',
})
.url({
message: 'WASP_WEB_CLIENT_URL must be a valid URL',
})
const jwtTokenSchema = z
.string({
required_error: 'JWT_SECRET is required',
})
// In development, we provide default values for some environment variables
// to make the development process easier
const serverDevSchema = z.object({
NODE_ENV: z.literal('development'),
WASP_SERVER_URL: serverUrlSchema
.default('{= defaultServerUrl =}'),
WASP_WEB_CLIENT_URL: clientUrlSchema
.default('{= defaultClientUrl =}'),
{=# isAuthEnabled =}
JWT_SECRET: jwtTokenSchema
.default('DEVJWTSECRET'),
{=/ isAuthEnabled =}
})
const serverProdSchema = z.object({
NODE_ENV: z.literal('production'),
WASP_SERVER_URL: serverUrlSchema,
WASP_WEB_CLIENT_URL: clientUrlSchema,
{=# isAuthEnabled =}
JWT_SECRET: jwtTokenSchema,
{=/ isAuthEnabled =}
})
const serverEnvSchema = z.discriminatedUnion('NODE_ENV', [
serverDevSchema.merge(serverCommonSchema),
serverProdSchema.merge(serverCommonSchema)
])
export const env = ensureEnvSchema(process.env, serverEnvSchema)

View File

@ -1,19 +0,0 @@
import * as z from 'zod'
import { ensureEnvSchema } from '../../env/index.js'
const serverEnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production']).default('development'),
PORT: z.coerce.number().default(3000),
SERVER_URL: z.string({
required_error: 'SERVER_URL is required',
}),
CLIENT_URL: z.string({
required_error: 'CLIENT_URL is required',
}),
JWT_SECRET: z.string({
required_error: 'JWT_SECRET is required',
}),
})
export const env = ensureEnvSchema(process.env, serverEnvSchema)

View File

@ -10,6 +10,8 @@ export { type ServerSetupFn } from './types/index.js'
export { HttpError } from './HttpError.js'
// PUBLIC API
export { MiddlewareConfigFn } from './middleware/index.js'
// PUBLIC API
export { env } from './env.js'
// PUBLIC API
export type DbSeedFn = (prisma: PrismaClient) => Promise<void>

View File

@ -1,5 +1,6 @@
import PgBoss from 'pg-boss'
import { config } from 'wasp/server'
import { env } from '../../../env.js'
import { config } from '../../../index.js'
const boss = createPgBoss()
@ -9,9 +10,9 @@ function createPgBoss() {
}
// Add an escape hatch for advanced configuration of pg-boss to overwrite our defaults.
if (process.env.PG_BOSS_NEW_OPTIONS) {
if (env.PG_BOSS_NEW_OPTIONS) {
try {
pgBossNewOptions = JSON.parse(process.env.PG_BOSS_NEW_OPTIONS)
pgBossNewOptions = JSON.parse(env.PG_BOSS_NEW_OPTIONS)
} catch {
console.error(
'Environment variable PG_BOSS_NEW_OPTIONS was not parsable by JSON.parse()!'

View File

@ -1,3 +1,5 @@
export function stripTrailingSlash(url: string): string
export function stripTrailingSlash(url: undefined): undefined
export function stripTrailingSlash(url?: string): string | undefined {
return url?.replace(/\/$/, "");
}

View File

@ -7,10 +7,9 @@
"comment-filip": "The server.js location changed because we have now included client source files above .wasp/out/server/src.",
"scripts": {
"bundle": "rollup --config --silent",
"start": "npm run validate-env && node --enable-source-maps -r dotenv/config bundle/server.js",
"start": "node --enable-source-maps -r dotenv/config bundle/server.js",
"bundle-and-start": "npm run bundle && npm run start",
"watch": "nodemon --exec 'npm run bundle-and-start || exit 1'",
"validate-env": "node -r dotenv/config ./scripts/validate-env.mjs",
"db-seed": "npm run bundle && node --enable-source-maps -r dotenv/config bundle/dbSeed.js",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"start-production": "{=& startProductionScript =}"

View File

@ -1,5 +0,0 @@
import { throwIfNotValidAbsoluteURL } from 'wasp/universal/validators';
console.info("🔍 Validating environment variables...");
throwIfNotValidAbsoluteURL(process.env.WASP_WEB_CLIENT_URL, 'Environment variable WASP_WEB_CLIENT_URL');
throwIfNotValidAbsoluteURL(process.env.WASP_SERVER_URL, 'Environment variable WASP_SERVER_URL');

View File

@ -11,6 +11,7 @@ import { resetPassword } from "../email/resetPassword.js";
import { verifyEmail } from "../email/verifyEmail.js";
import { GetVerificationEmailContentFn, GetPasswordResetEmailContentFn } from "wasp/server/auth/email";
import { handleRejection } from "wasp/server/utils";
import { env } from "wasp/server";
{=# userSignupFields.isDefined =}
{=& userSignupFields.importStatement =}
@ -70,7 +71,7 @@ const config: ProviderConfig = {
clientRoute: '{= emailVerificationClientRoute =}',
getVerificationEmailContent: _waspGetVerificationEmailContent,
{=# isDevelopment =}
isEmailAutoVerified: process.env.SKIP_EMAIL_VERIFICATION_IN_DEV === 'true',
isEmailAutoVerified: env.SKIP_EMAIL_VERIFICATION_IN_DEV,
{=/ isDevelopment =}
{=^ isDevelopment =}
isEmailAutoVerified: false,

View File

@ -1,10 +1,11 @@
import {
Request as ExpressRequest,
Response as ExpressResponse,
} from "express";
import { parseCookies } from "oslo/cookie";
} from 'express';
import { parseCookies } from 'oslo/cookie';
import type { ProviderConfig } from "wasp/auth/providers/types";
import type { ProviderConfig } from 'wasp/auth/providers/types';
import { config } from 'wasp/server';
import type { OAuthStateFieldName } from './state';
@ -17,8 +18,7 @@ export function setOAuthCookieValue(
const cookieName = `${provider.id}_${fieldName}`;
res.cookie(cookieName, value, {
httpOnly: true,
// TODO: use server config to determine if secure
secure: process.env.NODE_ENV === "production",
secure: !config.isDevelopment,
path: "/",
maxAge: 60 * 60 * 1000, // 1 hour
});

View File

@ -3,7 +3,7 @@ import { useSocket } from 'wasp/client/webSocket'
import { Link } from 'wasp/client/router'
import { logout, useAuth } from 'wasp/client/auth'
import { useQuery, getDate } from 'wasp/client/operations'
import { env } from 'wasp/client/env'
import { env } from 'wasp/client'
import './Main.css'
import { getName } from './user'
@ -17,6 +17,7 @@ export function App() {
const connectionIcon = isConnected ? '🟢' : '🔴'
// TODO: enable users to define their own client env vars
const appName = import.meta.env.REACT_APP_NAME
? import.meta.env.REACT_APP_NAME
: 'TODO App'

View File

@ -2,6 +2,7 @@ import { type Application } from 'express'
import { mySpecialJob } from 'wasp/server/jobs'
import {
config,
env,
type MiddlewareConfigFn,
type ServerSetupFn,
} from 'wasp/server'

View File

@ -0,0 +1,23 @@
module Wasp.Generator.EmailSenders
( getEnabledEmailProvidersJson,
)
where
import Data.Aeson (KeyValue ((.=)), object)
import qualified Data.Aeson as Aeson
import qualified Wasp.AppSpec.App.EmailSender as AS.App.EmailSender
getEnabledEmailProvidersJson :: AS.App.EmailSender.EmailSender -> Aeson.Value
getEnabledEmailProvidersJson emailSender =
object $
makeProviderJson
<$> providersKeyAndName
where
providersKeyAndName =
[ ("isSmtpProviderUsed", AS.App.EmailSender.SMTP),
("isSendGridProviderUsed", AS.App.EmailSender.SendGrid),
("isMailgunProviderUsed", AS.App.EmailSender.Mailgun),
("isDummyProviderUsed", AS.App.EmailSender.Dummy)
]
makeProviderJson (key, name) = key .= (enabledEmailSenderName == name)
enabledEmailSenderName = AS.App.EmailSender.provider emailSender

View File

@ -15,10 +15,7 @@ import qualified Wasp.SemanticVersion as SV
data EmailSenderProvider = EmailSenderProvider
{ npmDependency :: Maybe AS.Dependency.Dependency,
setupFnFile :: Path' (Rel ProvidersDir) File',
-- We have to use explicit boolean keys in templates (e.g. "isSMTPProviderEnabled") so each
-- provider provides its own key which we pass to the template.
isEnabledKey :: String
setupFnFile :: Path' (Rel ProvidersDir) File'
}
deriving (Show, Eq)
@ -31,8 +28,7 @@ smtp :: EmailSenderProvider
smtp =
EmailSenderProvider
{ npmDependency = Just nodeMailerDependency,
setupFnFile = [relfile|smtp.ts|],
isEnabledKey = "isSmtpProviderUsed"
setupFnFile = [relfile|smtp.ts|]
}
where
nodeMailerVersionRange :: SV.Range
@ -45,8 +41,7 @@ sendGrid :: EmailSenderProvider
sendGrid =
EmailSenderProvider
{ npmDependency = Just sendGridDependency,
setupFnFile = [relfile|sendgrid.ts|],
isEnabledKey = "isSendGridProviderUsed"
setupFnFile = [relfile|sendgrid.ts|]
}
where
sendGridVersionRange :: SV.Range
@ -59,8 +54,7 @@ mailgun :: EmailSenderProvider
mailgun =
EmailSenderProvider
{ npmDependency = Just mailgunDependency,
setupFnFile = [relfile|mailgun.ts|],
isEnabledKey = "isMailgunProviderUsed"
setupFnFile = [relfile|mailgun.ts|]
}
where
mailgunVersionRange :: SV.Range
@ -73,6 +67,5 @@ dummy :: EmailSenderProvider
dummy =
EmailSenderProvider
{ npmDependency = Nothing,
setupFnFile = [relfile|dummy.ts|],
isEnabledKey = "isDummyProviderUsed"
setupFnFile = [relfile|dummy.ts|]
}

View File

@ -4,23 +4,57 @@ module Wasp.Generator.SdkGenerator.EnvValidation
)
where
import Data.Aeson (KeyValue ((.=)), object)
import Data.Maybe (isJust)
import StrongPath (relfile)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.AppSpec.Valid (getApp)
import qualified Wasp.Generator.AuthProviders as AuthProviders
import qualified Wasp.Generator.EmailSenders as EmailSenders
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.SdkGenerator.Common as C
import qualified Wasp.Generator.ServerGenerator.Common as Server
import qualified Wasp.Generator.WebAppGenerator.Common as WebApp
import qualified Wasp.Project.Db as Db
genEnvValidation :: AppSpec -> Generator [FileDraft]
genEnvValidation _spec =
genEnvValidation spec =
sequence
[ genFileCopy [relfile|env/index.ts|],
genFileCopy [relfile|client/env/index.ts|],
genFileCopy [relfile|server/env/index.ts|]
[ genServerEnv spec,
genClientEnv,
genFileCopy [relfile|env/index.ts|]
]
where
genFileCopy = return . C.mkTmplFd
genServerEnv :: AppSpec -> Generator FileDraft
genServerEnv spec = return $ C.mkTmplFdWithData tmplPath tmplData
where
tmplPath = [relfile|server/env.ts|]
tmplData =
object
[ "isAuthEnabled" .= isJust maybeAuth,
"databaseUrlEnvVarName" .= Db.databaseUrlEnvVarName,
"defaultClientUrl" .= WebApp.getDefaultDevClientUrl spec,
"defaultServerUrl" .= Server.defaultDevServerUrl,
"defaultServerPort" .= Server.defaultServerPort,
"enabledAuthProviders" .= (AuthProviders.getEnabledAuthProvidersJson <$> maybeAuth),
"isEmailSenderUsed" .= isJust maybeEmailSender,
"enabledEmailSenders" .= (EmailSenders.getEnabledEmailProvidersJson <$> maybeEmailSender)
]
maybeAuth = AS.App.auth app
maybeEmailSender = AS.App.emailSender app
app = snd $ getApp spec
genClientEnv :: Generator FileDraft
genClientEnv = return $ C.mkTmplFdWithData tmplPath tmplData
where
tmplPath = [relfile|client/env.ts|]
tmplData = object ["defaultServerUrl" .= Server.defaultDevServerUrl]
depsRequiredByEnvValidation :: [AS.Dependency.Dependency]
depsRequiredByEnvValidation =
AS.Dependency.fromList

View File

@ -1,9 +1,11 @@
module Wasp.Generator.SdkGenerator.Server.EmailSenderG where
module Wasp.Generator.SdkGenerator.Server.EmailSenderG
( genNewEmailSenderApi,
depsRequiredByEmail,
)
where
import Data.Aeson (object, (.=))
import qualified Data.Aeson as Aeson
import Data.Maybe (fromMaybe, isJust, maybeToList)
import qualified Data.Text
import StrongPath (File', Path', Rel, relfile, (</>))
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec.App as AS.App
@ -11,6 +13,7 @@ import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.AppSpec.App.EmailSender (EmailSender)
import qualified Wasp.AppSpec.App.EmailSender as AS.EmailSender
import Wasp.AppSpec.Valid (getApp)
import qualified Wasp.Generator.EmailSenders as EmailSenders
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.SdkGenerator.Common as C
@ -32,7 +35,7 @@ genIndex :: EmailSender -> Generator FileDraft
genIndex email = return $ C.mkTmplFdWithData tmplPath tmplData
where
tmplPath = [relfile|server/email/index.ts|]
tmplData = getEmailProvidersJson email
tmplData = EmailSenders.getEnabledEmailProvidersJson email
genCore :: EmailSender -> Generator [FileDraft]
genCore email =
@ -47,7 +50,7 @@ genCoreIndex :: EmailSender -> Generator FileDraft
genCoreIndex email = return $ C.mkTmplFdWithData tmplPath tmplData
where
tmplPath = [relfile|server/email/core/index.ts|]
tmplData = getEmailProvidersJson email
tmplData = EmailSenders.getEnabledEmailProvidersJson email
genCoreTypes :: EmailSender -> Generator FileDraft
genCoreTypes email = return $ C.mkTmplFdWithData tmplPath tmplData
@ -95,14 +98,6 @@ depsRequiredByEmail spec = maybeToList maybeNpmDepedency
maybeProvider = getEmailSenderProvider <$> (AS.App.emailSender . snd . getApp $ spec)
maybeNpmDepedency = maybeProvider >>= Providers.npmDependency
getEmailProvidersJson :: EmailSender -> Aeson.Value
getEmailProvidersJson email =
object [isEnabledKey .= True]
where
provider :: Providers.EmailSenderProvider
provider = getEmailSenderProvider email
isEnabledKey = Data.Text.pack $ Providers.isEnabledKey provider
getEmailSenderProvider :: EmailSender -> Providers.EmailSenderProvider
getEmailSenderProvider email = case AS.EmailSender.provider email of
AS.EmailSender.SMTP -> Providers.smtp

View File

@ -35,7 +35,6 @@ genOAuth auth
sequence
[ genIndexTs auth,
genRedirectHelper,
genFileCopy $ oauthDirInSdkTemplatesDir </> [relfile|env.ts|],
genFileCopy $ oauthDirInSdkTemplatesDir </> [relfile|oneTimeCode.ts|],
genFileCopy $ oauthDirInSdkTemplatesDir </> [relfile|provider.ts|]
]

View File

@ -75,7 +75,6 @@ genServer spec =
<++> genSrcDir spec
<++> genDotEnv spec
<++> genJobs spec
<++> genEnvValidationScript
<++> genApis spec
<++> genCrud spec
where
@ -252,12 +251,6 @@ genRoutesIndex spec =
operationsRouteInRootRouter :: String
operationsRouteInRootRouter = "operations"
genEnvValidationScript :: Generator [FileDraft]
genEnvValidationScript =
return
[ C.mkTmplFd [relfile|scripts/validate-env.mjs|]
]
genMiddleware :: AppSpec -> Generator [FileDraft]
genMiddleware spec =
sequence

View File

@ -227,6 +227,8 @@ getIndexTs spec =
relPathToWebAppSrcDir :: Path Posix (Rel importLocation) (Dir C.WebAppSrcDir)
relPathToWebAppSrcDir = [reldirP|./|]
-- TODO: see if this is still needed after introducing the Zod
-- validation for the env vars.
genEnvValidationScript :: Generator [FileDraft]
genEnvValidationScript =
return

View File

@ -281,6 +281,7 @@ library
Wasp.Generator.DbGenerator.Jobs
Wasp.Generator.DbGenerator.Operations
Wasp.Generator.DockerGenerator
Wasp.Generator.EmailSenders
Wasp.Generator.ExternalCodeGenerator.Common
Wasp.Generator.ExternalConfig.Common
Wasp.Generator.ExternalConfig.PackageJson