Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(api): Added type inference and runtime validation to process.env #200

Merged
merged 7 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
DATABASE_URL=postgresql://postgres:[email protected]:5432/keyshade_db
[email protected]

REDIS_URL=redis://127.0.0.1:6379
REDIS_PASSWORD=

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
node-version: 20.x
registry-url: https://registry.npmjs.org
- name: install
run: npm install --no-save @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/changelog @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits
run: pnpm install --no-save @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/changelog @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits
- name: release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/validate-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jobs:
GITHUB_CLIENT_SECRET: dummy
GITHUB_CALLBACK_URL: dummy
REDIS_URL: redis://localhost:6379
JWT_SECRET: secret
run: pnpm run e2e:api

- name: Upload e2e test coverage reports to Codecov
Expand Down
12 changes: 8 additions & 4 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "node dist/main",
"dev": "nest start --watch",
"dev": "cross-env NODE_ENV=dev nest start --watch",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"db:generate-types": "pnpm dlx prisma generate --schema=src/prisma/schema.prisma",
"db:generate-migrations": "pnpx prisma migrate dev --create-only --skip-seed --schema=src/prisma/schema.prisma",
"db:deploy-migrations": " pnpx prisma migrate deploy --schema=src/prisma/schema.prisma",
"db:validate": "pnpm dlx prisma validate --schema=src/prisma/schema.prisma",
"db:format": "pnpm dlx prisma format --schema=src/prisma/schema.prisma",
"db:reset": "pnpm dlx prisma migrate reset --force --schema=src/prisma/schema.prisma",
"sourcemaps": "sentry-cli sourcemaps inject ./dist && sentry-cli sourcemaps upload ./dist || echo 'Failed to upload source maps to Sentry'"
},
"dependencies": {
Expand All @@ -24,7 +30,6 @@
"@nestjs/schedule": "^4.0.1",
"@nestjs/swagger": "^7.3.0",
"@nestjs/websockets": "^10.3.7",
"@prisma/client": "^5.10.1",
"@socket.io/redis-adapter": "^8.3.0",
"@supabase/supabase-js": "^2.39.6",
"@types/uuid": "^9.0.8",
Expand All @@ -38,7 +43,6 @@
"passport-github2": "^0.1.12",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"prisma": "^5.10.1",
"redis": "^4.6.13",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
Expand Down Expand Up @@ -92,4 +96,4 @@
"keywords": [],
"author": "",
"license": "ISC"
}
}
10 changes: 9 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ import { ApprovalModule } from '../approval/approval.module'
import { SocketModule } from '../socket/socket.module'
import { ProviderModule } from '../provider/provider.module'
import { ScheduleModule } from '@nestjs/schedule'
import { EnvSchema } from '../common/env/env.schema'
import { IntegrationModule } from '../integration/integration.module'

