Slefhostet und postgres
This commit is contained in:
18
greenlns-landing/Caddyfile
Normal file
18
greenlns-landing/Caddyfile
Normal file
@@ -0,0 +1,18 @@
|
||||
{$SITE_DOMAIN} {
|
||||
encode zstd gzip
|
||||
|
||||
@storage path /storage /storage/*
|
||||
handle @storage {
|
||||
uri strip_prefix /storage
|
||||
reverse_proxy minio:9000
|
||||
}
|
||||
|
||||
@api path /api /api/* /auth /auth/* /v1 /v1/* /health /plants /plants/*
|
||||
handle @api {
|
||||
reverse_proxy api:3000
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy landing:3000
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,41 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
# GreenLens Landing
|
||||
|
||||
Self-hosted Next.js landing page for GreenLens. The production stack in this directory runs:
|
||||
|
||||
- `caddy` for TLS and reverse proxy
|
||||
- `landing` for the Next.js standalone app
|
||||
- `api` for the Express backend from `../server`
|
||||
- `postgres` for persistent app data
|
||||
- `minio` for object storage under `/storage/*`
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Production stack
|
||||
|
||||
From `greenlns-landing/docker-compose.yml`:
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Required environment variables:
|
||||
|
||||
- `SITE_DOMAIN`
|
||||
- `SITE_URL`
|
||||
- `POSTGRES_PASSWORD`
|
||||
- `JWT_SECRET`
|
||||
- `MINIO_SECRET_KEY`
|
||||
|
||||
Optional service secrets:
|
||||
|
||||
- `OPENAI_API_KEY`
|
||||
- `STRIPE_SECRET_KEY`
|
||||
- `STRIPE_PUBLISHABLE_KEY`
|
||||
- `STRIPE_WEBHOOK_SECRET`
|
||||
- `REVENUECAT_WEBHOOK_SECRET`
|
||||
- `PLANT_IMPORT_ADMIN_KEY`
|
||||
|
||||
13
greenlns-landing/app/robots.ts
Normal file
13
greenlns-landing/app/robots.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://greenlenspro.com').trim()
|
||||
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
},
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = 'https://greenlns-landing.vercel.app'
|
||||
|
||||
return [
|
||||
{
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://greenlenspro.com').trim()
|
||||
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
@@ -16,11 +16,17 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/privacy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
]
|
||||
}
|
||||
{
|
||||
url: `${baseUrl}/privacy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/terms`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,60 +6,24 @@ import { siteConfig } from '@/lib/site'
|
||||
const CONTENT = {
|
||||
de: {
|
||||
title: 'Nutzungsbedingungen',
|
||||
intro:
|
||||
'Durch die Nutzung von GreenLens stimmst du diesen Nutzungsbedingungen zu. Bitte lies sie sorgfaeltig durch.',
|
||||
section1: '1. Leistungen',
|
||||
text1:
|
||||
'GreenLens bietet KI-gestuetzte Pflanzenidentifikation, Gesundheitsdiagnosen und Pflegeerinnerungen. Die App kann kostenlos genutzt werden; erweiterte Funktionen erfordern ein Abonnement oder Credits.',
|
||||
section2: '2. Abonnements und In-App-Kaeufe',
|
||||
text2:
|
||||
'GreenLens Pro ist ein automatisch erneuerbares Abonnement (monatlich oder jaehrlich). Es wird ueber deinen Apple-Account abgerechnet. Die Verlaengerung erfolgt automatisch, sofern du nicht mindestens 24 Stunden vor Ablauf des Abrechnungszeitraums kuendigst. Credits sind einmalige Kaeufe und nicht uebertragbar.',
|
||||
section3: '3. Kuendigung',
|
||||
text3:
|
||||
'Du kannst dein Abonnement jederzeit in den iPhone-Einstellungen unter deinem Apple-ID-Konto kuendigen. Nach der Kuendigung behast du den Zugriff bis zum Ende des bezahlten Zeitraums.',
|
||||
section4: '4. Haftungsausschluss',
|
||||
text4:
|
||||
'GreenLens stellt Informationen auf Basis von KI-Analysen bereit. Diese ersetzen keine professionelle Beratung. Wir uebernehmen keine Haftung fuer Schaeden, die durch die Nutzung der App entstehen.',
|
||||
section5: '5. Kontakt',
|
||||
text5: 'Bei Fragen zu diesen Nutzungsbedingungen erreichst du uns per E-Mail.',
|
||||
intro: 'Diese Bedingungen regeln die Nutzung von GreenLens und der dazugehoerigen Services.',
|
||||
section1: 'GreenLens wird als digitale App und Web-Service fuer Pflanzenscans, Informationen und accountbezogene Funktionen bereitgestellt.',
|
||||
section2: 'Vor dem Livegang muessen diese Bedingungen durch rechtlich gepruefte und vollstaendige Vertragstexte ersetzt werden.',
|
||||
contactLabel: 'Kontakt',
|
||||
},
|
||||
en: {
|
||||
title: 'Terms of Use',
|
||||
intro:
|
||||
'By using GreenLens, you agree to these Terms of Use. Please read them carefully.',
|
||||
section1: '1. Services',
|
||||
text1:
|
||||
'GreenLens provides AI-powered plant identification, health diagnosis, and care reminders. The app is free to use; advanced features require a subscription or credits.',
|
||||
section2: '2. Subscriptions and In-App Purchases',
|
||||
text2:
|
||||
'GreenLens Pro is an auto-renewable subscription (monthly or yearly). Payment is charged to your Apple Account. Your subscription automatically renews unless cancelled at least 24 hours before the end of the current billing period. Credits are one-time purchases and are non-transferable.',
|
||||
section3: '3. Cancellation',
|
||||
text3:
|
||||
'You can cancel your subscription at any time in iPhone Settings under your Apple ID account. After cancellation, you retain access until the end of the paid period.',
|
||||
section4: '4. Disclaimer',
|
||||
text4:
|
||||
'GreenLens provides information based on AI analysis. This does not replace professional advice. We accept no liability for damages arising from use of the app.',
|
||||
section5: '5. Contact',
|
||||
text5: 'If you have questions about these Terms of Use, contact us by email.',
|
||||
title: 'Terms of Service',
|
||||
intro: 'These terms govern the use of GreenLens and its related services.',
|
||||
section1: 'GreenLens is provided as a digital app and web service for plant scans, information, and account-related functionality.',
|
||||
section2: 'Before launch, replace this placeholder with legally reviewed and complete terms for your business.',
|
||||
contactLabel: 'Contact',
|
||||
},
|
||||
es: {
|
||||
title: 'Terminos de Uso',
|
||||
intro:
|
||||
'Al usar GreenLens, aceptas estos Terminos de Uso. Por favor, leelos detenidamente.',
|
||||
section1: '1. Servicios',
|
||||
text1:
|
||||
'GreenLens ofrece identificacion de plantas, diagnostico de salud y recordatorios de cuidado basados en IA. La app es gratuita; las funciones avanzadas requieren una suscripcion o creditos.',
|
||||
section2: '2. Suscripciones y Compras',
|
||||
text2:
|
||||
'GreenLens Pro es una suscripcion de renovacion automatica (mensual o anual). El pago se carga a tu cuenta de Apple. La suscripcion se renueva automaticamente salvo que la canceles al menos 24 horas antes del final del periodo actual. Los creditos son compras unicas y no son transferibles.',
|
||||
section3: '3. Cancelacion',
|
||||
text3:
|
||||
'Puedes cancelar tu suscripcion en cualquier momento en los Ajustes del iPhone bajo tu cuenta de Apple ID. Tras la cancelacion, conservas el acceso hasta el final del periodo pagado.',
|
||||
section4: '4. Exencion de responsabilidad',
|
||||
text4:
|
||||
'GreenLens proporciona informacion basada en analisis de IA. Esto no reemplaza el asesoramiento profesional. No aceptamos responsabilidad por danos derivados del uso de la app.',
|
||||
section5: '5. Contacto',
|
||||
text5: 'Si tienes preguntas sobre estos Terminos de Uso, contactanos por correo electronico.',
|
||||
title: 'Terminos del Servicio',
|
||||
intro: 'Estos terminos regulan el uso de GreenLens y sus servicios relacionados.',
|
||||
section1: 'GreenLens se ofrece como app y servicio web para escaneo de plantas, informacion y funciones de cuenta.',
|
||||
section2: 'Antes del lanzamiento, sustituye este texto por terminos completos revisados legalmente.',
|
||||
contactLabel: 'Contacto',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -72,18 +36,10 @@ export default function TermsPage() {
|
||||
<h1>{c.title}</h1>
|
||||
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
|
||||
<p>{c.intro}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section1}</h2>
|
||||
<p>{c.text1}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section2}</h2>
|
||||
<p>{c.text2}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section3}</h2>
|
||||
<p>{c.text3}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section4}</h2>
|
||||
<p>{c.text4}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section5}</h2>
|
||||
<p>{c.text5}</p>
|
||||
<p style={{ marginTop: '0.75rem' }}>
|
||||
<a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
|
||||
<p>{c.section1}</p>
|
||||
<p>{c.section2}</p>
|
||||
<p>
|
||||
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,66 +1,111 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-GreenLens}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-GreenLens}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-GreenLens}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
# Expose to Railway/external — set firewall rules on your server!
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY is required}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./nginx/certs:/etc/nginx/certs:ro
|
||||
depends_on:
|
||||
- app
|
||||
- minio
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2.8-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
SITE_DOMAIN: ${SITE_DOMAIN:-greenlenspro.com}
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
landing:
|
||||
condition: service_started
|
||||
api:
|
||||
condition: service_healthy
|
||||
|
||||
landing:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
NEXT_PUBLIC_SITE_URL: ${SITE_URL:-https://greenlenspro.com}
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-greenlns}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-greenlns}
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: ${POSTGRES_DB:-greenlns}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-greenlns}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
MINIO_ENDPOINT: minio
|
||||
MINIO_PORT: 9000
|
||||
MINIO_USE_SSL: "false"
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-greenlns-minio}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required}
|
||||
MINIO_BUCKET: ${MINIO_BUCKET:-plant-images}
|
||||
MINIO_PUBLIC_URL: ${MINIO_PUBLIC_URL:-https://greenlenspro.com/storage}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
OPENAI_SCAN_MODEL: ${OPENAI_SCAN_MODEL:-gpt-5-mini}
|
||||
OPENAI_HEALTH_MODEL: ${OPENAI_HEALTH_MODEL:-gpt-5-mini}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
||||
STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY:-}
|
||||
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:-}
|
||||
REVENUECAT_WEBHOOK_SECRET: ${REVENUECAT_WEBHOOK_SECRET:-}
|
||||
REVENUECAT_PRO_ENTITLEMENT_ID: ${REVENUECAT_PRO_ENTITLEMENT_ID:-pro}
|
||||
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
|
||||
PLANT_IMPORT_ADMIN_KEY: ${PLANT_IMPORT_ADMIN_KEY:-}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-greenlns}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-greenlns}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-greenlns-minio}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required}
|
||||
command: server /data
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
caddy_config:
|
||||
caddy_data:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export const siteConfig = {
|
||||
name: 'GreenLens',
|
||||
domain: 'https://greenlns-landing.vercel.app',
|
||||
supportEmail: 'knuth.timo@gmail.com',
|
||||
legalEmail: 'knuth.timo@gmail.com',
|
||||
const siteUrl = (process.env.NEXT_PUBLIC_SITE_URL || 'https://greenlenspro.com').trim()
|
||||
|
||||
export const siteConfig = {
|
||||
name: 'GreenLens',
|
||||
domain: siteUrl,
|
||||
supportEmail: 'knuth.timo@gmail.com',
|
||||
legalEmail: 'knuth.timo@gmail.com',
|
||||
iosAppStoreUrl: '',
|
||||
androidPlayStoreUrl: '',
|
||||
company: {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
import path from 'node:path'
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
turbopack: {
|
||||
root: path.join(__dirname),
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://greenlns-landing.vercel.app/sitemap.xml
|
||||
Reference in New Issue
Block a user