Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useEffect } from 'react';
'use client';

import React, { useEffect, useState } from 'react';
import { AppMode } from './types';
import { useAppStore } from './store';
import { useAuth } from './contexts/AuthContext';
import Header from './components/Header';
import ModelingView from './components/ModelingView';
import AnalyzingView from './components/AnalyzingView';
Expand All @@ -9,8 +12,15 @@ import ModelListView from './components/ModelListView';
import CreateNewModelModal from './components/CreateNewModelModal';
import HistoryPanel from './components/HistoryPanel';
import ConversationalAnalyst from './components/ConversationalAnalyst';
import { AuthPage } from './components/auth/AuthPage';
import { Scratchpad } from './components/Scratchpad';
import { AnalysisHistory } from './components/AnalysisHistory';

const App: React.FC = () => {
const { user, isLoading: isAuthLoading, isAuthenticated } = useAuth();
const [isScratchpadOpen, setIsScratchpadOpen] = useState(false);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);

const {
model,
mode,
Expand All @@ -25,8 +35,14 @@ const App: React.FC = () => {
// Runs once on mount
useEffect(() => {
initializeTheme();
fetchAvailableModels();
}, []); // Remove dependencies to prevent infinite loops
}, []);

// Fetch models when authenticated
useEffect(() => {
if (isAuthenticated) {
fetchAvailableModels();
}
}, [isAuthenticated]);

const renderModelContent = () => {
if (!model) {
Expand All @@ -45,12 +61,27 @@ const App: React.FC = () => {
}
};

// Show auth loading state
if (isAuthLoading) {
return (
<div className="flex justify-center items-center h-screen bg-tandt-bg dark:bg-gray-900">
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-tandt-primary"></div>
<span className="ml-4 text-lg text-gray-500 dark:text-gray-400">Loading...</span>
</div>
);
}

// Show auth page if not authenticated
if (!isAuthenticated) {
return <AuthPage />;
}

// Show full-screen loader only on initial application load
if (isLoading && availableModels.length === 0 && !model) {
return (
<div className="flex justify-center items-center h-screen bg-tandt-bg dark:bg-gray-900">
<div className="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-tandt-primary"></div>
<span className="ml-4 text-lg text-gray-500 dark:text-gray-400">Loading...</span>
<span className="ml-4 text-lg text-gray-500 dark:text-gray-400">Loading models...</span>
</div>
);
}
Expand All @@ -64,12 +95,30 @@ const App: React.FC = () => {
</>
) : (
<>
<Header />
<Header
onOpenScratchpad={() => setIsScratchpadOpen(true)}
onOpenHistory={() => setIsHistoryOpen(true)}
/>
<main className="p-4 sm:p-6 lg:p-8">
{renderModelContent()}
</main>
<HistoryPanel />
{isChatAnalystOpen && <ConversationalAnalyst />}

{/* Scratchpad panel */}
<Scratchpad
modelId={model.Idug}
isOpen={isScratchpadOpen}
onClose={() => setIsScratchpadOpen(false)}
/>

{/* Analysis History panel */}
<AnalysisHistory
modelId={model.Idug}
currentElementsData={model.Model}
isOpen={isHistoryOpen}
onClose={() => setIsHistoryOpen(false)}
/>
</>
)}
</div>
Expand Down
7 changes: 7 additions & 0 deletions KINSHIP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# KINSHIP

## repo `jgwill/veritas` cloned in: `/workspace/repos/jgwill/veritas`

* Next major version migrated of this repo
* Should probably become where we will work

67 changes: 67 additions & 0 deletions app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { neon } from '@neondatabase/serverless'
import { NextResponse } from 'next/server'

const sql = neon(process.env.DATABASE_URL!)

function generateToken(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let token = ''
for (let i = 0; i < 64; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length))
}
return token
}