@Module({
controllers: [AppController],
imports: [
ConfigModule.forRoot({
isGlobal: true
isGlobal: true,
// For some reason config module is looking for .env in the api directory so defining custom path
envFilePath: '../../.env',
validate: EnvSchema.parse,
validationOptions: {
allowUnknown: false,
abortEarly: true
}
}),
ScheduleModule.forRoot(),
PassportModule,
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/auth/guard/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { toSHA256 } from '../../../common/to-sha256'
const X_E2E_USER_EMAIL = 'x-e2e-user-email'
const X_KEYSHADE_TOKEN = 'x-keyshade-token'

// FIXME: Error at line:47 & line:55 process.env.NODE_ENV === 'dev'

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
Expand All @@ -40,12 +42,15 @@ export class AuthGuard implements CanActivate {
const request = context.switchToHttp().getRequest()
const authType = this.getAuthType(request)

//@ts-expect-error process.env.NODE_ENV parses to 'dev'
if (process.env.NODE_ENV !== 'e2e' && authType === 'NONE') {
throw new ForbiddenException('No authentication provided')
}

// In case the environment is e2e, we want to authenticate the user using the email
// else we want to authenticate the user using the JWT token.

// @ts-expect-error process.env.NODE_ENV parses to 'dev'
if (authType !== 'API_KEY' && process.env.NODE_ENV === 'e2e') {
const email = request.headers[X_E2E_USER_EMAIL]
if (!email) {
Expand Down
7 changes: 7 additions & 0 deletions apps/api/src/common/env/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { EnvSchemaType } from './env.schema'

declare global {
namespace NodeJS {
interface ProcessEnv extends EnvSchemaType {}
}
}
75 changes: 75 additions & 0 deletions apps/api/src/common/env/env.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { z } from 'zod'

/*

Apparently zod validates empty strings.

https://github.com/colinhacks/zod/issues/2466

So if you have your variable in the .env set to empty, zod turns a blind eye to it since it parses to

VARIABLE = ''

To over come this you need to set a min length (.min()) if you want zod to throw an error

Zod only throws errors if a variable is missing completely from .env

Use the .optional() property if you are okay with a variable being omitted from .env file

*/

const e2eEnvSchema = z.object({
NODE_ENV: z.literal('e2e'),
DATABASE_URL: z.string(),
REDIS_URL: z.string(),
JWT_SECRET: z.string()
})

const generalSchema = z.object({
NODE_ENV: z.literal('dev'),
DATABASE_URL: z.string(),
ADMIN_EMAIL: z.string(),

REDIS_URL: z.string(),
REDIS_PASSWORD: z.string().optional(),

GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
GITHUB_CALLBACK_URL: z.string().optional(),

API_PORT: z.string().optional(),

GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
GOOGLE_CALLBACK_URL: z.string().optional(),

GITLAB_CLIENT_ID: z.string().optional(),
GITLAB_CLIENT_SECRET: z.string().optional(),
GITLAB_CALLBACK_URL: z.string().optional(),

SENTRY_DSN: z.string().optional(),
SENTRY_ORG: z.string().optional(),
SENTRY_PROJECT: z.string().optional(),
SENTRY_TRACES_SAMPLE_RATE: z.string().optional(),
SENTRY_PROFILES_SAMPLE_RATE: z.string().optional(),
SENTRY_ENV: z.string().optional(),

SMTP_HOST: z.string(),
SMTP_PORT: z.string(),
SMTP_EMAIL_ADDRESS: z.string(),
SMTP_PASSWORD: z.string(),
// TODO: add regex check for FORM_EMAIL value as represented in .env.example (your-name <[email protected]>)
jamesfrye420 marked this conversation as resolved.
Show resolved Hide resolved
FROM_EMAIL: z.string(),

JWT_SECRET: z.string(),

WEB_FRONTEND_URL: z.string().url(),
WORKSPACE_FRONTEND_URL: z.string().url()
})

export type EnvSchemaType = z.infer<typeof generalSchema>

export const EnvSchema = z.discriminatedUnion('NODE_ENV', [
e2eEnvSchema,
generalSchema
])
2 changes: 2 additions & 0 deletions apps/api/src/user/service/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export class UserService {
}

private async checkIfAdminExistsOrCreate() {
// @ts-expect-error process.env.NODE_ENV parses to 'dev'
// FIXME
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'e2e') {
return
}
Expand Down
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,17 @@
"test": "turbo run test",
"test:api": "pnpm unit:api && pnpm e2e:api",
"unit:api": "pnpm db:generate-types && turbo run test --filter=api -- --config=jest.config.ts",
"e2e:api:prepare": "docker compose down && docker compose -f docker-compose-test.yml up -d && pnpm db:generate-types && NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' pnpm run db:deploy-migrations",
"e2e:api": "pnpm run e2e:api:prepare && NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' turbo run test --no-cache --filter=api -- --runInBand --config=jest.e2e-config.ts --coverage --coverageDirectory=../../coverage-e2e/api --coverageReporters=json && pnpm run e2e:api:teardown",
"e2e:api:prepare": "docker compose down && docker compose -f docker-compose-test.yml up -d && pnpm db:generate-types && cross-env NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' pnpm run db:deploy-migrations",
"e2e:api": "pnpm run e2e:api:prepare && cross-env NODE_ENV='e2e' DATABASE_URL='postgresql://prisma:prisma@localhost:5432/tests' turbo run test --no-cache --filter=api -- --runInBand --config=jest.e2e-config.ts --coverage --coverageDirectory=../../coverage-e2e/api --coverageReporters=json && pnpm run e2e:api:teardown",
"e2e:api:teardown": "docker compose -f docker-compose-test.yml down",
"test:web": "turbo run test --filter=web",
"test:workspace": "turbo run test --filter=workspace",
"db:generate-types": "pnpm dlx prisma generate --schema=apps/api/src/prisma/schema.prisma",
"db:generate-migrations": "pnpx prisma migrate dev --create-only --skip-seed --schema=apps/api/src/prisma/schema.prisma",
"db:deploy-migrations": " pnpx prisma migrate deploy --schema=apps/api/src/prisma/schema.prisma",
"db:validate": "pnpm dlx prisma validate --schema=apps/api/src/prisma/schema.prisma",
"db:format": "pnpm dlx prisma format --schema=apps/api/src/prisma/schema.prisma",
"db:reset": "pnpm dlx prisma migrate reset --force --schema=apps/api/src/prisma/schema.prisma",
"db:generate-types": "pnpm run --filter=api db:generate-types",
"db:generate-migrations": "pnpm run --filter=api db:generate-migrations",
"db:deploy-migrations": "pnpm run --filter=api db:deploy-migrations",
"db:validate": "pnpm run --filter=api db:validate",
"db:format": "pnpm run --filter=api db:format",
"db:reset": "pnpm run --filter=api db:reset",
"format": "prettier apps/**/*.{ts,tsx} --write",
"prepare": "husky",
"sourcemaps:api": "turbo run sourcemaps --filter=api"
Expand All @@ -126,6 +126,7 @@
"devDependencies": {
"@sentry/cli": "^2.28.6",
"@sentry/webpack-plugin": "^2.14.2",
"cross-env": "^7.0.3",
"husky": "^9.0.11",
"prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.5.11",
Expand All @@ -135,10 +136,11 @@
},
"dependencies": {
"@million/lint": "^0.0.73",
"@prisma/client": "5.12.1",
"@prisma/client": "^5.13.0",
"@sentry/node": "^7.102.0",
"@sentry/profiling-node": "^7.102.0",
"million": "^3.0.5",
"sharp": "^0.33.3"
"sharp": "^0.33.3",
"zod": "^3.23.6"
}
}
Loading
Loading