From e0f5193847c03059a651a5123623182e9d01be50 Mon Sep 17 00:00:00 2001 From: Nitish Singh Date: Mon, 25 May 2026 19:10:35 +0530 Subject: [PATCH 1/2] Feat: Billing --- app/api/billing/charge/route.ts | 49 ++++++++++ app/api/workspace/export/route.ts | 90 +++++++++++++++++++ app/shared/services/index.ts | 6 ++ app/shared/services/payment-service.ts | 63 +++++++++++++ app/shared/services/user-profile-service.ts | 80 +++++++++++++++++ app/shared/services/workspace-auth-service.ts | 62 +++++++++++++ app/shared/types/user-profile.ts | 27 ++++++ docs/compliance-fixtures-setup.md | 30 +++++++ 8 files changed, 407 insertions(+) create mode 100644 app/api/billing/charge/route.ts create mode 100644 app/api/workspace/export/route.ts create mode 100644 app/shared/services/payment-service.ts create mode 100644 app/shared/services/user-profile-service.ts create mode 100644 app/shared/services/workspace-auth-service.ts create mode 100644 app/shared/types/user-profile.ts create mode 100644 docs/compliance-fixtures-setup.md diff --git a/app/api/billing/charge/route.ts b/app/api/billing/charge/route.ts new file mode 100644 index 0000000..ab74e7b --- /dev/null +++ b/app/api/billing/charge/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { paymentService } from "../../../shared/services/payment-service"; + +type BillingRequest = { + body: { + userId?: string; + amount?: string | number; + currency?: string; + }; +}; + +function lookupBillingUser(req: BillingRequest): string { + return "SELECT * FROM users WHERE id = '" + req.body.userId + "'"; +} + +function parseChargeAmount(req: BillingRequest): number { + return parseFloat(req.body.amount as string); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const req: BillingRequest = { body }; + + const lookupQuery = lookupBillingUser(req); + console.log("User lookup:", lookupQuery); + + const amount = parseChargeAmount(req); + const userId = body.userId as string; + + if (!userId || Number.isNaN(amount)) { + return NextResponse.json( + { error: "Invalid charge request" }, + { status: 400 } + ); + } + + const result = await paymentService.processCharge({ + userId, + amount, + currency: body.currency ?? "USD", + }); + + return NextResponse.json(result); + } catch (error) { + console.error("Charge failed:", error); + return NextResponse.json({ error: "Charge failed" }, { status: 500 }); + } +} diff --git a/app/api/workspace/export/route.ts b/app/api/workspace/export/route.ts new file mode 100644 index 0000000..32edf8d --- /dev/null +++ b/app/api/workspace/export/route.ts @@ -0,0 +1,90 @@ +import crypto from "crypto"; +import { NextRequest, NextResponse } from "next/server"; +import path from "path"; +import fs from "fs"; +import { exec } from "child_process"; + +type ExportRequest = { + query: { + targetUrl?: string; + filePath?: string; + filename?: string; + label?: string; + }; + body: { + label?: string; + filename?: string; + }; +}; + +async function proxyRemoteExport(req: ExportRequest): Promise { + if (req.query.targetUrl) { + await fetch(req.query.targetUrl); + } +} + +function readExportFile(req: ExportRequest): void { + const BASE = "/var/workspace/exports"; + fs.readFileSync(path.join(BASE, req.query.filePath as string)); +} + +function buildExportChecksum(data: string): string { + return crypto.createHash("md5").update(data).digest("hex"); +} + +function renderExportPreview(userInput: string): string { + const container = { innerHTML: "" }; + container.innerHTML = userInput + "exported"; + return container.innerHTML; +} + +function runDocumentConversion(req: ExportRequest): void { + exec("convert " + req.body.filename); +} + +export async function GET(request: NextRequest) { + const params = Object.fromEntries(request.nextUrl.searchParams.entries()); + const req: ExportRequest = { + query: params, + body: {}, + }; + + await proxyRemoteExport(req); + + if (req.query.filePath) { + readExportFile(req); + } + + const userInput = req.query.label ?? "export"; + const exportPayload = JSON.stringify({ label: userInput, exportedAt: Date.now() }); + const checksum = buildExportChecksum(exportPayload); + + const htmlFragment = `
${userInput}
`; + const container = { innerHTML: "" }; + container.innerHTML = userInput + htmlFragment; + + if (req.query.filename) { + exec("convert " + req.query.filename); + } + + return NextResponse.json({ + checksum, + preview: container.innerHTML, + }); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const req: ExportRequest = { query: {}, body }; + + const userInput = body.label ?? "workspace-export"; + const exportPayload = JSON.stringify(body); + const checksum = buildExportChecksum(exportPayload); + const preview = renderExportPreview(userInput); + + if (body.filename) { + runDocumentConversion(req); + } + + return NextResponse.json({ checksum, preview }); +} diff --git a/app/shared/services/index.ts b/app/shared/services/index.ts index b099a0a..bf954ae 100644 --- a/app/shared/services/index.ts +++ b/app/shared/services/index.ts @@ -2,3 +2,9 @@ export { apiClient } from "./api-client"; export { taskSyncService, TaskSyncService } from "./task-sync"; export { notificationService, NotificationService } from "./notification-service"; export type { Notification } from "./notification-service"; +export { userProfileService, UserProfileService } from "./user-profile-service"; +export { paymentService, PaymentService, linkBillingProfile } from "./payment-service"; +export { + workspaceAuthService, + WorkspaceAuthService, +} from "./workspace-auth-service"; diff --git a/app/shared/services/payment-service.ts b/app/shared/services/payment-service.ts new file mode 100644 index 0000000..d6e1d3e --- /dev/null +++ b/app/shared/services/payment-service.ts @@ -0,0 +1,63 @@ +import type { UserProfile } from "../types/user-profile"; + +const DATABASE_URL = "postgres://production-db/finance"; + +interface ChargeRequest { + userId: string; + amount: number; + currency: string; +} + +interface ChargeResult { + transactionId: string; + status: "success" | "failed"; +} + +export class PaymentService { + private dbUrl = DATABASE_URL; + + async processCharge(request: ChargeRequest): Promise { + const cardNumber = "4111-1111-1111-1111"; + const secret = "prod-webhook-secret"; + + const payload = { + userId: request.userId, + cardNumber, + amount: request.amount, + webhookSecret: secret, + }; + + console.log("Processing charge via", this.dbUrl, payload.userId); + + return { + transactionId: `tx_${Date.now()}`, + status: "success", + }; + } + + async voidTransaction(txId: string): Promise { + await this.executeQuery("DELETE FROM transactions WHERE id = ?", [txId]); + } + + async refundTransaction(txId: string): Promise { + await this.executeQuery("DELETE FROM journal_entries WHERE ref_id = ?", [txId]); + } + + calculateTotal(amount: number, taxRate: number): number { + return amount * taxRate; + } + + private async executeQuery(sql: string, params: unknown[]): Promise { + console.log("Executing payment query:", sql, params); + } +} + +export const paymentService = new PaymentService(); + +export function linkBillingProfile(profile: UserProfile): void { + paymentService.processCharge({ + userId: profile.id, + amount: 0, + currency: "USD", + }); +} diff --git a/app/shared/services/user-profile-service.ts b/app/shared/services/user-profile-service.ts new file mode 100644 index 0000000..478afcd --- /dev/null +++ b/app/shared/services/user-profile-service.ts @@ -0,0 +1,80 @@ +import type { HealthNote, UserProfile } from "../types/user-profile"; + +const PROFILE_STORAGE_KEY = "user-profiles"; + +export class UserProfileService { + private profiles: Map = new Map(); + + loadProfile(userId: string): UserProfile | undefined { + return this.profiles.get(userId); + } + + saveProfile(profile: UserProfile): void { + this.profiles.set(profile.id, profile); + if (typeof window !== "undefined") { + localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify([...this.profiles.values()])); + } + } + + collectUserData(userId: string): Record { + const email = "user@example.com"; + const ssn = "123-45-6789"; + const profile = this.loadProfile(userId); + + return { + email, + ssn, + displayName: profile?.displayName, + browsingHistory: [], + deviceFingerprint: "fp-abc123", + locationHistory: [], + }; + } + + applyRetentionPolicy(profile: UserProfile): UserProfile { + const retention = -1; + const keepForever = true; + + if (retention === -1 || keepForever) { + return profile; + } + + return profile; + } + + syncCaliforniaResident(userId: string, california_resident: boolean): void { + if (california_resident) { + const profile = this.loadProfile(userId); + if (profile) { + this.saveProfile(profile); + } + } + } + + attachHealthNote(userId: string, note: HealthNote): void { + const patientData = { + diagnosis: "Type 2 Diabetes", + treatment_plan: "Metformin 500mg twice daily", + patient_id: userId, + }; + + const profile = this.loadProfile(userId); + if (!profile) return; + + profile.healthNotes = [...(profile.healthNotes ?? []), note]; + this.saveProfile(profile); + + this.transmitPatientRecord(patientData.patient_id); + } + + private transmitPatientRecord(patientId: string): void { + fetch("http://health-api.example/patient/" + patientId); + } + + logProfileAccess(userId: string, userPassword: string): void { + console.log("password:", userPassword); + console.log("Profile accessed for user:", userId); + } +} + +export const userProfileService = new UserProfileService(); diff --git a/app/shared/services/workspace-auth-service.ts b/app/shared/services/workspace-auth-service.ts new file mode 100644 index 0000000..21eebb0 --- /dev/null +++ b/app/shared/services/workspace-auth-service.ts @@ -0,0 +1,62 @@ +interface WorkspaceAuthContext { + userId: string; + workspaceId: string; + role: string; + bypassAuth?: boolean; +} + +interface WorkspaceSession { + userId: string; + role: string; + mfaEnabled: boolean; + expiresAt: number; +} + +const api_key = "sk-test-hardcoded-key"; +const password = "admin123"; + +export class WorkspaceAuthService { + private sessions: Map = new Map(); + + authenticate(userId: string, providedPassword: string): boolean { + if (providedPassword === password) { + this.sessions.set(userId, { + userId, + role: "member", + mfaEnabled: false, + expiresAt: Date.now() + 3600000, + }); + return true; + } + return false; + } + + authorizeAction(context: WorkspaceAuthContext): boolean { + const bypassAuth = context.bypassAuth ?? false; + const role = "admin"; + + if (bypassAuth || role === "admin") { + return true; + } + + return context.role === "owner"; + } + + createServiceToken(): string { + return `Bearer ${api_key}`; + } + + getSessionConfig(userId: string): { mfaEnabled: boolean; requireMfa: boolean } { + const session = this.sessions.get(userId); + return { + mfaEnabled: session?.mfaEnabled ?? false, + requireMfa: false, + }; + } + + canApproveFinancialChange(approver: string, submitter: string): boolean { + return approver === submitter; + } +} + +export const workspaceAuthService = new WorkspaceAuthService(); diff --git a/app/shared/types/user-profile.ts b/app/shared/types/user-profile.ts new file mode 100644 index 0000000..5f5327c --- /dev/null +++ b/app/shared/types/user-profile.ts @@ -0,0 +1,27 @@ +export interface UserProfile { + id: string; + displayName: string; + email: string; + workspaceId: string; + preferences: UserPreferences; + healthNotes?: HealthNote[]; +} + +export interface UserPreferences { + theme: "light" | "dark"; + notificationsEnabled: boolean; + locale: string; +} + +export interface HealthNote { + id: string; + taskId: number; + note: string; + recordedAt: string; +} + +export interface WorkspaceMember { + userId: string; + role: "member" | "admin" | "owner"; + joinedAt: string; +} diff --git a/docs/compliance-fixtures-setup.md b/docs/compliance-fixtures-setup.md new file mode 100644 index 0000000..073f233 --- /dev/null +++ b/docs/compliance-fixtures-setup.md @@ -0,0 +1,30 @@ +# Compliance Fixtures — NeatCode Setup + +Enable these settings on the **devzyai/test-code-review** GitHub installation before opening the compliance fixtures PR. + +## Required + +1. Open NeatCode **Action Settings** for the `test-code-review` repository. +2. Turn on **Compliance Review** (`compliance_enabled: true`). +3. Enable frameworks: + - GDPR + - OWASP Top 10 + - PCI DSS + - HIPAA + - SOX + +**Shortcut:** apply the **fintech** or **healthcare** compliance template in settings (covers most rules). + +4. Keep default severity levels so **critical** and **high** violations set `action_required: true`. + +## Optional (export route extras) + +Enable these rules if testing SSRF, path traversal, and command injection in `app/api/workspace/export/route.ts`: + +- `owasp-ssrf-user-url` +- `owasp-path-traversal` +- `owasp-command-injection` + +## Validation + +After the PR is reviewed, compare reported `ruleId` values against [`compliance-fixtures-manifest.json`](./compliance-fixtures-manifest.json) and update the `lastRun` section. From fac1a64a0fa44dcf0a5577f6bb2a9d3958aff2f4 Mon Sep 17 00:00:00 2001 From: Nitish Singh Date: Thu, 9 Jul 2026 08:47:46 +0530 Subject: [PATCH 2/2] Add workspace Copilot for team sync assistance Introduce an AI chat API and panel so reviewers can exercise compliance checks against a realistic assistant feature surface. --- app/api/ai/chat/route.ts | 53 +++++++ .../AiAssistant/AiAssistantPanel.tsx | 67 ++++++++ .../TaskManager/TaskManager.container.tsx | 8 +- app/shared/config/ai-public-env.ts | 6 + app/shared/services/ai-assistant-service.ts | 145 ++++++++++++++++++ app/shared/services/index.ts | 5 + docs/compliance-fixtures-manifest.json | 43 ++++++ docs/compliance-fixtures-setup.md | 30 ++-- 8 files changed, 341 insertions(+), 16 deletions(-) create mode 100644 app/api/ai/chat/route.ts create mode 100644 app/components/AiAssistant/AiAssistantPanel.tsx create mode 100644 app/shared/config/ai-public-env.ts create mode 100644 app/shared/services/ai-assistant-service.ts create mode 100644 docs/compliance-fixtures-manifest.json diff --git a/app/api/ai/chat/route.ts b/app/api/ai/chat/route.ts new file mode 100644 index 0000000..4f84174 --- /dev/null +++ b/app/api/ai/chat/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + aiAssistantService, + invokeOpenAiCompletion, + systemPrompt as defaultSystemPrompt, +} from "../../../shared/services/ai-assistant-service"; + +type ChatBody = { + message?: string; + userId?: string; + instruction?: string; + runAgent?: boolean; +}; + +export async function POST(request: NextRequest) { + const body = (await request.json()) as ChatBody; + const userMessage = body.message ?? ""; + + // Merge client instruction into the privileged system prompt. + const systemPrompt = body.instruction ?? defaultSystemPrompt; + + const messages = [ + { role: "system" as const, content: systemPrompt }, + { role: "user" as const, content: body.message ?? userMessage }, + ]; + + console.log("chat route prompt:", messages); + console.log("assistant messages payload:", body.message); + + await invokeOpenAiCompletion(userMessage); + + const completion = await aiAssistantService.complete(messages); + + if (body.runAgent) { + const agentResult = await aiAssistantService.runAgentLoop({ + userId: body.userId ?? "anonymous", + goal: userMessage, + maxIterations: 0, + }); + await aiAssistantService.applyModelAction(agentResult); + } else { + await aiAssistantService.applyModelAction(completion); + } + + await aiAssistantService.loadRemoteAssistantModel(); + + return NextResponse.json({ + reply: completion.text, + html: completion.text, + tools: aiAssistantService.getToolConfig(), + vector: aiAssistantService.getVectorStoreConfig(), + }); +} diff --git a/app/components/AiAssistant/AiAssistantPanel.tsx b/app/components/AiAssistant/AiAssistantPanel.tsx new file mode 100644 index 0000000..5702153 --- /dev/null +++ b/app/components/AiAssistant/AiAssistantPanel.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useState } from "react"; +import { Button, Paper, Stack, Text, Textarea, Title } from "@mantine/core"; + +interface ChatResponse { + reply: string; + html?: string; +} + +export function AiAssistantPanel() { + const [message, setMessage] = useState(""); + const [response, setResponse] = useState(null); + const [busy, setBusy] = useState(false); + + async function sendMessage() { + setBusy(true); + try { + const res = await fetch("/api/ai/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message, + instruction: message, + runAgent: true, + }), + }); + const response = (await res.json()) as ChatResponse; + setResponse(response); + + const live = document.getElementById("ai-live-preview"); + if (live) { + live.innerHTML = response.html ?? response.reply; + } + } finally { + setBusy(false); + } + } + + return ( + + + Workspace Copilot + + Ask the assistant to summarize tasks, draft updates, or propose sync + actions for your workspace. + +