export async function POST(request: Request) {
try {
const { email, password } = await request.json()

if (!email || !password) {
return NextResponse.json(
{ error: 'Email and password are required' },
{ status: 400 }
)
}

// Verify credentials
const users = await sql`
SELECT id, email, display_name
FROM users
WHERE email = ${email.toLowerCase()}
AND password_hash = crypt(${password}, password_hash)
`

if (users.length === 0) {
return NextResponse.json(
{ error: 'Invalid email or password' },
{ status: 401 }
)
}

const user = users[0]

// Create new session
const token = generateToken()
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days

await sql`
INSERT INTO sessions (user_id, token, expires_at)
VALUES (${user.id}, ${token}, ${expiresAt.toISOString()})
`

return NextResponse.json({
user: {
id: user.id,
email: user.email,
display_name: user.display_name
},
token
})
} catch (error) {
console.error('Login error:', error)
return NextResponse.json(
{ error: 'Login failed. Please try again.' },
{ status: 500 }
)
}
}
27 changes: 27 additions & 0 deletions app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { neon } from '@neondatabase/serverless'
import { NextResponse } from 'next/server'

const sql = neon(process.env.DATABASE_URL!)

export async function POST(request: Request) {
try {
const authHeader = request.headers.get('Authorization')

if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7)

// Delete the session
await sql`
DELETE FROM sessions WHERE token = ${token}
`
}

return NextResponse.json({ success: true })
} catch (error) {
console.error('Logout error:', error)
return NextResponse.json(
{ error: 'Logout failed' },
{ status: 500 }
)
}
}
78 changes: 78 additions & 0 deletions app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { neon } from '@neondatabase/serverless'
import { NextResponse } from 'next/server'

const sql = neon(process.env.DATABASE_URL!)

function generateToken(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let token = ''
for (let i = 0; i < 64; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length))
}
return token
}

export async function POST(request: Request) {
try {
const { email, password, displayName } = await request.json()

if (!email || !password) {
return NextResponse.json(
{ error: 'Email and password are required' },
{ status: 400 }
)
}

if (password.length < 6) {
return NextResponse.json(
{ error: 'Password must be at least 6 characters' },
{ status: 400 }
)
}

// Check if user already exists
const existingUsers = await sql`
SELECT id FROM users WHERE email = ${email.toLowerCase()}
`

if (existingUsers.length > 0) {
return NextResponse.json(
{ error: 'An account with this email already exists' },
{ status: 409 }
)
}

// Create user with hashed password
const users = await sql`
INSERT INTO users (email, password_hash, display_name)
VALUES (${email.toLowerCase()}, crypt(${password}, gen_salt('bf')), ${displayName || null})
RETURNING id, email, display_name, created_at
`

const user = users[0]

// Create session
const token = generateToken()
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days

await sql`
INSERT INTO sessions (user_id, token, expires_at)
VALUES (${user.id}, ${token}, ${expiresAt.toISOString()})
`

return NextResponse.json({
user: {
id: user.id,
email: user.email,
display_name: user.display_name
},
token
})
} catch (error) {
console.error('Registration error:', error)
return NextResponse.json(
{ error: 'Registration failed. Please try again.' },
{ status: 500 }
)
}
}
50 changes: 50 additions & 0 deletions app/api/auth/validate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { neon } from '@neondatabase/serverless'
import { NextResponse } from 'next/server'

const sql = neon(process.env.DATABASE_URL!)

export async function GET(request: Request) {
try {
const authHeader = request.headers.get('Authorization')

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'No authorization token provided' },
{ status: 401 }
)
}

const token = authHeader.substring(7)

// Find valid session
const sessions = await sql`
SELECT s.user_id, u.id, u.email, u.display_name
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.token = ${token} AND s.expires_at > NOW()
`

if (sessions.length === 0) {
return NextResponse.json(
{ error: 'Invalid or expired session' },
{ status: 401 }
)
}

const user = sessions[0]

return NextResponse.json({
user: {
id: user.id,
email: user.email,
display_name: user.display_name
}
})
} catch (error) {
console.error('Session validation error:', error)
return NextResponse.json(
{ error: 'Session validation failed' },
{ status: 500 }
)
}
}
Loading