feat: added desktop sso endpoints

This commit is contained in:
2026-05-28 16:05:47 -07:00
parent 91a376ac0a
commit bf6171a667
11 changed files with 1077 additions and 5 deletions

161
VIBNCODE_PLAN.md Normal file
View File

@@ -0,0 +1,161 @@
# VibnCode: Cloud-Powered Agent Desktop IDE Architecture & Implementation Plan
**Project Name:** `vibncode` (formerly TalkCody)
**Target Architecture:** Desktop Thin Client with Monaco + Native Cloud Hosting Integration
**Backend Platform:** Vibnai Cloud Infrastructure (`vibn-frontend`, `vibn-agent-runner`, Gitea, Coolify)
**Status File Location:** `master-ai/VIBNCODE_PLAN.md`
---
## 1. System High-Level Architecture Diagram
`vibncode` functions as a high-fidelity window into your cloud-hosted workspace, routing all actions, skills, and terminal commands to the `vibn-agent-runner` and Gitea/Coolify backend.
```mermaid
graph TD
%% Styling
classDef client fill:#E1F5FE,stroke:#0288D1,stroke-width:2px;
classDef runner fill:#EDE7F6,stroke:#5E35B1,stroke-width:2px;
classDef infra fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px;
subgraph Client [Desktop client - vibncode]
UI[React 19 / Monaco Editor UI]
State[Zustand Stores / Local SQLite DB Cache]
Tauri[Tauri v2 App Wrapper]
end
subgraph CloudRunner [Cloud Workspace - vibn-agent-runner]
Runner[Agent Session Runner Engine]
Workspace[Sandboxed Project Dir: /workspaces]
Tools[Vibn Platform & Sentry Tools]
end
subgraph Infrastructure [Vibnai Platform]
DB[(PostgreSQL Database)]
WebAPI[Next.js API Server: vibn-frontend]
Gitea[(Gitea Git Server)]
Coolify[Coolify Server Hosting]
end
%% Connections
UI <-->|1. Event Streams & File Sync| WebAPI
UI -->|2. Start/Cancel Execution| WebAPI
WebAPI -->|3. Route Exec Request| Runner
Runner <-->|4. Code Edits & Shell Runs| Workspace
Runner -->|5. Save Output & File Changes| WebAPI
WebAPI <-->|6. Write State & Logs| DB
Runner <-->|7. Push/Pull/Clone Code| Gitea
Runner -->|8. Manage & Deploy Apps| Coolify
Workspace -->|9. Pull Guidelines| Gitea
class UI,State,Tauri client;
class Runner,Workspace,Tools runner;
class DB,WebAPI,Gitea,Coolify,Workspace infra;
```
---
## 2. Structural Requirements & Feature Specification
### 2.1 Rebranding to `vibncode`
* All codebase identifiers, folder structures, logs, config registries, and binary product properties are renamed from `talkcody` to `vibncode` / `VibnCode` to prevent trademark leaks.
* The application bundle identifier is modified to `com.vibnai.vibncode`.
### 2.2 Preservation of the Desktop UI Experience
* The Monaco Editor, collapsible multi-panel grid, dark theme, and rich agent step logs are preserved exactly.
* All compiler execution and file system checks are offloaded to your secure cloud container runner.
### 2.3 Cloud-Hosted Live Browser Preview Pane
* **Split-Screen Interface**: Adjacent to the Monaco code editor, we inject a live webview/iframe tab.
* **Wildcard DNS Mapping**: The pane points directly to your dynamic wildcard URL generated during a dev server boot:
`https://[projectSlug]-[devServerPort].preview.vibnai.com`
* **Instant Hot-Module Replacement (HMR)**: As the cloud agent or the developer edits files, Vite or Next.js compiles the changes inside the cloud container in milliseconds. Traefik reverse-proxies the stream, causing the embedded browser pane to update instantly on-screen without requiring a manual refresh.
### 2.4 Autonomous Gitea & Coolify Environment Administration
* We add tabs to the left navigation panel matching the modules of your Vibnai Web Dashboard:
1. **Code [📝]**: Monaco editor, virtual filesystem browser.
2. **Plan [📋]**: Synchronized feature checklist showing requirements directly tied to `fs_projects.phase_data`.
3. **Host [🚀]**: Lists running Coolify application containers, database states, environment credentials (`.env`), and displays live resource monitors (RAM, CPU, Build Logs).
4. **Market [📊]**: Suggests GBP categories, researches competitor domains (DataForSEO), and performs technical site scrapes.
5. **Preview [🌐]**: Embedded browser preview tab rendering the wildcard live deployment URL.
* **Autonomous Agent Tools**: The AI is equipped with Gitea and Coolify administration commands (e.g. `git_commit_and_push`, `apps_create_database`, `apps_deploy`, `hosting_set_env`, `project_recent_errors`). The AI can provision databases, modify env keys, verify compiling logs, and fix its own code bugs inside the sandboxed cloud workspaces autonomously.
---
## 3. Database & Database Synchronization Specifications
### 3.1 Mapping Local Client States to PostgreSQL
We will link `vibncode`'s local state structures with your `vibn-frontend` PostgreSQL databases:
* **Project Lists**: Retrieved via `GET /api/projects` querying the `fs_projects` and `fs_users` tables.
* **Active Agent Session**: Instantiated via `POST /api/projects/[projectId]/agent/sessions` and updated by `vibn-agent-runner` sending `PATCH /api/projects/[projectId]/agent/sessions/[sessionId]` with `outputLine` and `changedFile` updates.
* **Virtual File Tree**: Read via `GET /api/workspace/tree` which performs a directory scrape of `/workspaces/[giteaRepo]` and renders a visual file browser in the desktop client's sidebar.
---
## 4. Key Code Paths & Planned Refactoring
To implement this architecture elegantly, the following specific files will be refactored:
### 4.1 Client-Side Refactoring (Tauri / React)
1. **`src/services/file-service.ts`**: Replace `@tauri-apps/plugin-fs` reads/writes with authorized cloud workspace endpoints:
* `GET /api/workspace/tree` $\rightarrow$ Scrapes Gitea directory tree.
* `POST /api/workspace/file/read` $\rightarrow$ Fetches content for Monaco.
* `POST /api/workspace/file/write` $\rightarrow$ Writes edits back to the cloud.
2. **`src/services/execution-service.ts`**:
* Redirect `startExecution()` to trigger `POST /api/projects/[projectId]/agent/execute` on the server instead of running the loop inside the local app process.
* Connect to `vibn-frontend`'s SSE or session poll endpoint to map logs, changed file notifications, and status streams directly to Zustand.
3. **`src/components/chat/embedded-preview.tsx`** (New Component):
* Incorporate a split-screen panel with an `<iframe src="https://[subdomain].preview.vibnai.com" />` which shows/hides adjacent to the Monaco Editor when a dev server is booted.
### 4.2 Server-Side Porting (`vibn-agent-runner`)
1. **Incorporate Parallel Agent Brain**:
* Extract TalkCody's `tool-dependency-analyzer.ts` and `tool-executor.ts` (TypeScript) and port them inside `vibn-agent-runner/src/orchestration/`.
* This equips your cloud runner with **4-level Smart Concurrency** (batching parallel reads and serializing write reviews inside Docker).
2. **Enable the Ralph Loop Autonomy**:
* Inject `ralph-loop-service.ts` into your `runSessionAgent()` background executor. This ensures that when the agent outputs self-reflection keywords, the cloud-runner automatically spawns a continuation cycle until the task is complete.
---
## 5. Component-Level Service Redirections (The Vibn Integration Bridge)
To preserve the rich desktop UI completely intact and prevent breaking any complex React views, we map all native local OS dependencies inside `src/components/` to go over our cloud services instead of rewriting the files individually:
### 5.1 Virtualizing file-editor-content.tsx & file-tree.tsx
* **The Component**: The Monaco Editor panel and the Sidebar File Tree.
* **Current State**: Calls `repositoryService.readFileContent(path)` and `fastDirectoryTreeService.buildDirectoryTree(rootPath)` which invoke local Rust file-readers and Tauri FS plugins.
* **Re-engineered Bridge**: We modify `repositoryService` and `fastDirectoryTreeService` to POST to your secure Next.js `POST /api/mcp` endpoint with the actions `"fs.read"` and `"fs.tree"`. This fetches the file content and directories list directly from Gitea inside the `/workspaces` Docker environment and populates Monaco seamlessly.
### 5.2 Redirecting empty-repository-state.tsx
* **The Component**: The welcoming dashboard panel shown when no local repository folder is opened.
* **Current State**: Prompts the developer to select a folder on their local hard drive.
* **Re-engineered Bridge**: When the developer logs in and selects a project on the `projects-page.tsx`, we automatically set the active project `root_path` to the cloud repository path (derived from the database) and immediately trigger the virtual directory tree, bypassing local disk queries entirely.
### 5.3 Bridging file-preview.tsx (Chat Attachments)
* **The Component**: The attachment downloader inside your chat messages (saving images/videos locally).
* **Current State**: Uses Tauri's `save` dialog and `plugin-fs` to write bytes to the local Downloads folder.
* **Re-engineered Bridge**: We modify this to download attachments directly from your GCP Cloud Storage bucket / R2 CDN (`https://cdn.vibnai.com/attachments/...`) over HTTP using the native browser download manager.
---
## 6. Phased Implementation Roadmap
```
PHASE 1: System Rebranding & Desktop App Clean-up
├── Rebrand 'talkcody' to 'vibncode' globally across folders & package assets.
├── Modify Tauri config JSONs with the bundle ID 'com.vibnai.vibncode'.
└── Disable local compilers and mock local SQLite caches.
PHASE 2: Backend API Development in Next.js
├── Implement virtual file endpoints (POST /api/workspace/file/read, GET /tree).
└── Establish SSE / WebSockets stream routes for agent output lines.
PHASE 3: UI Layout & Cloud Connection Setup
├── Implement left-nav tabs: Plan, Host, Market, and Code.
├── Hook file explorer & Monaco to read/write cloud files.
└── Embed wildcard preview tab pointing to *.preview.vibnai.com.
PHASE 4: Orchestrating the Cloud Agent Brain
├── Migrate TalkCody's parallel analyzer & Ralph Loop to vibn-agent-runner.
└── Enable full, autonomous environment management (Gitea, Sentry, Coolify API).
```

