mirror of
https://github.com/Open-Dev-Society/OpenStock.git
synced 2026-05-06 21:50:16 +08:00
added donation
This commit is contained in:
17
README.md
17
README.md
@@ -242,13 +242,15 @@ BETTER_AUTH_SECRET=your_better_auth_secret
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Finnhub
|
||||
FINNHUB_API_KEY=your_finnhub_key
|
||||
# Optional client-exposed variant if needed by client code:
|
||||
NEXT_PUBLIC_FINNHUB_API_KEY=
|
||||
# Note: NEXT_PUBLIC_FINNHUB_API_KEY is required for Vercel deployment
|
||||
NEXT_PUBLIC_FINNHUB_API_KEY=your_finnhub_key
|
||||
FINNHUB_BASE_URL=https://finnhub.io/api/v1
|
||||
|
||||
# Inngest AI (Gemini)
|
||||
GEMINI_API_KEY=your_gemini_api_key
|
||||
# Inngest Signing Key (required for Vercel deployment)
|
||||
# Get this from your Inngest dashboard: https://app.inngest.com/env/settings/keys
|
||||
INNGEST_SIGNING_KEY=your_inngest_signing_key
|
||||
|
||||
# Email (Nodemailer via Gmail; consider App Passwords if 2FA)
|
||||
NODEMAILER_EMAIL=youraddress@gmail.com
|
||||
@@ -268,12 +270,15 @@ BETTER_AUTH_SECRET=your_better_auth_secret
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Finnhub
|
||||
FINNHUB_API_KEY=your_finnhub_key
|
||||
NEXT_PUBLIC_FINNHUB_API_KEY=
|
||||
# Note: NEXT_PUBLIC_FINNHUB_API_KEY is required for Vercel deployment
|
||||
NEXT_PUBLIC_FINNHUB_API_KEY=your_finnhub_key
|
||||
FINNHUB_BASE_URL=https://finnhub.io/api/v1
|
||||
|
||||
# Inngest AI (Gemini)
|
||||
GEMINI_API_KEY=your_gemini_api_key
|
||||
# Inngest Signing Key (required for Vercel deployment)
|
||||
# Get this from your Inngest dashboard: https://app.inngest.com/env/settings/keys
|
||||
INNGEST_SIGNING_KEY=your_inngest_signing_key
|
||||
|
||||
# Email (Nodemailer via Gmail; consider App Passwords if 2FA)
|
||||
NODEMAILER_EMAIL=youraddress@gmail.com
|
||||
@@ -329,7 +334,7 @@ public/assets/images/ # logos and screenshots
|
||||
|
||||
- Finnhub
|
||||
- Stock search, company profiles, and market news.
|
||||
- Set `FINNHUB_API_KEY` and `FINNHUB_BASE_URL` (default: https://finnhub.io/api/v1).
|
||||
- Set `NEXT_PUBLIC_FINNHUB_API_KEY` and `FINNHUB_BASE_URL` (default: https://finnhub.io/api/v1).
|
||||
- Free tiers may return delayed quotes; respect rate limits and terms.
|
||||
|
||||
- TradingView
|
||||
|
||||
@@ -3,6 +3,7 @@ import {auth} from "@/lib/better-auth/auth";
|
||||
import {headers} from "next/headers";
|
||||
import {redirect} from "next/navigation";
|
||||
import Footer from "@/components/Footer";
|
||||
import DonatePopup from "@/components/DonatePopup";
|
||||
|
||||
const Layout = async ({ children }: { children : React.ReactNode }) => {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
@@ -24,6 +25,7 @@ const Layout = async ({ children }: { children : React.ReactNode }) => {
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
<DonatePopup />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
106
components/DonatePopup.tsx
Normal file
106
components/DonatePopup.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Heart, Github } from 'lucide-react';
|
||||
|
||||
const DONATE_POPUP_KEY = 'opendevsociety-donate-popup-dismissed';
|
||||
const DONATE_POPUP_DELAY = 3000; // Show after 3 seconds
|
||||
const DONATE_POPUP_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
|
||||
const GITHUB_SPONSOR_URL = 'https://github.com/sponsors/ravixalgorithm';
|
||||
|
||||
export default function DonatePopup() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has dismissed popup
|
||||
const dismissed = localStorage.getItem(DONATE_POPUP_KEY);
|
||||
|
||||
if (dismissed) {
|
||||
const dismissedTime = parseInt(dismissed, 10);
|
||||
const now = Date.now();
|
||||
// Show again after cooldown period
|
||||
if (now - dismissedTime < DONATE_POPUP_COOLDOWN) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show popup after delay
|
||||
const timer = setTimeout(() => {
|
||||
setOpen(true);
|
||||
}, DONATE_POPUP_DELAY);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Listen for custom event from donate button
|
||||
useEffect(() => {
|
||||
const handleOpenPopup = () => setOpen(true);
|
||||
window.addEventListener('open-donate-popup', handleOpenPopup);
|
||||
return () => window.removeEventListener('open-donate-popup', handleOpenPopup);
|
||||
}, []);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setOpen(false);
|
||||
// Store dismissal time
|
||||
localStorage.setItem(DONATE_POPUP_KEY, Date.now().toString());
|
||||
};
|
||||
|
||||
const handleDonate = () => {
|
||||
window.open(GITHUB_SPONSOR_URL, '_blank', 'noopener,noreferrer');
|
||||
handleDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="!bg-gray-800 !border-teal-600/50 text-gray-100 max-w-md mx-4 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-teal-500/20 rounded-lg">
|
||||
<Heart className="h-6 w-6 text-teal-400 fill-teal-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-2xl font-bold text-gray-100">
|
||||
Keep OpenStock Free
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-gray-400 text-base leading-relaxed pt-2">
|
||||
Your overwhelming love for OpenStock and Open Dev Society has helped us grow,
|
||||
but we're hitting Vercel's free tier limits.
|
||||
<br /><br />
|
||||
Help us keep OpenStock free and accessible for everyone by supporting us on GitHub Sponsors.
|
||||
Every contribution, no matter how small, makes a difference! 💙
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 mt-6">
|
||||
<Button
|
||||
onClick={handleDonate}
|
||||
className="flex-1 bg-gradient-to-r from-teal-500 to-cyan-500 hover:from-teal-600 hover:to-cyan-600 text-white font-semibold h-11 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
<Github className="h-4 w-4 mr-2" />
|
||||
Sponsor on GitHub
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDismiss}
|
||||
variant="outline"
|
||||
className="flex-1 border-teal-600/50 text-teal-400 hover:bg-teal-600/10 hover:text-teal-300 h-11 transition-all duration-200"
|
||||
>
|
||||
Maybe Later
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center mt-4">
|
||||
This popup won't appear again for 24 hours after dismissing
|
||||
</p>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const Footer = () => {
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<Link href="/" className="flex items-center gap-2 mb-4">
|
||||
<Image
|
||||
src="https://i.ibb.co/r28VWPjS/Screenshot-2025-10-04-123317-Picsart-Ai-Image-Enhancer-removebg-preview.png"
|
||||
src="/assets/images/logo.png"
|
||||
alt="OpenStock"
|
||||
width={150}
|
||||
height={38}
|
||||
|
||||
@@ -12,7 +12,7 @@ const Header = async ({ user }: { user: User }) => {
|
||||
<div className="container header-wrapper">
|
||||
<Link href="/" className="flex items-center justify-center gap-2">
|
||||
<Image
|
||||
src="https://i.ibb.co/r28VWPjS/Screenshot-2025-10-04-123317-Picsart-Ai-Image-Enhancer-removebg-preview.png"
|
||||
src="/assets/images/logo.png"
|
||||
alt="OpenStock"
|
||||
width={200}
|
||||
height={50}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
'use client'
|
||||
|
||||
|
||||
import React from 'react'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import {NAV_ITEMS} from "@/lib/constants";
|
||||
import Link from "next/link";
|
||||
import {usePathname} from "next/navigation";
|
||||
import SearchCommand from "@/components/SearchCommand";
|
||||
import { Heart } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// Create context for popup state
|
||||
const DonatePopupContext = createContext<{
|
||||
openDonatePopup: () => void;
|
||||
}>({
|
||||
openDonatePopup: () => {}
|
||||
});
|
||||
|
||||
export const useDonatePopup = () => useContext(DonatePopupContext);
|
||||
|
||||
const NavItems = ({initialStocks}: { initialStocks: StockWithWatchlistStatus[]}) => {
|
||||
const pathname = usePathname()
|
||||
@@ -15,8 +26,15 @@ const NavItems = ({initialStocks}: { initialStocks: StockWithWatchlistStatus[]})
|
||||
|
||||
return pathname.startsWith(path);
|
||||
}
|
||||
|
||||
const openDonatePopup = () => {
|
||||
// Trigger the popup by dispatching a custom event
|
||||
window.dispatchEvent(new CustomEvent('open-donate-popup'));
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="flex flex-col sm:flex-row p-2 gap-3 sm:gap-10 font-medium">
|
||||
<DonatePopupContext.Provider value={{ openDonatePopup }}>
|
||||
<ul className="flex flex-col sm:flex-row p-2 gap-3 sm:gap-10 font-medium">
|
||||
{NAV_ITEMS.map(({href, label}) => {
|
||||
if (href === '/search') return (
|
||||
<li key="search-trigger">
|
||||
@@ -33,7 +51,18 @@ const NavItems = ({initialStocks}: { initialStocks: StockWithWatchlistStatus[]})
|
||||
</Link>
|
||||
</li>
|
||||
})}
|
||||
<li key="donate">
|
||||
<Button
|
||||
onClick={openDonatePopup}
|
||||
className="bg-gradient-to-r from-teal-500 to-cyan-500 hover:from-teal-600 hover:to-cyan-600 text-white font-semibold px-4 py-2 rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105 flex items-center gap-2 animate-pulse"
|
||||
size="sm"
|
||||
>
|
||||
<Heart className="h-4 w-4 fill-current" />
|
||||
Donate
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</DonatePopupContext.Provider>
|
||||
)
|
||||
}
|
||||
export default NavItems
|
||||
|
||||
@@ -9,10 +9,17 @@ export const signUpWithEmail = async ({ email, password, fullName, country, inve
|
||||
const response = await auth.api.signUpEmail({ body: { email, password, name: fullName } })
|
||||
|
||||
if(response) {
|
||||
await inngest.send({
|
||||
name: 'app/user.created',
|
||||
data: { email, name: fullName, country, investmentGoals, riskTolerance, preferredIndustry }
|
||||
})
|
||||
try {
|
||||
console.log('📤 Sending Inngest event: app/user.created for', email);
|
||||
await inngest.send({
|
||||
name: 'app/user.created',
|
||||
data: { email, name: fullName, country, investmentGoals, riskTolerance, preferredIndustry }
|
||||
});
|
||||
console.log('✅ Inngest event sent successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to send Inngest event:', error);
|
||||
// Don't fail signup if email fails
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: response }
|
||||
|
||||
@@ -25,7 +25,7 @@ export { fetchJSON };
|
||||
export async function getNews(symbols?: string[]): Promise<MarketNewsArticle[]> {
|
||||
try {
|
||||
const range = getDateRange(5);
|
||||
const token = process.env.FINNHUB_API_KEY ?? NEXT_PUBLIC_FINNHUB_API_KEY;
|
||||
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
|
||||
if (!token) {
|
||||
throw new Error('FINNHUB API key is not configured');
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export async function getNews(symbols?: string[]): Promise<MarketNewsArticle[]>
|
||||
|
||||
export const searchStocks = cache(async (query?: string): Promise<StockWithWatchlistStatus[]> => {
|
||||
try {
|
||||
const token = process.env.FINNHUB_API_KEY ?? NEXT_PUBLIC_FINNHUB_API_KEY;
|
||||
const token = NEXT_PUBLIC_FINNHUB_API_KEY;
|
||||
if (!token) {
|
||||
// If no token, log and return empty to avoid throwing per requirements
|
||||
console.error('Error in stock search:', new Error('FINNHUB API key is not configured'));
|
||||
|
||||
@@ -2,5 +2,7 @@ import {Inngest} from "inngest"
|
||||
|
||||
export const inngest = new Inngest({
|
||||
id: "openStock",
|
||||
ai: {gemini: {apiKey: process.env.GEMINI_API_KEY}}
|
||||
ai: {gemini: {apiKey: process.env.GEMINI_API_KEY}},
|
||||
// Add signing key for Vercel deployment
|
||||
signingKey: process.env.INNGEST_SIGNING_KEY,
|
||||
})
|
||||
@@ -33,12 +33,20 @@ export const sendSignUpEmail = inngest.createFunction(
|
||||
})
|
||||
|
||||
await step.run('send-welcome-email', async () => {
|
||||
const part = response.candidates?.[0]?.content?.parts?.[0];
|
||||
const introText = (part && 'text' in part ? part.text : null) ||'Thanks for joining Openstock. You now have the tools to track markets and make smarter moves.'
|
||||
try {
|
||||
const part = response.candidates?.[0]?.content?.parts?.[0];
|
||||
const introText = (part && 'text' in part ? part.text : null) ||'Thanks for joining Openstock. You now have the tools to track markets and make smarter moves.'
|
||||
|
||||
const { data: { email, name } } = event;
|
||||
const { data: { email, name } } = event;
|
||||
|
||||
return await sendWelcomeEmail({ email, name, intro: introText });
|
||||
console.log(`📧 Attempting to send welcome email to: ${email}`);
|
||||
const result = await sendWelcomeEmail({ email, name, intro: introText });
|
||||
console.log(`✅ Welcome email sent successfully to: ${email}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ Error sending welcome email:', error);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -106,13 +114,28 @@ export const sendDailyNewsSummary = inngest.createFunction(
|
||||
|
||||
// Step #4: (placeholder) Send the emails
|
||||
await step.run('send-news-emails', async () => {
|
||||
await Promise.all(
|
||||
const results = await Promise.allSettled(
|
||||
userNewsSummaries.map(async ({ user, newsContent}) => {
|
||||
if(!newsContent) return false;
|
||||
if(!newsContent) {
|
||||
console.log(`⏭️ Skipping email for ${user.email} - no news content`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return await sendNewsSummaryEmail({ email: user.email, date: getFormattedTodayDate(), newsContent })
|
||||
try {
|
||||
console.log(`📧 Attempting to send news summary email to: ${user.email}`);
|
||||
const result = await sendNewsSummaryEmail({ email: user.email, date: getFormattedTodayDate(), newsContent });
|
||||
console.log(`✅ News summary email sent successfully to: ${user.email}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to send news summary email to ${user.email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
console.log(`📊 Email sending summary: ${successful} successful, ${failed} failed`);
|
||||
})
|
||||
|
||||
return { success: true, message: 'Daily news summary emails sent successfully' }
|
||||
|
||||
@@ -1,44 +1,84 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import {WELCOME_EMAIL_TEMPLATE, NEWS_SUMMARY_EMAIL_TEMPLATE} from "@/lib/nodemailer/templates";
|
||||
|
||||
// Verify transporter configuration
|
||||
if (!process.env.NODEMAILER_EMAIL || !process.env.NODEMAILER_PASSWORD) {
|
||||
console.warn('⚠️ NODEMAILER_EMAIL or NODEMAILER_PASSWORD is not set. Email functionality will not work.');
|
||||
}
|
||||
|
||||
export const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: process.env.NODEMAILER_EMAIL!,
|
||||
pass: process.env.NODEMAILER_PASSWORD!,
|
||||
}
|
||||
},
|
||||
// Add connection timeout and retry options
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
maxMessages: 3,
|
||||
})
|
||||
|
||||
export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) => {
|
||||
const htmlTemplate = WELCOME_EMAIL_TEMPLATE
|
||||
.replace('{{name}}', name)
|
||||
.replace('{{intro}}', intro);
|
||||
|
||||
const mailOptions = {
|
||||
from: `"Openstock" <opendevsociety@gmail.com>`,
|
||||
to: email,
|
||||
subject: `Welcome to Openstock - your open-source stock market toolkit!`,
|
||||
text: 'Thanks for joining Openstock, an initiative by open dev society',
|
||||
html: htmlTemplate,
|
||||
// Verify connection on startup
|
||||
transporter.verify((error, success) => {
|
||||
if (error) {
|
||||
console.error('❌ Nodemailer transporter verification failed:', error);
|
||||
} else {
|
||||
console.log('✅ Nodemailer transporter is ready to send emails');
|
||||
}
|
||||
});
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) => {
|
||||
try {
|
||||
if (!process.env.NODEMAILER_EMAIL || !process.env.NODEMAILER_PASSWORD) {
|
||||
throw new Error('Email credentials not configured');
|
||||
}
|
||||
|
||||
const htmlTemplate = WELCOME_EMAIL_TEMPLATE
|
||||
.replace('{{name}}', name)
|
||||
.replace('{{intro}}', intro);
|
||||
|
||||
const mailOptions = {
|
||||
from: `"Openstock" <${process.env.NODEMAILER_EMAIL}>`,
|
||||
to: email,
|
||||
subject: `Welcome to Openstock - your open-source stock market toolkit!`,
|
||||
text: 'Thanks for joining Openstock, an initiative by open dev society',
|
||||
html: htmlTemplate,
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log('✅ Welcome email sent successfully:', info.messageId);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to send welcome email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const sendNewsSummaryEmail = async (
|
||||
{ email, date, newsContent }: { email: string; date: string; newsContent: string }
|
||||
): Promise<void> => {
|
||||
const htmlTemplate = NEWS_SUMMARY_EMAIL_TEMPLATE
|
||||
.replace('{{date}}', date)
|
||||
.replace('{{newsContent}}', newsContent);
|
||||
) => {
|
||||
try {
|
||||
if (!process.env.NODEMAILER_EMAIL || !process.env.NODEMAILER_PASSWORD) {
|
||||
throw new Error('Email credentials not configured');
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
from: `"Openstock" <opendevsociety@gmail.com>`,
|
||||
to: email,
|
||||
subject: `📈 Market News Summary Today - ${date}`,
|
||||
text: `Today's market news summary from Openstock`,
|
||||
html: htmlTemplate,
|
||||
};
|
||||
const htmlTemplate = NEWS_SUMMARY_EMAIL_TEMPLATE
|
||||
.replace('{{date}}', date)
|
||||
.replace('{{newsContent}}', newsContent);
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
const mailOptions = {
|
||||
from: `"Openstock" <${process.env.NODEMAILER_EMAIL}>`,
|
||||
to: email,
|
||||
subject: `📈 Market News Summary Today - ${date}`,
|
||||
text: `Today's market news summary from Openstock`,
|
||||
html: htmlTemplate,
|
||||
};
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log('✅ News summary email sent successfully:', info.messageId);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to send news summary email:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
1345
package-lock.json
generated
1345
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"country-data-list": "^1.5.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"inngest": "^3.44.0",
|
||||
"inngest": "^3.47.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"mongodb": "^6.20.0",
|
||||
"mongoose": "^8.19.0",
|
||||
|
||||
112
scripts/check-env.mjs
Normal file
112
scripts/check-env.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Environment Variables Checker
|
||||
* Run: node scripts/check-env.mjs
|
||||
*/
|
||||
|
||||
const requiredVars = {
|
||||
// Core
|
||||
'NODE_ENV': 'development or production',
|
||||
|
||||
// Database
|
||||
'MONGODB_URI': 'MongoDB connection string',
|
||||
|
||||
// Better Auth
|
||||
'BETTER_AUTH_SECRET': 'Secret key for Better Auth',
|
||||
'BETTER_AUTH_URL': 'Auth URL (e.g., http://localhost:3000)',
|
||||
|
||||
// Finnhub
|
||||
'NEXT_PUBLIC_FINNHUB_API_KEY': 'Finnhub API key (public)',
|
||||
'FINNHUB_BASE_URL': 'Finnhub API base URL',
|
||||
|
||||
// Inngest
|
||||
'GEMINI_API_KEY': 'Google Gemini API key',
|
||||
'INNGEST_SIGNING_KEY': 'Inngest signing key (for Vercel)',
|
||||
|
||||
// Email
|
||||
'NODEMAILER_EMAIL': 'Gmail address for sending emails',
|
||||
'NODEMAILER_PASSWORD': 'Gmail app password (not regular password)',
|
||||
};
|
||||
|
||||
const optionalVars = {
|
||||
'FINNHUB_API_KEY': 'Legacy Finnhub key (deprecated, use NEXT_PUBLIC_FINNHUB_API_KEY)',
|
||||
};
|
||||
|
||||
console.log('🔍 Checking Environment Variables...\n');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
let missing = [];
|
||||
let present = [];
|
||||
let warnings = [];
|
||||
|
||||
// Check required variables
|
||||
for (const [key, description] of Object.entries(requiredVars)) {
|
||||
const value = process.env[key];
|
||||
if (!value || value.trim() === '') {
|
||||
missing.push({ key, description });
|
||||
} else {
|
||||
present.push({ key, description, value: maskValue(value) });
|
||||
}
|
||||
}
|
||||
|
||||
// Check optional variables
|
||||
for (const [key, description] of Object.entries(optionalVars)) {
|
||||
const value = process.env[key];
|
||||
if (value) {
|
||||
warnings.push({ key, description, message: 'This variable is deprecated or optional' });
|
||||
}
|
||||
}
|
||||
|
||||
// Display results
|
||||
console.log('\n✅ Present Variables:');
|
||||
console.log('-'.repeat(60));
|
||||
if (present.length === 0) {
|
||||
console.log(' None found');
|
||||
} else {
|
||||
present.forEach(({ key, description, value }) => {
|
||||
console.log(` ✓ ${key}`);
|
||||
console.log(` ${description}`);
|
||||
console.log(` Value: ${value}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.log('\n❌ Missing Variables:');
|
||||
console.log('-'.repeat(60));
|
||||
missing.forEach(({ key, description }) => {
|
||||
console.log(` ✗ ${key}`);
|
||||
console.log(` ${description}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log('\n⚠️ Warnings:');
|
||||
console.log('-'.repeat(60));
|
||||
warnings.forEach(({ key, message }) => {
|
||||
console.log(` ⚠ ${key}: ${message}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log(`Summary: ${present.length}/${Object.keys(requiredVars).length} required variables present`);
|
||||
if (missing.length > 0) {
|
||||
console.log(`\n⚠️ Missing ${missing.length} required variable(s).`);
|
||||
console.log('\nTo fix:');
|
||||
console.log('1. Create a .env file in the project root');
|
||||
console.log('2. Add the missing variables');
|
||||
console.log('3. For Vercel: Add these in Project Settings > Environment Variables');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n✅ All required environment variables are set!');
|
||||
}
|
||||
|
||||
// Helper function to mask sensitive values
|
||||
function maskValue(value) {
|
||||
if (value.length <= 8) {
|
||||
return '***';
|
||||
}
|
||||
return value.substring(0, 4) + '***' + value.substring(value.length - 4);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user