Skip to content
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
60 changes: 54 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,32 +1,80 @@
# ─── App ──────────────────────────────────────────────────────────────────────
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:3001

# ─── Stellar Network ──────────────────────────────────────────────────────────
# Options: testnet | mainnet
# Options: testnet | mainnet | futurenet
NEXT_PUBLIC_STELLAR_NETWORK=testnet
NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"

# ─── Stellar Explorer (optional) ──────────────────────────────────────────────
# NEXT_PUBLIC_STELLAR_EXPLORER_URL=https://stellar.expert/explorer/testnet/tx

# ─── Campaign contract (optional) ─────────────────────────────────────────────
# NEXT_PUBLIC_CAMPAIGN_CONTRACT_ID=

# ─── Stellar Wallets Kit ──────────────────────────────────────────────────────
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
NEXT_PUBLIC_WALLET_APP_NAME=OrbitChain
NEXT_PUBLIC_WALLET_APP_DESCRIPTION=Blockchain-based crowdfunding on Stellar
NEXT_PUBLIC_WALLET_APP_URL=https://orbitchain.app
NEXT_PUBLIC_WALLET_APP_ICON=https://orbitchain.app/icon.png

# Wallet feature toggles (optional)
# NEXT_PUBLIC_FREIGHTER_SHOW_WHEN_MISSING=true
# NEXT_PUBLIC_ALBEDO_CALLBACK_URL=
# NEXT_PUBLIC_LOBSTR_ENABLED=true
# NEXT_PUBLIC_RABET_NETWORK=TESTNET
# NEXT_PUBLIC_XBULL_ENABLED=true
# NEXT_PUBLIC_HANA_ENABLED=false
# NEXT_PUBLIC_DEFAULT_WALLET=freighter

# ─── Auth (server-only) ───────────────────────────────────────────────────────
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=change-me-to-a-random-32-char-string
AUTH_SECRET=change-me-to-a-random-32-char-string

# OAuth providers (optional — leave empty to disable)
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=

# ─── Database (server-only) ───────────────────────────────────────────────────
DATABASE_URL=postgresql://user:password@localhost:5432/orbitchain

# ─── OAuth (server-only) ──────────────────────────────────────────────────────
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# REDIS_URL=redis://localhost:6379

# ─── Stellar Admin (server-only — never expose to client) ─────────────────────
STELLAR_ADMIN_SECRET_KEY=
# STELLAR_ADMIN_SECRET_KEY=

# ─── Cloudinary (optional) ────────────────────────────────────────────────────
# NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
# NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=
# CLOUDINARY_API_KEY=
# CLOUDINARY_API_SECRET=

# ─── Error tracking (optional) ────────────────────────────────────────────────
# NEXT_PUBLIC_ERROR_TRACKING_ENABLED=false
# NEXT_PUBLIC_ENVIRONMENT=development
# NEXT_PUBLIC_APP_VERSION=0.1.0
# NEXT_PUBLIC_ERROR_SAMPLE_RATE=1

# ─── Third-party (optional) ───────────────────────────────────────────────────
# NEXT_PUBLIC_ANCHOR_URL=
# NEXT_PUBLIC_STELLAR_EXPERT_URL=
# NEXT_PUBLIC_COINGECKO_API_KEY=
# NEXT_PUBLIC_SENTRY_DSN=
# NEXT_PUBLIC_GA_MEASUREMENT_ID=

# ─── Legacy (unused — components/n18n scaffolding) ────────────────────────────
# APP_NAME=OrbitChain
# DB_TYPE=postgres
# DB_HOST=localhost
# DB_PORT=5432
# DB_USERNAME=postgres
# DB_PASSWORD=password
# DB_NAME=
# DB_SYNCHRONIZE=false
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ npm start
| `start` | `npm run start` | Start production server |
| `lint` | `npm run lint` | Run ESLint |
| `type-check` | `npm run type-check` | Run TypeScript type checking |
| `test` | `npm test` | Run the test suite (env-contract regression tests) |
| `storybook` | `npm run storybook` | Launch Storybook component library |
| `build-storybook` | `npm run build-storybook` | Build Storybook for deployment |

Expand Down Expand Up @@ -250,14 +251,17 @@ We welcome contributions! Please follow these guidelines:
## 🧪 Testing

```bash
# Run Storybook component tests
npm run storybook
# Run the test suite (environment-contract regression tests)
npm test

# Run type checking
npm run type-check

# Run linter
npm run lint

# Run Storybook component tests
npm run storybook
```