1
talkcody Submodule

Submodule talkcody added at 5543bf9264

View File

@@ -0,0 +1,226 @@
# 🚀 Vibn Marketing Attribution & Identity Sync SDK
A modular, highly scalable, and decoupled SDK package to link anonymous site traffic tracking (**Umami Analytics**) with registered user profiles (**Vibn Database**), providing comprehensive **First-Touch Marketing Attribution** and acquisition logs.
Developed originally for **Missinglettr** and structured specifically to be drop-in ready for the **Vibn Platform** architecture.
---
## 📁 Package Inventory
This package contains three decoupled layers designed to be copied directly into your respective repositories:
| Path | Purpose | Language / Framework |
| :--- | :--- | :--- |
| `frontend/VibnTracker.tsx` | Client-side search params listener, sessionStorage / Cookie persistence, automated Umami injection, and session identity bridging (`identify()`). | TypeScript / Next.js React |
| `backend/models.py` | Abstract Base Django Model adding UTM and Referrer fields to any User / Client record. | Python / Django |
| `backend/mixins.py` | Django REST Framework (DRF) interceptor mixin to auto-capture attribution variables from incoming signup payloads. | Python / Django REST Framework |
| `umami-bridge/umami_service.py` | Reusable bridge to pull aggregated pageviews, session timestamps, and raw click-trails from Umami (via REST API or raw DB joins). | Python / Django |
---
## 🏛️ Architecture Overview
The system bridges anonymous traffic with database records using a secure **Identity Exchange**:
```
[Anonymous Visitor Clicks Ad / Tweet]
[Lands on Website with UTMs & Referrer]
│ ├── Auto-logged in Umami (analytics.vibnai.com)
│ └── Cached client-side in sessionStorage & Cookie fallback
[Signs Up / Logs in via Google OAuth]
[Frontend POSTs Signup payload + UTM params to Backend]
[Django Backend saves user record + permanently binds UTM columns]
[Frontend calls useVibnTracker.identify(user_id)]
[Umami merges ALL previous anonymous browsing history with this new User ID!]
```
---
## 💻 Step-by-Step Implementation Guide
### 1. Frontend Integration (Vibn Next.js Web App)
Copy `frontend/VibnTracker.tsx` into your Next.js project (e.g., inside `components/metrics/VibnTracker.tsx`):
#### A. Wrap your Root Layout:
In `app/layout.tsx` (or your entry file), wrap the tree with `VibnTrackerProvider`. This will automatically inject your Umami tracking script and listen for UTM parameters across your landing and console domains:
```tsx
import { VibnTrackerProvider } from "@/components/metrics/VibnTracker";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<VibnTrackerProvider
umamiWebsiteId="your-umami-website-uuid"
umamiScriptUrl="https://analytics.vibnai.com/script.js"
>
<body>{children}</body>
</VibnTrackerProvider>
</html>
);
}
```
#### B. Capture & Send on Signup / Google OAuth exchange:
In your signup or Google OAuth exchange page (where the user completes registration), extract the cached attribution parameters using the `useVibnTracker` hook and send them in the request body to your backend:
```tsx
import { useVibnTracker } from "@/components/metrics/VibnTracker";
export default function GoogleCallbackPage() {
const { getStoredAttribution, identify, track } = useVibnTracker();
const handleOAuthSignup = async (googleToken: string) => {
// 1. Get first-touch parameters cached in browser
const attribution = getStoredAttribution();
// 2. POST to your register endpoint
const response = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
google_token: googleToken,
...attribution // Sends utm_source, utm_medium, utm_campaign, referrer, etc.
})
});
if (response.ok) {
const user = await response.json();
// 3. BRIDGING IDENTITY: Link anonymous browser history with real user ID!
identify({
userId: user.id, // Primary key in your database
email: user.email,
name: user.name,
plan: user.plan
});
// 4. Track custom sign-up conversion in Umami
track("user_completed_signup", { plan: user.plan });
}
};
// ...
}
```
---
### 2. Backend Integration (Vibn Django API)
Copy `backend/models.py` and `backend/mixins.py` into your Django apps.
#### A. Inherit from the Abstract Model:
Add first-touch marketing attribution columns to your active User or APIClient model by inheriting from `VibnAbstractAttributionModel`:
```python
# your_app/models.py
from backend.models import VibnAbstractAttributionModel
class UserProfile(VibnAbstractAttributionModel):
# Your existing fields
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
class Meta:
db_table = "user_profile"
```
Then generate and apply your database migrations:
```bash
python manage.py makemigrations
python manage.py migrate
```
#### B. Intercept requests with the DRF View Mixin:
Add the `VibnAttributionCaptureMixin` to your Signup or Authentication API ViewSet. It will automatically intercept, extract, and write the UTM parameters into the saving model:
```python
# your_app/views.py
from rest_framework import viewsets
from backend.mixins import VibnAttributionCaptureMixin
from .models import UserProfile
from .serializers import UserProfileSerializer
class RegisterViewSet(VibnAttributionCaptureMixin, viewsets.ModelViewSet):
"""
User registration API view.
VibnAttributionCaptureMixin automatically grabs utm_source, utm_campaign,
and Referrer headers from requests and binds them onto the UserProfile on save.
"""
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
```
---
### 3. Expose Live Traffic Trails in Admin Consoles
Copy `umami-bridge/umami_service.py` into your Python services.
You can securely query stats directly from your Umami self-hosted database (read-only) inside your Admin panels, allowing you to render **actual pageviews, device breakdowns, and exact visitor session timelines** for any specific client.
#### A. Configure Umami DB Connection:
In Django's `settings.py`, register your Umami database:
```python
DATABASES = {
'default': {
# Your main application DB
},
'umami_db': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'umami_production_db',
'USER': 'readonly_user',
'PASSWORD': 'password',
'HOST': '34.19.250.135',
'PORT': '5432',
}
}
```
#### B. Fetch Live Session Data for Admin Console:
In your Admin API view (e.g. `admin_user_detail`), call the service to fetch raw click-trails:
```python
# your_admin_app/views.py
from rest_framework.response import Response
from umami_bridge.umami_service import VibnUmamiService
def get_admin_user_profile(request, user_id):
# Initialize service
umami = VibnUmamiService()
# 1. Fetch total web sessions, total pageviews, and first interaction timestamp
funnel = umami.get_aggregated_funnel_for_user(user_id, db_connection_name="umami_db")
# 2. Fetch the actual 50 most recent page clicks, devices, and browsers used by this client!
click_trail = umami.get_user_session_click_trail(user_id, db_connection_name="umami_db")
return Response({
"user_id": user_id,
"first_touch_web_funnel": funnel,
"live_click_trail": click_trail
})
```
---
## 🛡️ Best Practices & GDPR Compliance
1. **Self-Hosted Privacy:** Because Umami is hosted on your domain (`analytics.vibnai.com`), cookie and script blockades are minimized, and no data is shared with third parties (complying strictly with GDPR, CCPA, and PECR).
2. **First-Touch Preservation:** The mixin uses `if not client.utm_source:` checks on save, ensuring first-touch parameters (the exact ad or link that *originally* brought the user in) are preserved and never overwritten by subsequent organic logins.
3. **No Database Blockades:** The `umami_service.py` uses read-only connections and limits queries to `LIMIT 50` to make sure raw analytics querying never causes performance blocks on your main application database thread.

