Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ There are three projects which can be run individually.
### Web

```
cp apps/web/.env.local.example apps/web/.env.local
yarn workspace @app/web dev
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pinata } from 'apps/web/src/utils/pinata';
import { getPinata } from 'apps/web/src/utils/pinata';
import { isDevelopment } from 'libs/base-ui/constants';
import { NextResponse, NextRequest } from 'next/server';

Expand Down Expand Up @@ -50,6 +50,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'File is too large' }, { status: 500 });

// Upload
const pinata = getPinata();
const uploadData = await pinata.upload.file(file, {
groupId: '765ab5e4-0bc3-47bb-9d6a-35b308291009',
metadata: {
Expand All @@ -58,6 +59,7 @@ export async function POST(request: NextRequest) {
});
return NextResponse.json(uploadData, { status: 200 });
} catch (e) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
const errorMessage = e instanceof Error ? e.message : 'Internal Server Error';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
}
18 changes: 10 additions & 8 deletions apps/web/app/api/cloudinaryUrl/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { v2 as cloudinary } from 'cloudinary';
import { createHash } from 'crypto';
import { logger } from 'apps/web/src/utils/logger';

// Configure Cloudinary
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
import { requireEnv } from 'apps/web/src/utils/env';

const folderName = 'base-org-uploads';

Expand Down Expand Up @@ -86,6 +80,13 @@ async function getCloudinaryMediaUrl({

export async function POST(request: NextRequest) {
try {
// Only validate when this API route is actually invoked.
cloudinary.config({
cloud_name: requireEnv('NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME', { feature: 'Cloudinary' }),
api_key: requireEnv('CLOUDINARY_API_KEY', { feature: 'Cloudinary' }),
api_secret: requireEnv('CLOUDINARY_API_SECRET', { feature: 'Cloudinary' }),
});

const body = (await request.json()) as CloudinaryMediaUrlRequest;

const { media, width } = body;
Expand All @@ -105,6 +106,7 @@ export async function POST(request: NextRequest) {
}
} catch (error) {
logger.error('Error processing Cloudinary URL:', error);
return NextResponse.json({ error: 'Failed to process Cloudinary URL' }, { status: 500 });
const errorMessage = error instanceof Error ? error.message : 'Failed to process Cloudinary URL';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
}
1 change: 1 addition & 0 deletions apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
5 changes: 3 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
"version": "0.0.0",
"private": true,
"scripts": {
"check-env": "node ./scripts/check-env.js",
"start": "node --require ./tracer/initialize.js ./node_modules/.bin/next start",
"build": "next build",
"test": "jest",
"analyze": "ANALYZE=true next build",
"dev": "node --require ./tracer/initialize.js ./node_modules/.bin/next dev",
"dev": "yarn check-env && node --require ./tracer/initialize.js ./node_modules/.bin/next dev",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:headed": "playwright test --headed",
"e2e:metamask:prepare": "node ../../node_modules/@coinbase/onchaintestkit/src/cli/prepare-metamask.mjs",
"dev:million": "cross-env MILLION_LINT=true node --require ./tracer/initialize.js ./node_modules/.bin/next dev",
"dev:million": "yarn check-env && cross-env MILLION_LINT=true node --require ./tracer/initialize.js ./node_modules/.bin/next dev",
"lint": "next lint",
"update-contributors": "node ./scripts/updateContributors.js && npx prettier ./src/components/CoreContributors/CoreContributors.json -w",
"fetch-mirror-blog": "node ./scripts/pullLatestBlogPosts.js"
Expand Down
35 changes: 35 additions & 0 deletions apps/web/scripts/check-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const fs = require('fs');
const path = require('path');

const colors = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
reset: '\x1b[0m',
};

const rootDir = path.join(__dirname, '..');
const examplePath = path.join(rootDir, '.env.local.example');
const localPath = path.join(rootDir, '.env.local');

console.log('Checking environment variables...');

if (!fs.existsSync(localPath)) {
console.warn(`${colors.yellow}Warning: .env.local file is missing.${colors.reset}`);

if (fs.existsSync(examplePath)) {
console.log(
`Please copy ${colors.yellow}.env.local.example${colors.reset} to ${colors.yellow}.env.local${colors.reset} and configure it.`
);
} else {
console.log(
`Please ensure you have a ${colors.yellow}.env.local${colors.reset} file configured.`
);
}
// Do not block local dev; features will validate their own env vars on-demand.
process.exit(0);
}

console.log(
`${colors.green}✅ Environment configuration file found.${colors.reset}\n`
);
27 changes: 24 additions & 3 deletions apps/web/src/utils/bugsnag.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// import React from 'react';
import type { BugsnagPluginReactResult } from '@bugsnag/plugin-react';
import type { OnErrorCallback } from '@bugsnag/core/types/common';
import { formatMissingEnvMessage, getEnv } from 'apps/web/src/utils/env';

type BugsnagClientType = {
notify: (error: Error | string, onError?: OnErrorCallback) => void;
Expand All @@ -26,11 +27,31 @@ async function inititializeBugsnag() {
BugsnagPluginReactInstance = (await import('@bugsnag/plugin-react')).default;

try {
const apiKey = getEnv('NEXT_PUBLIC_BUGSNAG_API_KEY');
const notifyUrl = getEnv('NEXT_PUBLIC_BUGSNAG_NOTIFY_URL');
const sessionsUrl = getEnv('NEXT_PUBLIC_BUGSNAG_SESSIONS_URL');

// Bugsnag is optional; only initialize when it is configured.
if (!apiKey || !notifyUrl || !sessionsUrl) {
if (process.env.NODE_ENV !== 'production') {
const missing = [
!apiKey ? 'NEXT_PUBLIC_BUGSNAG_API_KEY' : null,
!notifyUrl ? 'NEXT_PUBLIC_BUGSNAG_NOTIFY_URL' : null,
!sessionsUrl ? 'NEXT_PUBLIC_BUGSNAG_SESSIONS_URL' : null,
].filter(Boolean);
console.warn(
formatMissingEnvMessage(missing.join(', '), { feature: 'Bugsnag' }),
);
}
BugsnagClient = null;
return;
}

BugsnagClient.start({
apiKey: process.env.NEXT_PUBLIC_BUGSNAG_API_KEY as string,
apiKey,
endpoints: {
notify: process.env.NEXT_PUBLIC_BUGSNAG_NOTIFY_URL as string,
sessions: process.env.NEXT_PUBLIC_BUGSNAG_SESSIONS_URL as string,
notify: notifyUrl,
sessions: sessionsUrl,
},
plugins: [new BugsnagPluginReactInstance()],
});
Expand Down
19 changes: 15 additions & 4 deletions apps/web/src/utils/datastores/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ import { Pool } from 'pg';
import { isDevelopment } from 'apps/web/src/constants';
import { Database } from './types';
import { logger } from 'apps/web/src/utils/logger';
import { requireEnv } from 'apps/web/src/utils/env';

function createDefaultPostgresManager() {
const user = isDevelopment ? process.env.POSTGRES_USER_DEVELOPMENT : process.env.POSTGRES_USER;
const password = isDevelopment ? process.env.POSTGRES_PASSWORD_DEVELOPMENT : process.env.POSTGRES_PASSWORD;
const host = isDevelopment ? process.env.POSTGRES_HOST_DEVELOPMENT : process.env.POSTGRES_HOST;
const dbName = isDevelopment ? process.env.POSTGRES_DB_NAME_DEVELOPMENT : process.env.POSTGRES_DB_NAME;
// Only validate when DB is actually used (getDb()).
const feature = 'Postgres';
const user = isDevelopment
? requireEnv('POSTGRES_USER_DEVELOPMENT', { feature })
: requireEnv('POSTGRES_USER', { feature });
const password = isDevelopment
? requireEnv('POSTGRES_PASSWORD_DEVELOPMENT', { feature })
: requireEnv('POSTGRES_PASSWORD', { feature });
const host = isDevelopment
? requireEnv('POSTGRES_HOST_DEVELOPMENT', { feature })
: requireEnv('POSTGRES_HOST', { feature });
const dbName = isDevelopment
? requireEnv('POSTGRES_DB_NAME_DEVELOPMENT', { feature })
: requireEnv('POSTGRES_DB_NAME', { feature });
const connectionString = `postgresql://${user}:${password}@${host}:5432/${dbName}`;
const poolConfig = isDevelopment
? {
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/utils/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
type EnvHelp = {
/**
* Human-friendly feature name, e.g. "Cloudinary upload" or "Bugsnag".
*/
feature?: string;
/**
* Where to configure the variable (defaults to apps/web/.env.local).
*/
configFile?: string;
/**
* Example file (defaults to apps/web/.env.local.example).
*/
exampleFile?: string;
};

function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}

