-
Notifications
You must be signed in to change notification settings - Fork 0
Create PI² app skeleton with landing page and APIs #139
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
base: codex/launch-live-love-feature
Are you sure you want to change the base?
Create PI² app skeleton with landing page and APIs #139
Conversation
Added detailed README for YONI and Mutterschiff project, including architecture, quickstart guide, security baseline, and next steps.
Implement GitHub OAuth callback handling to exchange code for access token.
Implement GitHub webhook handler for pull requests.
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
… JS/JSX files Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
…webhook handlers Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
…alidation Harden GitHub OAuth state handling
- Fix redirectWithStateReset to use absolute URLs - Use crypto.timingSafeEqual() for constant-time state comparison - Extract STATE_COOKIE_NAME to shared constants file Co-authored-by: pappensex <233804448+pappensex@users.noreply.github.com>
…r-blueprint-integration
…yment-process-470m5w
…yment-process-uxk6il
|
Deployment failed with the following error: Learn More: https://vercel.com/docs/concepts/projects/project-configuration |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request represents a substantial rebuild that transforms the YONI mental health support application into PI² (PI squared), a clarity and productivity system. The changes replace the previous specialized therapeutic focus with a modular productivity platform featuring task management, automation flows, finance tracking, space organization, and energy management.
Key Changes:
- Complete UI redesign with new landing page, navigation structure, and footer featuring clean, minimalist design
- Implementation of five core modules (CORE, FLOW, MONEY, SPACE, ENERGY) with API route stubs
- Addition of GDPR-compliant legal documentation (impressum, privacy policy, terms of service, cookie policy, security policy)
- Stripe integration skeleton for subscription checkout and webhook handling
- Removal of OpenAI chat functionality, GitHub OAuth, and blueprint/ritual management features
Reviewed changes
Copilot reviewed 43 out of 48 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
tailwind.config.js |
Updated color scheme from amethyst/purple theme to primary/gold/rose palette with new font families |
lib/utils.ts |
Added className utility function for conditional styling |
lib/types.ts |
Defined ModuleKey type and ModuleCard interface for the five core modules |
lib/stripe.ts |
Created Stripe client singleton with conditional initialization |
lib/db.ts |
Added database placeholder for future implementation |
components/ui/button.tsx |
Implemented reusable Button component with primary/secondary variants and optional Link rendering |
components/ui/card.tsx |
Created Card wrapper component for consistent card styling |
components/layout/CookieBanner.tsx |
Added GDPR cookie consent banner with localStorage persistence |
app/page.tsx |
Completely rebuilt landing page with hero section, module overview, and "First 100" call-to-action |
app/layout.tsx |
Redesigned root layout with new header navigation, footer with legal links, and font configuration |
app/globals.css |
Replaced extensive custom styling with Tailwind utility classes and simplified design tokens |
app/dashboard/page.tsx |
New dashboard page showing module overview with placeholder content |
app/auth/page.tsx |
Added authentication page placeholder for future auth integration |
app/legal/*/page.tsx |
Created five legal document pages (impressum, datenschutz, agb, cookies, security) rendering markdown content |
legal/*.md |
Added German legal documentation files with GDPR-compliant content |
app/api/checkout/route.ts |
Simplified Stripe checkout to handle subscription mode with priceId parameter |
app/api/stripe/webhook/route.ts |
Streamlined webhook handler for checkout.session.completed events |
app/api/*/route.ts |
Added stub API routes for tasks, flows, finance, energy, and space modules |
README.md |
Updated documentation to reflect PI² branding and simplified tech stack |
CHANGELOG.md |
Initialized changelog with v0.9.0 entry |
.env.example |
Updated environment variables for Stripe and database configuration |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!process.env.STRIPE_SECRET_KEY) { | ||
| console.warn("STRIPE_SECRET_KEY is not set."); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The console.warn for missing STRIPE_SECRET_KEY will execute on every module import. This could lead to console spam, especially in development environments with hot reloading. Consider either:
- Moving this warning to where stripe is actually used (e.g., in the API routes)
- Adding a guard to only warn once
- Using a proper logging library with log levels
Example fix:
let warnedAboutStripe = false;
if (!process.env.STRIPE_SECRET_KEY && !warnedAboutStripe) {
console.warn("STRIPE_SECRET_KEY is not set.");
warnedAboutStripe = true;
}
app/api/checkout/route.ts
Outdated
| export async function POST(req: NextRequest) { | ||
| try { | ||
| const body = await req.json(); | ||
| if (!stripe) { | ||
| return NextResponse.json({ error: "Stripe not configured" }, { status: 500 }); | ||
| } | ||
|
|
||
| const session = await stripe.checkout.sessions.create({ | ||
| mode: "payment", | ||
| const { priceId } = await req.json(); | ||
|
|
||
| line_items: [ | ||
| { | ||
| price_data: { | ||
| currency: "eur", | ||
| product_data: { | ||
| name: "YONI Support Access", | ||
| }, | ||
| unit_amount: 1111, // €11,11 | ||
| }, | ||
| quantity: 1, | ||
| }, | ||
| ], | ||
| if (!priceId) { | ||
| return NextResponse.json({ error: "Missing priceId" }, { status: 400 }); | ||
| } | ||
|
|
||
| success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`, | ||
| cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cancel`, | ||
| metadata: body?.metadata || {}, | ||
| }); | ||
| const session = await stripe.checkout.sessions.create({ | ||
| mode: "subscription", | ||
| line_items: [{ price: priceId, quantity: 1 }], | ||
| success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?checkout=success`, | ||
| cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/?checkout=cancel`, | ||
| }); | ||
|
|
||
| return NextResponse.json({ url: session.url }); | ||
| } catch (err: any) { | ||
| console.error("❌ Error creating checkout session:", err); | ||
| return new NextResponse(`Error: ${err.message}`, { status: 400 }); | ||
| } | ||
| return NextResponse.json({ url: session.url }); |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for the Stripe checkout session creation. If stripe.checkout.sessions.create() throws an error (network issues, invalid price ID, API errors), the API will return a 500 error with an unhandled exception rather than a useful error message.
Add try-catch error handling:
export async function POST(req: NextRequest) {
if (!stripe) {
return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
}
try {
const { priceId } = await req.json();
if (!priceId) {
return NextResponse.json({ error: "Missing priceId" }, { status: 400 });
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?checkout=success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/?checkout=cancel`,
});
return NextResponse.json({ url: session.url });
} catch (error) {
console.error("Checkout session creation failed:", error);
return NextResponse.json(
{ error: "Failed to create checkout session" },
{ status: 500 }
);
}
}| <p className="text-gray-600">Sichere dir Zugang zu PI². Auth wird später angebunden.</p> | ||
| </div> | ||
| <div className="space-y-4 text-center"> | ||
| <Button className="w-full justify-center">Login mit E-Mail (Platzhalter)</Button> |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The placeholder text mentions "Auth wird später angebunden" (Auth will be connected later), but the button text says "Login mit E-Mail (Platzhalter)" which translates to "Login with Email (Placeholder)".
For consistency and clarity, either use German throughout or English throughout. The codebase uses English for code/comments but German for user-facing text. However, "Platzhalter" in the button text might confuse users - consider removing it or making it less prominent:
- "Login mit E-Mail" (without Platzhalter)
- Or move the placeholder note to the description only
| <Button className="w-full justify-center">Login mit E-Mail (Platzhalter)</Button> | |
| <Button className="w-full justify-center">Login mit E-Mail</Button> |
| Geschäftskonto: | ||
| N26 Business – IBAN: DE18100110012091876561 |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The IBAN (DE18100110012091876561) is exposed in the impressum. While IBANs are not as sensitive as credit card numbers and are designed to be shared for receiving payments, publicly displaying a bank account number on a public repository could lead to:
- Unwanted/spam transactions
- Potential security concerns
- Privacy issues
Consider:
- Removing the specific IBAN and replacing it with a placeholder or generic contact instruction
- Or moving this to an environment variable/config that's not committed to the repository
- Or simply removing line 12-13 if bank details aren't required in the impressum
| export function cn(...classes: Array<string | false | null | undefined>) { | ||
| return classes.filter(Boolean).join(" "); | ||
| } |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The cn utility function doesn't handle the case where classes might include falsy values other than false, null, or undefined. The TypeScript type annotation suggests it should handle these cases, but the implementation could be more robust.
Consider using a more battle-tested solution like clsx or classnames library, or improve the implementation to handle edge cases:
export function cn(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(" ").trim();
}Or better yet, use the popular clsx library which is well-tested and handles many edge cases.
| <div className="container-wrapper py-12"> | ||
| <Card className="prose max-w-3xl whitespace-pre-line p-8 text-gray-800"> | ||
| <h1 className="mb-4 text-3xl font-serif text-text">Security</h1> | ||
| <div className="space-y-2 text-sm leading-relaxed" dangerouslySetInnerHTML={{ __html: content.replace(/\n/g, "<br />") }} /> |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The legal page components use dangerouslySetInnerHTML which creates a potential XSS (Cross-Site Scripting) vulnerability. While the content is read from local markdown files at build time, this pattern is risky and unnecessary.
Instead of using dangerouslySetInnerHTML with simple replace(/\n/g, "<br />"), you should either:
- Use a proper markdown parser like
react-markdownormarked - Simply preserve whitespace with CSS:
className="whitespace-pre-wrap"(which is already applied)
Since whitespace-pre-wrap is already in the className, the dangerouslySetInnerHTML and .replace(/\n/g, "<br />") are redundant. You can directly render the content as text.
| <div className="space-y-2 text-sm leading-relaxed" dangerouslySetInnerHTML={{ __html: content.replace(/\n/g, "<br />") }} /> | |
| <div className="space-y-2 text-sm leading-relaxed">{content}</div> |
| {moduleCards.map((module) => ( | ||
| <Card key={module.title} className="p-6"> | ||
| <p className="text-xs uppercase tracking-[0.2em] text-gray-500">{module.title}</p> | ||
| <h3 className="mt-3 text-xl font-semibold text-text">{module.title}</h3> |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The duplicate module title is displayed twice - once in the uppercase label and once in the h3. Line 73 shows {module.title} in the h3, but the title is already displayed on line 72. This appears to be a copy-paste error.
The h3 should likely display a more descriptive heading or be removed, since the module title is already shown as the label. Looking at the moduleCards data structure (lines 4-25), each module only has title and description fields, so displaying title twice is redundant.
| <h3 className="mt-3 text-xl font-semibold text-text">{module.title}</h3> |
| <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> | ||
| {modules.map((module) => ( | ||
| <Card key={module.title} className="p-6"> | ||
| <p className="text-xs uppercase tracking-[0.2em] text-gray-500">{module.title}</p> |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar issue as on the landing page - the module title is displayed twice: once as the uppercase label (line 27) and again as the h3 heading (line 28). This is redundant.
Consider either:
- Removing the duplicate on line 28 if the label is sufficient
- Using a different field for the h3 (like a longer descriptive name)
- Removing line 27 if the h3 provides enough context
| <p className="text-xs uppercase tracking-[0.2em] text-gray-500">{module.title}</p> |
| success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?checkout=success`, | ||
| cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/?checkout=cancel`, |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checkout API doesn't validate that NEXT_PUBLIC_APP_URL is set before using it in the success/cancel URLs. If this environment variable is missing, the URLs will be malformed (e.g., undefined/dashboard?checkout=success), causing the Stripe session creation to fail.
Add validation:
if (!process.env.NEXT_PUBLIC_APP_URL) {
return NextResponse.json({ error: "App URL not configured" }, { status: 500 });
}|
|
||
| PI² / pihoch2 | ||
| Inhaberin: Julia Rappl | ||
| [Anschrift einfügen] |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The impressum file contains a placeholder [Anschrift einfügen] (Insert address) on line 6. This is sensitive information that should be filled in before deployment to production, as an impressum is legally required for German websites and must contain a complete address.
Consider adding a build-time check or validation to ensure this placeholder is replaced before production deployment.
pappensex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{
"name": "yoni-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"scan:secrets": "node scripts/scan-secrets.js",
"check:env": "node scripts/check-env.js",
"ci:security": "npm run scan:secrets && npm run check:env && npm run lint",
"ci:full": "npm run scan:secrets && npm run check:env && npm run lint && npm run build"
},
"dependencies": {
"next": "14.2.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"@auth/prisma-adapter": "^1.5.0",
"@prisma/client": "^5.16.2",
"bcrypt": "^5.1.1",
"stripe": "^16.0.0",
"@stripe/stripe-js": "^2.5.0",
"zod": "^3.23.8",
"clsx": "^2.0.0"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-config-next": "14.2.4",
"prettier": "^3.2.5"
}
}
pappensex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{
"name": "yoni-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"scan:secrets": "node scripts/scan-secrets.js",
"check:env": "node scripts/check-env.js",
"ci:security": "npm run scan:secrets && npm run check:env && npm run lint",
"ci:full": "npm run scan:secrets && npm run check:env && npm run lint && npm run build"
},
"dependencies": {
"next": "14.2.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"@auth/prisma-adapter": "^1.5.0",
"@prisma/client": "^5.16.2",
"bcrypt": "^5.1.1",
"stripe": "^16.0.0",
"@stripe/stripe-js": "^2.5.0",
"zod": "^3.23.8",
"clsx": "^2.0.0"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-config-next": "14.2.4",
"prettier": "^3.2.5"
}
}
name: Security & Quality CI
on:
push:
branches:
- main
- develop
- feature/**
pull_request:
branches:
- main
- develop
jobs:
security-ci:
name: Security & Quality Checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses:actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: |
npm ci || npm install
- name: Run secret leak scan
run: node scripts/scan-secrets.js
- name: Check env schema
run: node scripts/check-env.js
- name: Run lint
run: |
if npm run | grep -q "lint"; then
npm run lint
else
echo "No lint script defined, skipping."
fi
- name: Run tests
run: |
if npm run | grep -q "test"; then
npm test
else
echo "No test script defined, skipping."
fi
// scripts/scan-secrets.js
// Fails CI if likely secrets appear in tracked files.
const { execSync } = require("node:child_process");
const fs = require("node:fs");
const SUSPICIOUS_PATTERNS = [
/otpauth://totp/gi,
/\b[A-Z2-7]{24,}\b/g,
/\bsk_(live|test)[0-9a-zA-Z]{16,}\b/g,
/\bpk(live|test)[0-9a-zA-Z]{16,}\b/g,
/\bghp[0-9a-zA-Z]{24,}\b/g,
/\bvercel_[0-9a-zA-Z]{24,}\b/g,
/\beyJ[A-Za-z0-9-]+.[A-Za-z0-9-]+.[A-Za-z0-9-]+\b/g,
/\bAIza[0-9A-Za-z-]{20,}\b/g,
/\bAKIA[0-9A-Z]{16}\b/g
];
const IGNORED_PATHS = ["node_modules/", ".next/", "out/", "build/"];
function getTrackedFiles() {
const out = execSync("git ls-files", { encoding: "utf-8" });
return out
.split("\n")
.filter(
(f) => f.trim() !== "" && !IGNORED_PATHS.some((p) => f.startsWith(p))
);
}
function scanFile(path) {
const content = fs.readFileSync(path, "utf-8");
const matches = [];
for (const pattern of SUSPICIOUS_PATTERNS) {
let match;
const regex = new RegExp(pattern);
while ((match = regex.exec(content)) !== null) {
matches.push({
pattern: pattern.toString(),
snippet: match[0],
index: match.index
});
}
}
return matches;
}
function main() {
const files = getTrackedFiles();
const problems = [];
for (const file of files) {
const matches = scanFile(file);
if (matches.length > 0) problems.push({ file, matches });
}
if (problems.length > 0) {
console.error("❌ SECRET LEAK DETECTED\n");
for (const p of problems) {
console.error(File: ${p.file});
p.matches.forEach((m) =>
console.error(
Pattern ${m.pattern} | Snippet: "${m.snippet.slice( 0, 40 )}" at index ${m.index}
)
);
console.error("");
}
process.exit(1);
}
console.log("✅ No suspicious secrets detected.");
}
main();
// scripts/check-env.js
// Validates .env.example against required keys and basic sanity.
const fs = require("node:fs");
const path = require("node:path");
const REQUIRED_ENV_VARS = [
"NEXT_PUBLIC_APP_URL",
"REVALIDATE_SECRET",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"OPENAI_API_KEY",
"DATABASE_URL"
];
function parseEnv(file) {
if (!fs.existsSync(file)) return {};
const lines = fs.readFileSync(file, "utf8").split("\n");
const env = {};
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const i = trimmed.indexOf("=");
if (i === -1) continue;
env[trimmed.slice(0, i).trim()] = trimmed.slice(i + 1).trim();
}
return env;
}
function main() {
const file = path.join(process.cwd(), ".env.example");
if (!fs.existsSync(file)) {
console.warn("
process.exit(0);
}
const env = parseEnv(file);
const missing = REQUIRED_ENV_VARS.filter((k) => !(k in env));
if (missing.length > 0) {
console.error("❌ Missing required keys in .env.example:\n");
missing.forEach((m) => console.error(" - " + m));
process.exit(1);
}
const suspicious = Object.entries(env).filter(([, v]) =>
/sk(live|test)|ghp_|vercel_|eyJ/.test(v)
);
if (suspicious.length > 0) {
console.error("❌ .env.example contains real-looking secrets:\n");
suspicious.forEach(([k, v]) => console.error( - ${k} = ${v}));
process.exit(1);
}
console.log("✅ .env.example is valid.");
}
main();
git add .
git commit -m "Full overwrite to new Security & CI standard"
git push
Summary
Testing
Codex Task