View File

@@ -0,0 +1,61 @@
import logging
from rest_framework import serializers
logger = logging.getLogger(__name__)
class VibnAttributionCaptureMixin:
"""
A Django REST Framework ViewSet/APIView mixin that automatically
extracts marketing attribution fields (UTMs, referrer) from incoming
request payloads or HTTP headers and binds them to the saving object.
Compatible with any Django REST Framework serializer.
"""
def extract_attribution_data(self, request):
"""
Extracts UTM and referrer keys from request data, falling back
to request headers if required.
"""
# Try request payload first
data = request.data or {}
utm_source = data.get("utm_source")
utm_medium = data.get("utm_medium")
utm_campaign = data.get("utm_campaign")
utm_content = data.get("utm_content")
utm_term = data.get("utm_term")
# Capture Referrer from payload or fallback to HTTP headers
referrer = data.get("referrer")
if not referrer:
referrer = request.META.get("HTTP_REFERER")
return {
"utm_source": utm_source,
"utm_medium": utm_medium,
"utm_campaign": utm_campaign,
"utm_content": utm_content,
"utm_term": utm_term,
"referrer": referrer,
}
def perform_create(self, serializer):
"""
In DRF ViewSets, overrides perform_create to automatically inject
attribution fields directly into the saved model instance.
"""
attrib_data = self.extract_attribution_data(self.request)
# Save serializer with attribution params injected as overrides
instance = serializer.save(
**{k: v for k, v in attrib_data.items() if v is not None}
)
logger.info(
f"VibnTracker: Saved attribution data to {instance.__class__.__name__} "
f"(Source: {attrib_data['utm_source']}, Campaign: {attrib_data['utm_campaign']})"
)
return instance

