mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 01:54:37 +03:00
Define env vars validation. Use validate env vars.
This commit is contained in:
parent
9d618ead9d
commit
7333b259af
@ -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
|
||||
|
@ -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 = {
|
||||
|
14
waspc/data/Generator/templates/sdk/wasp/client/env.ts
Normal file
14
waspc/data/Generator/templates/sdk/wasp/client/env.ts
Normal 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)
|
@ -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)
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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. =}
|
||||
|
@ -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,
|
||||
}
|
@ -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>;
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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 =}
|
||||
}
|
||||
}
|
||||
|
@ -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 =}
|
||||
|
136
waspc/data/Generator/templates/sdk/wasp/server/env.ts
Normal file
136
waspc/data/Generator/templates/sdk/wasp/server/env.ts
Normal 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)
|
@ -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)
|
@ -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>
|
||||
|
@ -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()!'
|
||||
|
@ -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(/\/$/, "");
|
||||
}
|
||||
|
@ -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 =}"
|
||||
|
@ -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');
|
@ -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,
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
23
waspc/src/Wasp/Generator/EmailSenders.hs
Normal file
23
waspc/src/Wasp/Generator/EmailSenders.hs
Normal 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
|
@ -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|]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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|]
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user