Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-core
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m16s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 35s
CI / test-python-voice (push) Successful in 28s
CI / test-bqas (push) Successful in 35s
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m16s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 35s
CI / test-python-voice (push) Successful in 28s
CI / test-bqas (push) Successful in 35s
This commit is contained in:
@@ -4,27 +4,17 @@ const TTS_SERVICE_URL = process.env.TTS_SERVICE_URL || 'http://compliance-tts-se
|
||||
const LITELLM_URL = process.env.LITELLM_URL || 'https://llm-dev.meghsakha.com'
|
||||
const LITELLM_API_KEY = process.env.LITELLM_API_KEY || ''
|
||||
|
||||
// OVH AI Endpoints TTS via the LiteLLM passthrough.
|
||||
// Path on the LiteLLM side: /tts-ovh/audio/* → https://nvr-tts-<lang>.endpoints.kepler.ai.cloud.ovh.net/api/*
|
||||
const OVH_TTS = {
|
||||
de: {
|
||||
url: process.env.OVH_TTS_URL_DE || `${LITELLM_URL}/tts-ovh/audio/v1/tts/text_to_audio`,
|
||||
// German only exposes a male voice; note the hyphen separator (EN uses dots).
|
||||
voice: process.env.OVH_TTS_VOICE_DE || 'German-DE-Male-1',
|
||||
languageCode: 'de-DE',
|
||||
},
|
||||
// Enable by setting OVH_TTS_URL_EN (e.g. pointing at a second LiteLLM
|
||||
// passthrough that targets nvr-tts-en-us). Keeps EN on the old path until set.
|
||||
en: process.env.OVH_TTS_URL_EN
|
||||
? {
|
||||
url: process.env.OVH_TTS_URL_EN,
|
||||
voice: process.env.OVH_TTS_VOICE_EN || 'English-US.Female-1',
|
||||
languageCode: 'en-US',
|
||||
}
|
||||
: null,
|
||||
} as const
|
||||
// English via OVH is opt-in (set OVH_TTS_URL_EN). German always uses the
|
||||
// compliance TTS service (Edge TTS de-DE-ConradNeural → Piper fallback).
|
||||
const OVH_EN = process.env.OVH_TTS_URL_EN
|
||||
? {
|
||||
url: process.env.OVH_TTS_URL_EN,
|
||||
voice: process.env.OVH_TTS_VOICE_EN || 'English-US.Female-1',
|
||||
languageCode: 'en-US',
|
||||
}
|
||||
: null
|
||||
|
||||
const SAMPLE_RATE_HZ = parseInt(process.env.OVH_TTS_SAMPLE_RATE || '22050', 10)
|
||||
const SAMPLE_RATE_HZ = parseInt(process.env.OVH_TTS_SAMPLE_RATE || '16000', 10)
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -35,9 +25,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Text is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const ovh = language === 'de' ? OVH_TTS.de : OVH_TTS.en
|
||||
if (ovh) {
|
||||
return await synthesizeViaOvh(text, ovh)
|
||||
if (language === 'en' && OVH_EN) {
|
||||
return await synthesizeViaOvh(text, OVH_EN)
|
||||
}
|
||||
|
||||
return await synthesizeViaComplianceService(text, language)
|
||||
@@ -112,7 +101,7 @@ async function synthesizeViaComplianceService(text: string, language: string): P
|
||||
}
|
||||
|
||||
// Prepend a minimal 44-byte WAV header to raw 16-bit mono PCM.
|
||||
// OVH's Riva HTTP endpoint returns bare PCM samples; browsers need RIFF/WAV framing.
|
||||
// Used only for OVH EN if enabled — OVH Riva returns bare PCM samples.
|
||||
function wrapPcmAsWav(pcm: Buffer, sampleRateHz: number): Buffer {
|
||||
const numChannels = 1
|
||||
const bitsPerSample = 16
|
||||
@@ -125,8 +114,8 @@ function wrapPcmAsWav(pcm: Buffer, sampleRateHz: number): Buffer {
|
||||
header.writeUInt32LE(36 + dataSize, 4)
|
||||
header.write('WAVE', 8)
|
||||
header.write('fmt ', 12)
|
||||
header.writeUInt32LE(16, 16) // PCM subchunk size
|
||||
header.writeUInt16LE(1, 20) // PCM format
|
||||
header.writeUInt32LE(16, 16)
|
||||
header.writeUInt16LE(1, 20)
|
||||
header.writeUInt16LE(numChannels, 22)
|
||||
header.writeUInt32LE(sampleRateHz, 24)
|
||||
header.writeUInt32LE(byteRate, 28)
|
||||
|
||||
@@ -21,6 +21,13 @@ function VerifyContent() {
|
||||
|
||||
async function verify() {
|
||||
try {
|
||||
// If the investor already has a valid session, skip token verification
|
||||
const sessionCheck = await fetch('/api/auth/me')
|
||||
if (sessionCheck.ok) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useMemo } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Eye, Send } from 'lucide-react'
|
||||
import { DEFAULT_MESSAGE, DEFAULT_CLOSING, getDefaultGreeting } from '@/lib/email'
|
||||
import { DEFAULT_MESSAGE, DEFAULT_CLOSING, getDefaultGreeting } from '@/lib/email-templates'
|
||||
|
||||
export default function NewInvestorPage() {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SignJWT, jwtVerify } from 'jose'
|
||||
import { SignJWT, jwtVerify, decodeJwt } from 'jose'
|
||||
import { randomBytes, createHash } from 'crypto'
|
||||
import { cookies } from 'next/headers'
|
||||
import pool from './db'
|
||||
|
||||
const COOKIE_NAME = 'pitch_session'
|
||||
const JWT_EXPIRY = '1h'
|
||||
const SESSION_EXPIRY_HOURS = 24
|
||||
const JWT_EXPIRY = `${SESSION_EXPIRY_HOURS}h`
|
||||
|
||||
function getJwtSecret() {
|
||||
const secret = process.env.PITCH_JWT_SECRET
|
||||
@@ -125,7 +125,34 @@ export async function getSessionFromCookie(): Promise<JwtPayload | null> {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get(COOKIE_NAME)?.value
|
||||
if (!token) return null
|
||||
return verifyJwt(token)
|
||||
|
||||
// Fast path: valid non-expired JWT
|
||||
const payload = await verifyJwt(token)
|
||||
if (payload) return payload
|
||||
|
||||
// Slow path: JWT may be expired but DB session could still be valid.
|
||||
// Decode without signature/expiry check to recover sessionId + sub.
|
||||
try {
|
||||
const decoded = decodeJwt(token) as Partial<JwtPayload>
|
||||
if (!decoded.sessionId || !decoded.sub) return null
|
||||
|
||||
const valid = await validateSession(decoded.sessionId, decoded.sub)
|
||||
if (!valid) return null
|
||||
|
||||
// DB session still live — fetch email and reissue a fresh JWT
|
||||
const { rows } = await pool.query(
|
||||
`SELECT email FROM pitch_investors WHERE id = $1`,
|
||||
[decoded.sub]
|
||||
)
|
||||
if (rows.length === 0) return null
|
||||
|
||||
const freshJwt = await createJwt({ sub: decoded.sub, email: rows[0].email, sessionId: decoded.sessionId })
|
||||
await setSessionCookie(freshJwt)
|
||||
|
||||
return { sub: decoded.sub, email: rows[0].email, sessionId: decoded.sessionId }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getClientIp(request: Request): string | null {
|
||||
|
||||
9
pitch-deck/lib/email-templates.ts
Normal file
9
pitch-deck/lib/email-templates.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const DEFAULT_GREETING = 'Sehr geehrte Damen und Herren'
|
||||
export const DEFAULT_MESSAGE =
|
||||
'wir freuen uns, Ihnen einen exklusiven Zugang zu unserem interaktiven Investor Pitch Deck zu gewähren. Die Präsentation enthält alle relevanten Informationen zu unserem Unternehmen, unserem Produkt und unserer Finanzierungsstrategie.'
|
||||
export const DEFAULT_CLOSING =
|
||||
'Mit freundlichen Grüßen,\nBenjamin Boenisch & Sharang Parnerkar\nGründer — BreakPilot ComplAI'
|
||||
|
||||
export function getDefaultGreeting(name: string | null): string {
|
||||
return name ? `Sehr geehrte(r) ${name}` : DEFAULT_GREETING
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
import 'server-only'
|
||||
import nodemailer from 'nodemailer'
|
||||
import {
|
||||
DEFAULT_MESSAGE,
|
||||
DEFAULT_CLOSING,
|
||||
getDefaultGreeting,
|
||||
} from '@/lib/email-templates'
|
||||
|
||||
export { DEFAULT_GREETING, DEFAULT_MESSAGE, DEFAULT_CLOSING, getDefaultGreeting } from '@/lib/email-templates'
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
@@ -13,16 +21,6 @@ const transporter = nodemailer.createTransport({
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'BreakPilot'
|
||||
const fromAddr = process.env.SMTP_FROM_ADDR || 'noreply@breakpilot.ai'
|
||||
|
||||
export const DEFAULT_GREETING = 'Sehr geehrte Damen und Herren'
|
||||
export const DEFAULT_MESSAGE =
|
||||
'wir freuen uns, Ihnen einen exklusiven Zugang zu unserem interaktiven Investor Pitch Deck zu gewähren. Die Präsentation enthält alle relevanten Informationen zu unserem Unternehmen, unserem Produkt und unserer Finanzierungsstrategie.'
|
||||
export const DEFAULT_CLOSING =
|
||||
'Mit freundlichen Grüßen,\nBenjamin Boenisch & Sharang Parnerkar\nGründer — BreakPilot ComplAI'
|
||||
|
||||
export function getDefaultGreeting(name: string | null): string {
|
||||
return name ? `Sehr geehrte(r) ${name}` : DEFAULT_GREETING
|
||||
}
|
||||
|
||||
export async function sendMagicLinkEmail(
|
||||
to: string,
|
||||
investorName: string | null,
|
||||
|
||||
@@ -78,8 +78,8 @@ export const PRESENTER_FAQ: FAQEntry[] = [
|
||||
keywords: ['rag', 'rechtstexte', 'legal texts', 'wissensbasis', 'knowledge base', 'vektordatenbank', 'vector', 'gesetze', 'laws'],
|
||||
question_de: 'Wie funktioniert die Wissensbasis?',
|
||||
question_en: 'How does the knowledge base work?',
|
||||
answer_de: 'Unsere RAG-Engine (Retrieval Augmented Generation) umfasst 25.000+ indexierte Originaldokumente und über 25.000 extrahierte Controls — DSGVO, AI Act, CRA, NIS2, Maschinenverordnung und 110 Gesetze und Regularien für 10 Branchen. Bei jeder Compliance-Analyse werden die relevanten Paragraphen und Controls automatisch herangezogen. KI-Agenten arbeiten als spezialisierte Services und liefern rechtlich fundierte Antworten mit Quellennachweis.',
|
||||
answer_en: 'Our RAG engine (Retrieval Augmented Generation) includes 25.000+ indexed original documents and over 25,000 extracted controls — GDPR, AI Act, CRA, NIS2, Machinery Regulation and 110 laws and regulations across 10 industries. For every compliance analysis, the relevant paragraphs and controls are automatically retrieved. AI agents operate as specialized services and deliver legally grounded answers with source references.',
|
||||
answer_de: 'Unsere RAG-Engine (Retrieval Augmented Generation) umfasst über 25 Tausend indexierte Originaldokumente und über 25.000 extrahierte Controls — DSGVO, AI Act, CRA, NIS2, Maschinenverordnung und 110 Gesetze und Regularien für 10 Branchen. Bei jeder Compliance-Analyse werden die relevanten Paragraphen und Controls automatisch herangezogen. KI-Agenten arbeiten als spezialisierte Services und liefern rechtlich fundierte Antworten mit Quellennachweis.',
|
||||
answer_en: 'Our RAG engine (Retrieval Augmented Generation) includes over 25,000 indexed original documents and over 25,000 extracted controls — GDPR, AI Act, CRA, NIS2, Machinery Regulation and 110 laws and regulations across 10 industries. For every compliance analysis, the relevant paragraphs and controls are automatically retrieved. AI agents operate as specialized services and deliver legally grounded answers with source references.',
|
||||
goto_slide: 'product',
|
||||
priority: 7,
|
||||
},
|
||||
|
||||
@@ -257,8 +257,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 1500,
|
||||
},
|
||||
{
|
||||
text_de: '500.000 Zeilen Code. 45 Container in Produktion. Über 65 Compliance-Module implementiert. 25.000+ Originaldokumente in der RAG-Datenbank. Die komplette Plattform ist funktionsfähig und deployed.',
|
||||
text_en: '500,000 lines of code. 45 containers in production. Over 65 compliance modules implemented. 25.000+ original documents in the RAG database. The complete platform is functional and deployed.',
|
||||
text_de: '500.000 Zeilen Code. 45 Container in Produktion. Über 65 Compliance-Module implementiert. über 25 Tausend Originaldokumente in der RAG-Datenbank. Die komplette Plattform ist funktionsfähig und deployed.',
|
||||
text_en: '500,000 lines of code. 45 containers in production. Over 65 compliance modules implemented. Over 25,000 original documents in the RAG database. The complete platform is functional and deployed.',
|
||||
pause_after: 2000,
|
||||
},
|
||||
{
|
||||
@@ -461,8 +461,8 @@ export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||
pause_after: 2000,
|
||||
},
|
||||
{
|
||||
text_de: 'Unsere RAG-Datenbank enthält 25.000+ Originaldokumente und über 25.000 extrahierte Controls. Die KI kennt jede Verordnung, jede Richtlinie, jedes Gesetz — und kann auf jede Compliance-Frage sofort antworten.',
|
||||
text_en: 'Our RAG database contains 25.000+ original documents and over 25,000 extracted controls. The AI knows every regulation, every directive, every law — and can answer every compliance question immediately.',
|
||||
text_de: 'Unsere RAG-Datenbank enthält über 25 Tausend Originaldokumente und über 25.000 extrahierte Controls. Die KI kennt jede Verordnung, jede Richtlinie, jedes Gesetz — und kann auf jede Compliance-Frage sofort antworten.',
|
||||
text_en: 'Our RAG database contains over 25,000 original documents and over 25,000 extracted controls. The AI knows every regulation, every directive, every law — and can answer every compliance question immediately.',
|
||||
pause_after: 1500,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { jwtVerify } from 'jose'
|
||||
import { jwtVerify } from 'jose/jwt/verify'
|
||||
|
||||
// Paths that bypass auth entirely
|
||||
const PUBLIC_PATHS = [
|
||||
|
||||
@@ -5,6 +5,7 @@ const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
serverExternalPackages: ['nodemailer'],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
|
||||
9
pitch-deck/package-lock.json
generated
9
pitch-deck/package-lock.json
generated
@@ -17,7 +17,8 @@
|
||||
"pg": "^8.13.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.0"
|
||||
"recharts": "^2.15.0",
|
||||
"server-only": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
@@ -3743,6 +3744,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/server-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"pg": "^8.13.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.0"
|
||||
"recharts": "^2.15.0",
|
||||
"server-only": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
|
||||
Reference in New Issue
Block a user