View File

@@ -0,0 +1,164 @@
from django.db import models
class VibnAbstractAttributionModel(models.Model):
"""
An abstract Django model that adds standard marketing attribution
(first-touch UTM parameters & referrers) to any model (e.g. User, APIClient, Customer).
To implement:
class APIClient(VibnAbstractAttributionModel):
name = models.CharField(max_length=255)
# ... other fields ...
"""
utm_source = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Marketing channel source (e.g. twitter, google, newsletter)",
)
utm_medium = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Marketing medium (e.g. cpc, social, organic, email)",
)
utm_campaign = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Marketing campaign name (e.g. mcp-launch, spring-discount)",
)
utm_content = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Specific content links or ad identifiers clicked",
)
utm_term = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Keywords searched or paid terms clicked",
)
referrer = models.CharField(
max_length=1000,
null=True,
blank=True,
help_text="First-touch browser referrer URL (e.g. https://t.co/, https://google.com)",
)
class Meta:
abstract = True
def save_attribution(
self,
utm_source,
utm_medium=None,
utm_campaign=None,
utm_content=None,
utm_term=None,
referrer=None,
overwrite=False,
):
"""
Helper method to securely save marketing attribution data if it hasn't been set yet.
"""
if overwrite or not self.utm_source:
self.utm_source = utm_source
self.utm_medium = utm_medium
self.utm_campaign = utm_campaign
self.utm_content = utm_content
self.utm_term = utm_term
self.referrer = referrer
self.save(
update_fields=[
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"utm_term",
"referrer",
]
if self.pk
else None
)
class VibnPageview(models.Model):
"""
Vibn Pageview Tracking Model.
Tracks active user interactions and page loads in real-time,
providing fully dynamic traffic data and click timelines.
"""
user = models.ForeignKey(
"UserProfile", # Replace with your custom User model name
on_delete=models.CASCADE,
related_name="pageviews",
null=True,
blank=True,
)
session_id = models.CharField(
max_length=255, db_index=True, help_text="Unique browser session tracker ID"
)
url_path = models.CharField(
max_length=500, db_index=True, help_text="Tracked page path, e.g. /pricing"
)
referrer = models.CharField(
max_length=1000, null=True, blank=True, help_text="Traffic referrer URL"
)
# Captured UTM variables
utm_source = models.CharField(max_length=255, null=True, blank=True)
utm_medium = models.CharField(max_length=255, null=True, blank=True)
utm_campaign = models.CharField(max_length=255, null=True, blank=True)
utm_content = models.CharField(max_length=255, null=True, blank=True)
utm_term = models.CharField(max_length=255, null=True, blank=True)
# Device/Geo details
device = models.CharField(max_length=100, default="desktop")
browser = models.CharField(max_length=100, default="chrome")
country = models.CharField(max_length=100, default="US")
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
db_table = "vibn_pageview"
verbose_name = "Vibn Pageview"
verbose_name_plural = "Vibn Pageviews"
ordering = ["-created_at"]
class VibnEvent(models.Model):
"""
Vibn Product Event Tracking Model.
Tracks custom user conversion events (e.g. signup, connect_channel, create_campaign)
and binds them to their sessions and profiles.
"""
user = models.ForeignKey(
"UserProfile", # Replace with your custom User model name
on_delete=models.CASCADE,
related_name="events",
null=True,
blank=True,
)
session_id = models.CharField(max_length=255, db_index=True)
event_name = models.CharField(
max_length=255,
db_index=True,
help_text="Custom event name, e.g. connect_social",
)
properties = models.JSONField(
default=dict, blank=True, help_text="Custom event properties JSON"
)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
db_table = "vibn_event"
verbose_name = "Vibn Event"
verbose_name_plural = "Vibn Events"
ordering = ["-created_at"]