---
Expand Down
2 changes: 1 addition & 1 deletion app/(main)/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ function createFallbackProject(id: string): Project | null {
}

async function fetchProject(id: string): Promise<Project | null> {
const apiBaseUrl = (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api').replace(/\/+$/, '');
const apiBaseUrl = (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001').replace(/\/+$/, '');

try {
const response = await fetch(`${apiBaseUrl}/projects/${encodeURIComponent(id)}`, {
Expand Down
2 changes: 1 addition & 1 deletion components/donations/DonationChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface RangeOption {

// ─── Constants ────────────────────────────────────────────────────────────────

const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

const RANGES: RangeOption[] = [
{ label: '7D', value: '7d', days: 7 },
Expand Down
2 changes: 1 addition & 1 deletion components/donations/DonationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function DonationModal({

const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 5000);
const apiBaseUrl = (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api').replace(/\/+$/, '');
const apiBaseUrl = (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001').replace(/\/+$/, '');
try {
const response = await fetch(`${apiBaseUrl}/donations/confirmation-email`, {
method: 'POST',
Expand Down
2 changes: 1 addition & 1 deletion components/donations/RecentDonations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const HORIZON_EXPLORER =
process.env.NEXT_PUBLIC_STELLAR_EXPLORER_URL ||
'https://stellar.expert/explorer/testnet/tx';

const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

const PAGE_SIZE = 10;

Expand Down
17 changes: 17 additions & 0 deletions instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Next.js instrumentation hook.
*
* Runs once when the Next.js server instance boots (`next dev` / `next start`)
* and during `next build`, so missing or invalid required environment
* variables fail fast with the variable name instead of surfacing later as
* cryptic runtime request errors.
*
* The schema in `lib/env.ts` is the single source of truth: it separates
* required from optional variables, so this hook only fails builds when a
* genuinely required variable is missing or invalid. Optional variables are
* reported as warnings and never break the build.
*/
export async function register(): Promise<void> {
const { assertEnv } = await import('./lib/env');
assertEnv();
}
2 changes: 1 addition & 1 deletion lib/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios';

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

export const apiClient = axios.create({
baseURL: API_URL,
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/verifyToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* here, swap this module's internals and every call site keeps working.
*/

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

export interface VerifiedToken {
userId: string;
Expand Down
120 changes: 119 additions & 1 deletion lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type EnvSchema = {
// API
NEXT_PUBLIC_API_URL: string;

// App / SEO
NEXT_PUBLIC_APP_URL: string;
NEXT_PUBLIC_BASE_URL: string;

// Stellar core
NEXT_PUBLIC_STELLAR_NETWORK: 'testnet' | 'mainnet' | 'futurenet';
NEXT_PUBLIC_STELLAR_HORIZON_URL: string;
Expand Down Expand Up @@ -61,6 +65,15 @@ type EnvSchema = {
NEXT_PUBLIC_SENTRY_DSN: string;
NEXT_PUBLIC_GA_MEASUREMENT_ID: string;

// Error tracking
NEXT_PUBLIC_ERROR_TRACKING_ENABLED: string;
NEXT_PUBLIC_ENVIRONMENT: string;
NEXT_PUBLIC_APP_VERSION: string;
NEXT_PUBLIC_ERROR_SAMPLE_RATE: string;

// Stellar explorer links
NEXT_PUBLIC_STELLAR_EXPLORER_URL: string;

// Cloudinary
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: string;
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET: string;
Expand All @@ -70,6 +83,27 @@ type EnvSchema = {
DATABASE_URL: string;
STELLAR_ADMIN_SECRET_KEY: string;
REDIS_URL: string;

// Cloudinary (server-side deletion)
CLOUDINARY_API_KEY: string;
CLOUDINARY_API_SECRET: string;

// NextAuth / OAuth providers
NEXTAUTH_SECRET: string;
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;

// Legacy n18n scaffolding (components/n18n — unused, declared to close the contract)
APP_NAME: string;
DB_TYPE: string;
DB_HOST: string;
DB_PORT: string;
DB_USERNAME: string;
DB_PASSWORD: string;
DB_NAME: string;
DB_SYNCHRONIZE: string;
};

// ---------------------------------------------------------------------------
Expand All @@ -82,14 +116,26 @@ interface Rule {
validate?: (value: string) => string | null;
}

const RULES: Record<keyof EnvSchema, Rule> = {
export const RULES: Record<keyof EnvSchema, Rule> = {
// API
NEXT_PUBLIC_API_URL: {
description: 'Backend API base URL',
required: true,
validate: (v) => (isUrl(v) ? null : 'Must be a valid http/https URL'),
},

// App / SEO
NEXT_PUBLIC_APP_URL: {
description: 'Canonical app URL used for shareable campaign links',
required: false,
validate: (v) => (isUrl(v) ? null : 'Must be a valid http/https URL'),
},
NEXT_PUBLIC_BASE_URL: {
description: 'Canonical site base URL for sitemap.xml, robots.txt and metadata',
required: false,
validate: (v) => (isUrl(v) ? null : 'Must be a valid http/https URL'),
},

// Stellar core
NEXT_PUBLIC_STELLAR_NETWORK: {
description: 'Stellar network (testnet | mainnet | futurenet)',
Expand Down Expand Up @@ -211,6 +257,39 @@ const RULES: Record<keyof EnvSchema, Rule> = {
NEXT_PUBLIC_SENTRY_DSN: { description: 'Sentry DSN', required: false },
NEXT_PUBLIC_GA_MEASUREMENT_ID: { description: 'Google Analytics measurement ID', required: false },

// Error tracking
NEXT_PUBLIC_ERROR_TRACKING_ENABLED: {
description: 'Enable centralized error tracking (true | false)',
required: false,
validate: (v) =>
!v || ['true', 'false'].includes(v) ? null : 'Must be "true" or "false"',
},
NEXT_PUBLIC_ENVIRONMENT: {
description: 'Environment label reported to the error tracker',
required: false,
},
NEXT_PUBLIC_APP_VERSION: {
description: 'App version reported to the error tracker',
required: false,
},
NEXT_PUBLIC_ERROR_SAMPLE_RATE: {
description: 'Fraction of sampled errors (0 to 1)',
required: false,
validate: (v) => {
const n = Number(v);
return !v || (Number.isFinite(n) && n >= 0 && n <= 1)
? null
: 'Must be a number between 0 and 1';
},
},

// Stellar explorer links
NEXT_PUBLIC_STELLAR_EXPLORER_URL: {
description: 'Stellar block explorer base URL for transaction links',
required: false,
validate: (v) => (isUrl(v) ? null : 'Must be a valid http/https URL'),
},

// Cloudinary
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: {
description: 'Cloudinary cloud name for image uploads',
Expand Down Expand Up @@ -244,6 +323,45 @@ const RULES: Record<keyof EnvSchema, Rule> = {
: 'Must be a valid Stellar secret key (56 chars, starts with S)',
},
REDIS_URL: { description: 'Redis connection URL', required: false },

// Cloudinary (server-side deletion)
CLOUDINARY_API_KEY: {
description: 'Cloudinary API key for server-side image deletion',
required: false,
},
CLOUDINARY_API_SECRET: {
description: 'Cloudinary API secret for server-side image deletion',
required: false,
},

// NextAuth / OAuth providers
NEXTAUTH_SECRET: {
description: 'NextAuth session secret (used alongside AUTH_SECRET)',
required: false,
},
GOOGLE_CLIENT_ID: { description: 'Google OAuth client ID', required: false },
GOOGLE_CLIENT_SECRET: { description: 'Google OAuth client secret', required: false },
GITHUB_CLIENT_ID: { description: 'GitHub OAuth client ID', required: false },
GITHUB_CLIENT_SECRET: { description: 'GitHub OAuth client secret', required: false },

// Legacy n18n scaffolding (components/n18n is unused; declared to close the contract)
APP_NAME: { description: 'Legacy: app name used by unused n18n scaffolding', required: false },
DB_TYPE: { description: 'Legacy: DB driver used by unused n18n scaffolding', required: false },
DB_HOST: { description: 'Legacy: DB host used by unused n18n scaffolding', required: false },
DB_PORT: {
description: 'Legacy: DB port used by unused n18n scaffolding',
required: false,
validate: (v) => (!v || /^\d+$/.test(v) ? null : 'Must be a numeric port'),
},
DB_USERNAME: { description: 'Legacy: DB username used by unused n18n scaffolding', required: false },
DB_PASSWORD: { description: 'Legacy: DB password used by unused n18n scaffolding', required: false },
DB_NAME: { description: 'Legacy: DB name used by unused n18n scaffolding', required: false },
DB_SYNCHRONIZE: {
description: 'Legacy: auto-sync schema flag used by unused n18n scaffolding',
required: false,
validate: (v) =>
!v || ['true', 'false'].includes(v) ? null : 'Must be "true" or "false"',
},
};

// ---------------------------------------------------------------------------
Expand Down
Loading