make all env variables required at build time #110 (#111)

* make all env variables required at build time

* update env files

* add .env.docker file with basic docker configuration

* update Readme with new instructions
This commit is contained in:
Matti Nannt
2022-10-16 11:10:20 +02:00
committed by GitHub
parent e910a97d32
commit 6cd3878700
16 changed files with 81 additions and 185 deletions
+6 -6
View File
@@ -39,19 +39,19 @@ DATABASE_URL='postgresql://postgres:postgres@postgres:5432/snoopforms?schema=pub
#####################
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
EMAIL_VERIFICATION_DISABLED=1
NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
PASSWORD_RESET_DISABLED=1
NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1
#######################
# Additional Options #
#######################
# TERMS_URL=https://www.example.com/terms
# PRIVACY_URL=https://www.example.com/privacy
# PUBLIC_IMPRINT_URL=https://www.example.com/imprint
# PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
# NEXT_PUBLIC_TERMS_URL=https://www.example.com/terms
# NEXT_PUBLIC_PRIVACY_URL=https://www.example.com/privacy
# NEXT_PUBLIC_IMPRINT_URL=https://www.example.com/imprint
# NEXT_PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
######################
# Posthog Tracking #
+6 -6
View File
@@ -41,19 +41,19 @@ SMTP_PASSWORD=smtpPassword
#####################
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
# EMAIL_VERIFICATION_DISABLED=1
# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
# PASSWORD_RESET_DISABLED=1
# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1
#######################
# Additional Options #
#######################
# TERMS_URL=https://www.example.com/terms
# PRIVACY_URL=https://www.example.com/privacy
# PUBLIC_IMPRINT_URL=https://www.example.com/imprint
# PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
# NEXT_PUBLIC_TERMS_URL=https://www.example.com/terms
# NEXT_PUBLIC_PRIVACY_URL=https://www.example.com/privacy
# NEXT_PUBLIC_IMPRINT_URL=https://www.example.com/imprint
# NEXT_PUBLIC_PRIVACY_URL=https://www.example.com/enduserPrivacy
######################
# Posthog Tracking #
-69
View File
@@ -1,69 +0,0 @@
name: Docker Build & Publish
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches:
- "main"
tags:
- "v*.*.*"
pull_request:
branches:
- "main"
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
file: apps/web/Dockerfile
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+4 -2
View File
@@ -126,7 +126,7 @@ git clone https://github.com/formbricks/snoopforms.git && cd snoopforms
```
Create a `.env` file based on `.env.docker` and change all fields according to your setup. This file comes with a basic setup and snoopForms works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings in the `.env` file. If you configured your email credentials, you can also comment the following lines to enable email verification (`# EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# PASSWORD_RESET_DISABLED=1`).
Create a `.env` file based on `.env.docker` and change all fields according to your setup. This file comes with a basic setup and snoopForms works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings in the `.env` file. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`).
Copy the `.env.docker` file to `.env` and edit it with an editor of your choice if needed.
@@ -136,7 +136,9 @@ cp .env.docker .env
```
Start the docker compose process to build and spin up the snoopForms container as well as the PostgreSQL database.
Note: The environment variables are used at build time. When you change environment variables later, you need to rebuild the image with `docker compose build` for the changes to take effect.
Finally start the docker compose process to build and spin up the snoopForms container as well as the PostgreSQL database.
```bash
+3 -1
View File
@@ -7,6 +7,9 @@ WORKDIR /app
# First install the dependencies (as they change less often)
COPY . .
# Copy .env file because Docker don't follow symlinks
COPY .env /app/apps/web/
RUN pnpm install
# Build the project
@@ -33,7 +36,6 @@ COPY --from=installer /app/apps/web/package.json .
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
COPY --from=installer --chown=nextjs:nodejs /.env ./apps/web/.env
COPY --from=installer --chown=nextjs:nodejs /app/packages/database/prisma ./packages/database/prisma
CMD pnpm dlx prisma migrate deploy && node apps/web/server.js
+9 -14
View File
@@ -1,9 +1,6 @@
import getConfig from "next/config";
import { createToken } from "./jwt";
const nodemailer = require("nodemailer");
const { serverRuntimeConfig } = getConfig();
interface sendEmailData {
to: string;
subject: string;
@@ -13,18 +10,18 @@ interface sendEmailData {
export const sendEmail = async (emailData: sendEmailData) => {
let transporter = nodemailer.createTransport({
host: serverRuntimeConfig.smtpHost,
port: serverRuntimeConfig.smtpPort,
secure: serverRuntimeConfig.smtpSecureEnabled, // true for 465, false for other ports
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE_ENABLED === "1", // true for 465, false for other ports
auth: {
user: serverRuntimeConfig.smtpUser,
pass: serverRuntimeConfig.smtpPassword,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
// logger: true,
// debug: true,
});
const emailDefaults = {
from: `snoopForms <${serverRuntimeConfig.mailFrom || "noreply@snoopforms.com"}>`,
from: `snoopForms <${process.env.MAIL_FROM || "noreply@snoopforms.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
};
@@ -33,9 +30,9 @@ export const sendVerificationEmail = async (user) => {
const token = createToken(user.id, user.email, {
expiresIn: "1d",
});
const verifyLink = `${serverRuntimeConfig.nextauthUrl}/auth/verify?token=${encodeURIComponent(token)}`;
const verifyLink = `${process.env.NEXTAUTH_URL}/auth/verify?token=${encodeURIComponent(token)}`;
const verificationRequestLink = `${
serverRuntimeConfig.nextauthUrl
process.env.NEXTAUTH_URL
}/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
await sendEmail({
to: user.email,
@@ -54,9 +51,7 @@ export const sendForgotPasswordEmail = async (user) => {
const token = createToken(user.id, user.email, {
expiresIn: "1d",
});
const verifyLink = `${serverRuntimeConfig.nextauthUrl}/auth/reset-password?token=${encodeURIComponent(
token
)}`;
const verifyLink = `${process.env.NEXTAUTH_URL}/auth/reset-password?token=${encodeURIComponent(token)}`;
await sendEmail({
to: user.email,
subject: "Reset your snoopForms password",
+2 -5
View File
@@ -1,11 +1,8 @@
import jwt from "jsonwebtoken";
import getConfig from "next/config";
import { prisma } from "database";
const { serverRuntimeConfig } = getConfig();
export function createToken(userId, userEmail, options = {}) {
return jwt.sign({ id: userId }, serverRuntimeConfig.nextauthSecret + userEmail, options);
return jwt.sign({ id: userId }, process.env.NEXTAUTH_SECRET + userEmail, options);
}
export async function verifyToken(token, userEmail = "") {
@@ -23,5 +20,5 @@ export async function verifyToken(token, userEmail = "") {
userEmail = foundUser.email;
}
return jwt.verify(token, serverRuntimeConfig.nextauthSecret + userEmail);
return jwt.verify(token, process.env.NEXTAUTH_SECRET + userEmail);
}
+3 -7
View File
@@ -1,22 +1,18 @@
import getConfig from "next/config";
import { hashString } from "./utils";
const { serverRuntimeConfig } = getConfig();
const enabled =
process.env.NODE_ENV === "production" &&
serverRuntimeConfig.posthogApiKey &&
serverRuntimeConfig.posthogApiHost;
process.env.NODE_ENV === "production" && process.env.POSTHOG_API_HOST && process.env.POSTHOG_API_KEY;
export const capturePosthogEvent = async (userId, eventName, properties = {}) => {
if (!enabled) {
return;
}
try {
await fetch(`${serverRuntimeConfig.posthogApiHost}/capture/`, {
await fetch(`${process.env.POSTHOG_API_HOST}/capture/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: serverRuntimeConfig.posthogApiKey,
api_key: process.env.POSTHOG_API_KEY,
event: eventName,
properties: {
distinct_id: hashString(userId.toString()),
+3 -6
View File
@@ -1,8 +1,5 @@
import getConfig from "next/config";
import { hashString } from "./utils";
const { serverRuntimeConfig } = getConfig();
/* We use this telemetry service to better understand how snoopForms is being used
and how we can improve it. All data including the IP address is collected anonymously
and we cannot trace anything back to you or your customers. If you still want to
@@ -10,9 +7,9 @@ const { serverRuntimeConfig } = getConfig();
export const sendTelemetry = async (eventName: string) => {
if (
!serverRuntimeConfig.telemetryDisabled &&
process.env.TELEMETRY_DISABLED !== "1" &&
process.env.NODE_ENV === "production" &&
serverRuntimeConfig.nextauthUrl !== "http://localhost:3000"
process.env.NEXTAUTH_URL !== "http://localhost:3000"
) {
try {
await fetch("https://posthog.snoopforms.com/capture/", {
@@ -22,7 +19,7 @@ export const sendTelemetry = async (eventName: string) => {
api_key: "phc_BTq4eagaCzPyUSURXVYwlScTQRvcmBDXjYh7OG6kiqw",
event: eventName,
properties: {
distinct_id: hashString(serverRuntimeConfig.nextauthUrl),
distinct_id: hashString(process.env.NEXTAUTH_URL),
},
timestamp: new Date().toISOString(),
}),
-23
View File
@@ -7,29 +7,6 @@ const nextConfig = {
experimental: {
outputFileTracingRoot: path.join(__dirname, "../../"),
},
serverRuntimeConfig: {
// Will only be available on the server side
nextauthSecret: process.env.NEXTAUTH_SECRET,
nextauthUrl: process.env.NEXTAUTH_URL,
mailFrom: process.env.MAIL_FROM,
smtpHost: process.env.SMTP_HOST,
smtpPort: process.env.SMTP_PORT,
smtpUser: process.env.SMTP_USER,
smtpPassword: process.env.SMTP_PASSWORD,
smtpSecureEnabled: process.env.SMTP_SECURE_ENABLED === "1",
posthogApiHost: process.env.POSTHOG_API_HOST,
posthogApiKey: process.env.POSTHOG_API_KEY,
telemetryDisabled: process.env.TELEMETRY_DISABLED === "1",
},
publicRuntimeConfig: {
// Will be available on both server and client
termsUrl: process.env.TERMS_URL,
privacyUrl: process.env.PRIVACY_URL,
publicImprintUrl: process.env.PUBLIC_IMPRINT_URL,
publicPrivacyUrl: process.env.PUBLIC_PRIVACY_URL,
emailVerificationDisabled: process.env.EMAIL_VERIFICATION_DISABLED === "1",
passwordResetDisabled: process.env.PASSWORD_RESET_DISABLED === "1",
},
async redirects() {
return [
{
+1 -4
View File
@@ -1,13 +1,10 @@
import { NextApiRequest, NextApiResponse } from "next";
import getConfig from "next/config";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { prisma } from "database";
import { verifyPassword } from "../../../lib/auth";
import { verifyToken } from "../../../lib/jwt";
const { publicRuntimeConfig } = getConfig();
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
return await NextAuth(req, res, {
providers: [
@@ -124,7 +121,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
],
callbacks: {
async signIn({ user }: any) {
if (user.emailVerified || publicRuntimeConfig.emailVerificationDisabled) {
if (user.emailVerified || process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED === "1") {
return true;
} else {
// Return false to display a default error message or you can return a URL to redirect to
+1 -6
View File
@@ -1,11 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "database";
import { sendVerificationEmail } from "../../../../lib/email";
import getConfig from "next/config";
import { capturePosthogEvent } from "../../../../lib/posthog";
const { publicRuntimeConfig } = getConfig();
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// POST /api/public/users
// Creates a new user
@@ -15,8 +12,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
let user = req.body;
user = { ...user, ...{ email: user.email.toLowerCase() } };
const { emailVerificationDisabled } = publicRuntimeConfig;
// create user in database
try {
const userData = await prisma.user.create({
@@ -24,7 +19,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
...user,
},
});
if (!emailVerificationDisabled) await sendVerificationEmail(userData);
if (process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") await sendVerificationEmail(userData);
capturePosthogEvent(user.email, "user created");
res.json(userData);
} catch (e) {
+1 -5
View File
@@ -1,14 +1,10 @@
import { XCircleIcon } from "@heroicons/react/24/solid";
import { signIn } from "next-auth/react";
import getConfig from "next/config";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import BaseLayoutUnauthorized from "../../components/layout/BaseLayoutUnauthorized";
const { publicRuntimeConfig } = getConfig();
const { passwordResetDisabled } = publicRuntimeConfig;
export default function SignInPage() {
const router = useRouter();
const { error } = router.query;
@@ -91,7 +87,7 @@ export default function SignInPage() {
Sign in
</button>
<div className="mt-3 text-center">
{!passwordResetDisabled && (
{process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && (
<Link href="/auth/forgot-password">
<a href="" className="text-red block text-xs hover:text-red-600">
Forgot your password?
+12 -14
View File
@@ -2,19 +2,14 @@ import { XCircleIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import getConfig from "next/config";
import { useState } from "react";
import BaseLayoutUnauthorized from "../../components/layout/BaseLayoutUnauthorized";
import { createUser } from "../../lib/users";
const { publicRuntimeConfig } = getConfig();
export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string>("");
const { emailVerificationDisabled, privacyUrl, termsUrl } = publicRuntimeConfig;
const handleSubmit = async (e) => {
e.preventDefault();
try {
@@ -25,9 +20,10 @@ export default function SignUpPage() {
e.target.elements.password.value
);
const url = emailVerificationDisabled
? `/auth/signup-without-verification-success`
: `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`;
const url =
process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED === "1"
? `/auth/signup-without-verification-success`
: `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`;
router.push(url);
} catch (e) {
@@ -136,24 +132,26 @@ export default function SignUpPage() {
<a className="text-red hover:text-red-600">Log in.</a>
</Link>
</div>
{(termsUrl || privacyUrl) && (
{(process.env.NEXT_PUBLIC_TERMS_URL || process.env.NEXT_PUBLIC_PRIVACY_URL) && (
<div className="mt-3 text-center text-xs text-gray-400">
By clicking &quot;Sign Up&quot;, you agree to our
<br />
{termsUrl && (
{process.env.NEXT_PUBLIC_TERMS_URL && (
<a
className="text-red hover:text-red-600"
href={termsUrl}
href={process.env.NEXT_PUBLIC_TERMS_URL}
rel="noreferrer"
target="_blank">
terms of service
</a>
)}
{termsUrl && privacyUrl && <span> and </span>}
{privacyUrl && (
{process.env.NEXT_PUBLIC_TERMS_URL && process.env.NEXT_PUBLIC_PRIVACY_URL && (
<span> and </span>
)}
{process.env.NEXT_PUBLIC_PRIVACY_URL && (
<a
className="text-red hover:text-red-600"
href={privacyUrl}
href={process.env.NEXT_PUBLIC_PRIVACY_URL}
rel="noreferrer"
target="_blank">
privacy policy
+8 -14
View File
@@ -5,10 +5,6 @@ import MessagePage from "../../components/MessagePage";
import { useNoCodeFormPublic } from "../../lib/noCodeForm";
import { useRouter } from "next/router";
import Image from "next/image";
import getConfig from "next/config";
const { publicRuntimeConfig } = getConfig();
const { publicPrivacyUrl, publicImprintUrl } = publicRuntimeConfig;
function NoCodeFormPublic() {
const router = useRouter();
@@ -43,18 +39,20 @@ function NoCodeFormPublic() {
) : (
<App formId={formId} blocks={noCodeForm.blocks} />
)}
{(publicPrivacyUrl || publicImprintUrl) && (
{(process.env.NEXT_PUBLIC_PRIVACY_URL || process.env.NEXT_PUBLIC_IMPRINT_URL) && (
<footer className="flex h-10 w-full items-center justify-center text-xs text-gray-300">
{publicImprintUrl && (
{process.env.NEXT_PUBLIC_IMPRINT_URL && (
<>
<a href={publicImprintUrl} target="_blank" rel="noreferrer">
<a href={process.env.NEXT_PUBLIC_IMPRINT_URL} target="_blank" rel="noreferrer">
Imprint
</a>
</>
)}
{publicImprintUrl && publicPrivacyUrl && <span className="px-2">|</span>}
{publicPrivacyUrl && (
<a href={publicPrivacyUrl} target="_blank" rel="noreferrer">
{process.env.NEXT_PUBLIC_IMPRINT_URL && process.env.NEXT_PUBLIC_PRIVACY_URL && (
<span className="px-2">|</span>
)}
{process.env.NEXT_PUBLIC_PRIVACY_URL && (
<a href={process.env.NEXT_PUBLIC_PRIVACY_URL} target="_blank" rel="noreferrer">
Privacy Policy
</a>
)}
@@ -65,8 +63,4 @@ function NoCodeFormPublic() {
);
}
NoCodeFormPublic.getInitialProps = () => {
return {};
};
export default NoCodeFormPublic;
+22 -3
View File
@@ -3,7 +3,27 @@
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
"outputs": ["dist/**", ".next/**"],
"env": [
"MAIL_FROM",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"NODE_ENV",
"NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED",
"NEXT_PUBLIC_IMPRINT_URL",
"NEXT_PUBLIC_PASSWORD_RESET_DISABLED",
"NEXT_PUBLIC_PRIVACY_URL",
"NEXT_PUBLIC_PRIVACY_URL",
"NEXT_PUBLIC_TERMS_URL",
"POSTHOG_API_HOST",
"POSTHOG_API_KEY",
"SMTP_HOST",
"SMTP_PORT",
"SMTP_USER",
"SMTP_PASSWORD",
"SMTP_SECURE_ENABLED",
"TELEMETRY_DISABLED"
]
},
"db:migrate:deploy": {
"outputs": []
@@ -23,6 +43,5 @@
"lint": {
"outputs": []
}
},
"globalEnv": ["NODE_ENV"]
}
}