View File

@@ -0,0 +1,185 @@
"use client";
import React, { useEffect, createContext, useContext } from "react";
// ── TYPES & INTERFACES ────────────────────────────────────────────────────────
export interface UTMParams {
utm_source: string | null;
utm_medium: string | null;
utm_campaign: string | null;
utm_content: string | null;
utm_term: string | null;
referrer: string | null;
}
export interface IdentifyTraits {
userId: string | number;
email: string;
name?: string;
plan?: string;
[key: string]: any;
}
export interface VibnTrackerContextType {
getStoredAttribution: () => UTMParams;
clearAttribution: () => void;
identify: (traits: IdentifyTraits) => void;
track: (eventName: string, properties?: Record<string, any>) => void;
}
declare global {
interface Window {
umami?: {
identify: (traits: Record<string, any>) => void;
track: (eventName: string, properties?: Record<string, any>) => void;
};
}
}
// ── UTILITY FUNCTIONS ─────────────────────────────────────────────────────────
const COOKIE_EXPIRY_DAYS = 30;
function setCookie(name: string, value: string, days: number) {
if (typeof document === "undefined") return;
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + (value || "") + expires + "; path=/; SameSite=Lax";
}
function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function deleteCookie(name: string) {
if (typeof document === "undefined") return;
document.cookie = name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
}
// ── CONTEXT PROVIDER ──────────────────────────────────────────────────────────
const VibnTrackerContext = createContext<VibnTrackerContextType | undefined>(undefined);
export function VibnTrackerProvider({
children,
umamiWebsiteId,
umamiScriptUrl = "https://analytics.vibnai.com/script.js",
}: {
children: React.ReactNode;
umamiWebsiteId?: string;
umamiScriptUrl?: string;
}) {
useEffect(() => {
if (typeof window === "undefined") return;
// 1. Extract UTMs from active search string
const urlParams = new URLSearchParams(window.location.search);
const keys: Array<keyof UTMParams> = [
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"utm_term",
];
keys.forEach((key) => {
const val = urlParams.get(key);
if (val) {
// Persist to sessionStorage (for current window lifespans)
sessionStorage.setItem(key, val);
// Persist to cookie (cross-session backup, used if user closes window and signs up later)
setCookie(`vibn_${key}`, val, COOKIE_EXPIRY_DAYS);
}
});
// 2. Persist browser referrer
if (!sessionStorage.getItem("referrer") && !getCookie("vibn_referrer")) {
const ref = document.referrer || "direct";
sessionStorage.setItem("referrer", ref);
setCookie("vibn_referrer", ref, COOKIE_EXPIRY_DAYS);
}
// 3. Inject Umami analytics script automatically if website ID provided
if (umamiWebsiteId && !document.querySelector(`script[data-website-id="${umamiWebsiteId}"]`)) {
const script = document.createElement("script");
script.src = umamiScriptUrl;
script.setAttribute("data-website-id", umamiWebsiteId);
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
}, [umamiWebsiteId, umamiScriptUrl]);
const getStoredAttribution = (): UTMParams => {
if (typeof window === "undefined") {
return {
utm_source: null,
utm_medium: null,
utm_campaign: null,
utm_content: null,
utm_term: null,
referrer: null,
};
}
return {
utm_source: sessionStorage.getItem("utm_source") || getCookie("vibn_utm_source"),
utm_medium: sessionStorage.getItem("utm_medium") || getCookie("vibn_utm_medium"),
utm_campaign: sessionStorage.getItem("utm_campaign") || getCookie("vibn_utm_campaign"),
utm_content: sessionStorage.getItem("utm_content") || getCookie("vibn_utm_content"),
utm_term: sessionStorage.getItem("utm_term") || getCookie("vibn_utm_term"),
referrer: sessionStorage.getItem("referrer") || getCookie("vibn_referrer"),
};
};
const clearAttribution = () => {
if (typeof window === "undefined") return;
const keys = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "referrer"];
keys.forEach((key) => {
sessionStorage.removeItem(key);
deleteCookie(`vibn_${key}`);
});
};
const identify = (traits: IdentifyTraits) => {
if (typeof window !== "undefined" && window.umami) {
window.umami.identify(traits);
console.log(`[VibnTracker] User identified in Umami: ${traits.email}`);
}
};
const track = (eventName: string, properties?: Record<string, any>) => {
if (typeof window !== "undefined" && window.umami) {
window.umami.track(eventName, properties);
console.log(`[VibnTracker] Event tracked: "${eventName}"`, properties || "");
}
};
return (
<VibnTrackerContext.Provider
value={{ getStoredAttribution, clearAttribution, identify, track }}
>
{children}
</VibnTrackerContext.Provider>
);
}
// ── CUSTOM HOOK ───────────────────────────────────────────────────────────────
export function useVibnTracker() {
const context = useContext(VibnTrackerContext);
if (!context) {
throw new Error("useVibnTracker must be used within a VibnTrackerProvider");
}
return context;
}