export function getEnv(name: string): string | undefined {
const value = process.env[name];
return isNonEmptyString(value) ? value : undefined;
}

export function formatMissingEnvMessage(name: string, help: EnvHelp = {}): string {
const featurePrefix = help.feature ? `[${help.feature}] ` : '';
const configFile = help.configFile ?? 'apps/web/.env.local';
const exampleFile = help.exampleFile ?? 'apps/web/.env.local.example';

return (
`${featurePrefix}Missing required environment variable: ${name}\n` +
`Add it to ${configFile} (you can copy from ${exampleFile}).`
);
}

export function requireEnv(name: string, help: EnvHelp = {}): string {
const value = getEnv(name);
if (value) return value;
throw new Error(formatMissingEnvMessage(name, help));
}

17 changes: 13 additions & 4 deletions apps/web/src/utils/pinata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { PinataSDK } from 'pinata';
import { requireEnv } from 'apps/web/src/utils/env';

export const pinata = new PinataSDK({
pinataJwt: `${process.env.PINATA_API_KEY}`,
pinataGateway: `${process.env.PINATA_GATEWAY_URL}`,
});
let _pinata: PinataSDK | undefined;

export function getPinata(): PinataSDK {
if (_pinata) return _pinata;

// Only validate when the feature is actually used (e.g. IPFS upload route).
const pinataJwt = requireEnv('PINATA_API_KEY', { feature: 'Pinata' });
const pinataGateway = requireEnv('PINATA_GATEWAY_URL', { feature: 'Pinata' });

_pinata = new PinataSDK({ pinataJwt, pinataGateway });
return _pinata;
}