View File

@@ -0,0 +1,139 @@
import logging
import requests
from django.db import connections
from django.utils import timezone
logger = logging.getLogger(__name__)
class VibnUmamiService:
"""
Vibn Umami Bridge Service.
Supports querying Umami analytics via either its official REST API or
performing direct, high-performance database JOINs against your self-hosted Postgres tables.
"""
def __init__(self, api_url=None, token=None, website_id=None):
self.api_url = api_url or "https://analytics.vibnai.com/api"
self.token = token
self.website_id = website_id
# ── DRIVER 1: REST API INTEGRATION ────────────────────────────────────────
def _get_headers(self):
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
}
def fetch_website_stats(self, start_at, end_at):
"""
Query website aggregated statistics (pageviews, visitors, bounce_rate)
via Umami REST API.
"""
if not self.website_id or not self.token:
logger.warning("Umami API connection details missing.")
return None
url = f"{self.api_url}/websites/{self.website_id}/stats"
params = {
"startAt": int(start_at.timestamp() * 1000),
"endAt": int(end_at.timestamp() * 1000),
}
try:
response = requests.get(
url, headers=self._get_headers(), params=params, timeout=10
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to fetch Umami API stats: {e}")
return None
# ── DRIVER 2: DIRECT POSTGRESQL READ-ONLY QUERIES ─────────────────────────
def get_user_session_click_trail(self, user_id, db_connection_name="umami_db"):
"""
Retrieves raw browsing timeline for a identified user directly from
the self-hosted Umami DB instance using Django cross-database routing.
"""
if db_connection_name not in connections:
logger.warning(
f"Database connection '{db_connection_name}' is not configured."
)
return []
query = """
SELECT
we.created_at,
we.event_name,
we.url_path,
we.url_query,
s.device,
s.browser,
s.country
FROM website_event we
JOIN session s ON we.session_id = s.session_id
WHERE s.user_id = %s
ORDER BY we.created_at DESC
LIMIT 50;
"""
try:
with connections[db_connection_name].cursor() as cursor:
cursor.execute(query, [str(user_id)])
rows = cursor.fetchall()
trail = []
for r in rows:
trail.append(
{
"timestamp": r[0].isoformat()
if hasattr(r[0], "isoformat")
else r[0],
"event": r[1],
"path": r[2],
"query": r[3],
"device": r[4],
"browser": r[5],
"country": r[6],
}
)
return trail
except Exception as e:
logger.error(f"Direct Umami DB query failed: {e}")
return []
def get_aggregated_funnel_for_user(self, user_id, db_connection_name="umami_db"):
"""
Retrieves high-level counts for pageviews, unique sessions, and first-seen dates
directly from the raw Umami tables.
"""
if db_connection_name not in connections:
return None
query = """
SELECT
COUNT(*),
COUNT(DISTINCT we.session_id),
MIN(we.created_at)
FROM website_event we
JOIN session s ON we.session_id = s.session_id
WHERE s.user_id = %s;
"""
try:
with connections[db_connection_name].cursor() as cursor:
cursor.execute(query, [str(user_id)])
row = cursor.fetchone()
if row:
return {
"total_interactions": row[0],
"total_sessions": row[1],
"first_seen_at": row[2].isoformat()
if hasattr(row[2], "isoformat")
else row[2],
}
return None
except Exception as e:
logger.error(f"Failed to fetch aggregated user funnel: {e}")
return None

1
vibn-code Submodule

Submodule vibn-code added at 5543bf9264

View File

@@ -0,0 +1,46 @@
/**
* GET /api/auth/me
*
* Exposes a profile endpoint for the VibnCode desktop app.
* Resolves the Bearer vibn_sk_... Workspace API key, queries the database,
* and returns the corresponding owner's user details.
*/
import { NextResponse } from "next/server";
import { requireWorkspacePrincipal } from "@/lib/auth/workspace-auth";
import { queryOne } from "@/lib/db-postgres";
export async function GET(request: Request) {
// 1. Authenticate the Workspace API key or Browser Session
const principal = await requireWorkspacePrincipal(request);
if (principal instanceof NextResponse) return principal;
try {
// 2. Query user details from fs_users
const user = await queryOne<{
id: string;
data: any;
}>(
`SELECT id, data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// 3. Return user profile compatible with the desktop client's User expectations
return NextResponse.json({
user: {
id: user.id,
name: user.data?.name || user.data?.display_name || "Vibn Owner",
email: user.data?.email || "",
photoUrl: user.data?.image || user.data?.photo_url || null,
workspace: principal.workspace.slug,
}
});
} catch (err) {
console.error("[api/auth/me GET]", err);
return NextResponse.json({ error: "Failed to resolve profile" }, { status: 500 });
}
}

View File

@@ -0,0 +1,61 @@
/**
* GET /api/auth/token
*
* Secure endpoint called by the browser during desktop SSO.
* Verifies the user has a valid NextAuth browser session, resolves or mints
* a Workspace API key, and returns it.
*/
import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { queryOne } from "@/lib/db-postgres";
import { getWorkspaceByOwner } from "@/lib/workspaces";
import { mintWorkspaceApiKey, listWorkspaceApiKeys, revealWorkspaceApiKey } from "@/lib/auth/workspace-auth";
export async function GET() {
// 1. Verify caller has an active NextAuth browser session cookie
const session = await authSession();
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Fetch the corresponding Postgres user
const user = await queryOne<{ id: string }>(
`SELECT id FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
[session.user.email]
);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 401 });
}
// 3. Get the user's active workspace
const workspace = await getWorkspaceByOwner(user.id);
if (!workspace) {
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
}
try {
// 4. Try to reuse their existing, active workspace API key to avoid key bloating
const keys = await listWorkspaceApiKeys(workspace.id);
const activeKey = keys.find((k) => !k.revoked_at);
if (activeKey) {
const revealed = await revealWorkspaceApiKey(workspace.id, activeKey.id);
if (revealed) {
return NextResponse.json({ token: revealed.token });
}
}
// 5. Otherwise, mint a fresh key for the desktop client
const minted = await mintWorkspaceApiKey({
workspaceId: workspace.id,
name: "VibnCode Desktop SSO",
createdBy: user.id,
});
return NextResponse.json({ token: minted.token });
} catch (err) {
console.error("[api/auth/token GET]", err);
return NextResponse.json({ error: "Failed to resolve workspace token" }, { status: 500 });
}
}

View File

@@ -21,14 +21,39 @@ function AuthPageInner() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [ssoProcessing, setSsoProcessing] = React.useState(false);
useEffect(() => { useEffect(() => {
if (status === "authenticated" && session?.user?.email) { if (status === "authenticated" && session?.user?.email) {
const isVibnCodeSSO = searchParams?.get("vibncode") === "true";
if (isVibnCodeSSO) {
setSsoProcessing(true);
// Call our new secure token endpoint
fetch("/api/auth/token")
.then((r) => r.json())
.then((data) => {
if (data.token) {
// Deep-link redirect back to the VibnCode desktop app
window.location.href = `vibncode://auth/callback?token=${data.token}`;
} else {
console.error("SSO Token missing from response", data);
setSsoProcessing(false);
}
})
.catch((err) => {
console.error("Desktop SSO failed:", err);
setSsoProcessing(false);
});
return;
}
const workspace = deriveWorkspace(session.user.email); const workspace = deriveWorkspace(session.user.email);
// Check if user has projects. If 0, go to onboarding, else go to projects. // Check if user has projects. If 0, go to onboarding, else go to projects.
fetch("/api/projects") fetch("/api/projects")
.then(r => r.json()) .then((r) => r.json())
.then(d => { .then((d) => {
if (d.projects && d.projects.length > 0) { if (d.projects && d.projects.length > 0) {
router.push(`/${workspace}/projects`); router.push(`/${workspace}/projects`);
} else { } else {
@@ -39,7 +64,7 @@ function AuthPageInner() {
} }
}, [status, session, router, searchParams]); }, [status, session, router, searchParams]);
if (status === "loading") { if (status === "loading" || ssoProcessing) {
return ( return (
<div <div
className="new-site-wrapper" className="new-site-wrapper"
@@ -80,7 +105,9 @@ function AuthPageInner() {
textTransform: "uppercase", textTransform: "uppercase",
}} }}
> >
Checking session {ssoProcessing
? "Authorizing VibnCode Desktop..."
: "Checking session"}
</div> </div>
</div> </div>
</div> </div>