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
3 changes: 3 additions & 0 deletions ts-langchain/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ next-env.d.ts
.vscode
# LangGraph API
.langgraph_api

# Bun
bun.lock
5 changes: 4 additions & 1 deletion ts-langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"node": ">=18"
},
"dependencies": {
"@auth0/ai-langchain": "^4.1.0",
"@auth0/ai-langchain": "^5.0.0",
"@auth0/ai": "^6.0.0",
"@auth0/nextjs-auth0": "^4.13.0",
"@langchain/community": "^0.3.53",
"@langchain/core": "^0.3.72",
Expand All @@ -39,6 +40,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-slot": "^1.2.0",
"@slack/web-api": "^7.9.3",
"@types/pg": "^8.15.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -56,6 +58,7 @@
"next": "15.2.4",
"next-themes": "^0.4.4",
"nuqs": "^2.4.3",
"octokit": "^5.0.3",
"pdf-parse": "^1.1.1",
"pg": "^8.16.3",
"postgres": "^3.4.5",
Expand Down
4 changes: 2 additions & 2 deletions ts-langchain/src/app/api/chat/[..._path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { initApiPassthrough } from 'langgraph-nextjs-api-passthrough';

import { getAccessToken, getUser } from '@/lib/auth0';


export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } = initApiPassthrough({
apiUrl: process.env.LANGGRAPH_API_URL,
baseRoute: 'chat/',
Expand All @@ -20,12 +19,13 @@ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, runtime } = initApiPassth
configurable: {
_credentials: {
user: await getUser(),
}
},
},
},
};
}

return body;
},
disableWarningLog: true,
});
35 changes: 35 additions & 0 deletions ts-langchain/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Suspense } from 'react';
import { redirect } from 'next/navigation';
import { Loader2 } from 'lucide-react';

import { auth0 } from '@/lib/auth0';
import ProfileContent from '@/components/auth0/profile/profile-content';

export default async function ProfilePage() {
const session = await auth0.getSession();

if (!session || !session.user) {
redirect('/auth/login');
}

return (
<div className="min-h-full bg-white/5">
<div className="max-w-4xl mx-auto p-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Profile</h1>
<p className="text-white/70">Manage your connected accounts</p>
</div>

<Suspense
fallback={
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-white/60" />
</div>
}
>
<ProfileContent user={session.user} />
</Suspense>
</div>
</div>
);
}
142 changes: 142 additions & 0 deletions ts-langchain/src/components/auth0/profile/connected-accounts-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use client';

import { UserPlus, Loader2, ExternalLink, Trash2 } from 'lucide-react';
import { ConnectedAccount, deleteConnectedAccount } from '@/lib/actions/profile';
import { format } from 'date-fns';
import { useState } from 'react';

interface ConnectedAccountsCardProps {
connectedAccounts: ConnectedAccount[];
loading: boolean;
onAccountDeleted?: () => void;
}

export default function ConnectedAccountsCard({
connectedAccounts,
loading,
onAccountDeleted,
}: ConnectedAccountsCardProps) {
const [deletingId, setDeletingId] = useState<string | null>(null);

const handleDelete = async (accountId: string) => {
if (!confirm('Are you sure you want to delete this connected account?')) {
return;
}

setDeletingId(accountId);
try {
const result = await deleteConnectedAccount(accountId);
if (result.success) {
// Refresh the list
onAccountDeleted?.();
} else {
alert(`Failed to delete account: ${result.error}`);
}
} catch (error) {
alert('An error occurred while deleting the account');
} finally {
setDeletingId(null);
}
};
return (
<div className="bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-white">Connected Accounts</h2>
<span className="text-sm text-white/60">{connectedAccounts.length} connected</span>
</div>

{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-white/60" />
</div>
) : (
<div className="space-y-4">
{/* Current Linked Accounts */}
{connectedAccounts.length > 0 ? (
<div className="space-y-3">
{connectedAccounts.map((account) => {
return (
<div
key={account.id}
className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/10"
>
<div className="flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-white">{account.connection}</p>
<button
onClick={() => handleDelete(account.id)}
disabled={deletingId === account.id}
className="ml-4 p-2 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Delete connected account"
>
{deletingId === account.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
</button>
</div>
<div className="flex items-center justify-between mb-2">
<div className="flex gap-4 text-xs text-white/60">
{account.created_at && (
<span>Created: {format(new Date(account.created_at), 'dd-MMM-yy HH:mm')}</span>
)}
{account.expires_at && (
<span>Expires: {format(new Date(account.expires_at), 'dd-MMM-yy HH:mm')}</span>
)}
</div>
</div>
{account.scopes && account.scopes.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-white/60">Scopes:</span>
<div className="flex flex-wrap gap-1.5">
{account.scopes.map((scope) => (
<span
key={scope}
className="text-xs bg-white/10 px-2 py-0.5 rounded text-white/80 border border-white/5 truncate max-w-[250px]"
title={scope}
>
{scope}
</span>
))}
</div>
</div>
)}
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8">
<UserPlus className="h-12 w-12 text-white/40 mx-auto mb-3" />
<p className="text-white/60">No additional accounts connected</p>
</div>
)}

{/* Information Box */}
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 mt-6">
<div className="flex items-start space-x-3">
<ExternalLink className="h-5 w-5 text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="text-blue-100 font-medium mb-1">
<a
href="https://auth0.com/ai/docs/intro/token-vault#what-is-connected-accounts-for-token-vault"
target="_blank"
rel="noopener noreferrer"
>
Connected Accounts
</a>
</p>
<p className="text-blue-200/80 text-xs leading-relaxed">
Connect social accounts to sign in with multiple providers using the same profile. Your primary
account cannot be unlinked.
</p>
</div>
</div>
</div>
</div>
)}
</div>
);
}
50 changes: 50 additions & 0 deletions ts-langchain/src/components/auth0/profile/profile-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import { useState, useEffect } from 'react';

import UserInfoCard from './user-info-card';
import ConnectedAccountsCard from './connected-accounts-card';
import { ConnectedAccount, fetchConnectedAccounts } from '@/lib/actions/profile';

interface KeyValueMap {
[key: string]: any;
}

export default function ProfileContent({ user }: { user: KeyValueMap }) {
const [connectedAccounts, setConnectedAccounts] = useState<ConnectedAccount[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
loadConnectedAccounts();
}, []);

const loadConnectedAccounts = async () => {
try {
const accounts = await fetchConnectedAccounts();
console.log('Fetched Linked Accounts:', accounts);
setConnectedAccounts(accounts);
} catch (error) {
console.error('Error fetching linked accounts:', error);
} finally {
setLoading(false);
}
};

return (
<div className="grid grid-cols-2 lg:grid-cols-2 gap-6">
{/* User Info Card */}
<div className="lg:col-span-1">
<UserInfoCard user={user} />
</div>

{/* Linked Accounts Card */}
<div className="lg:col-span-1">
<ConnectedAccountsCard
connectedAccounts={connectedAccounts}
loading={loading}
onAccountDeleted={loadConnectedAccounts}
/>
</div>
</div>
);
}
104 changes: 104 additions & 0 deletions ts-langchain/src/components/auth0/profile/user-info-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { User, Mail, Globe, Shield } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';

interface KeyValueMap {
[key: string]: any;
}

function getAvatarFallback(user: KeyValueMap) {
const givenName = user.given_name;
const familyName = user.family_name;
const nickname = user.nickname;
const name = user.name;

if (givenName && familyName) {
return `${givenName[0]}${familyName[0]}`;
}

if (nickname) {
return nickname[0];
}

return name?.[0] || 'U';
}

export default function UserInfoCard({ user }: { user: KeyValueMap }) {
return (
<div className="bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 p-6">
<div className="flex flex-col items-center space-y-4">
{/* Avatar */}
<Avatar className="h-24 w-24">
<AvatarImage src={user.picture} alt={user.name} />
<AvatarFallback className="text-2xl bg-primary text-primary-foreground">
{getAvatarFallback(user)}
</AvatarFallback>
</Avatar>

{/* Basic Info */}
<div className="text-center space-y-2">
<h2 className="text-2xl font-semibold text-white">{user.name || user.nickname || 'User'}</h2>
{user.email && (
<p className="text-white/70 flex items-center gap-2 justify-center">
<Mail className="h-4 w-4" />
{user.email}
{user.email_verified && (
<span title="Verified">
<Shield className="h-4 w-4 text-green-400" />
</span>
)}
</p>
)}
</div>
</div>

{/* Detailed Information */}
<div className="mt-6 space-y-4">
<div className="border-t border-white/20 pt-4">
<h3 className="text-lg font-medium text-white mb-3">Account Details</h3>

<div className="space-y-3 text-sm">
{user.sub && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-white/60" />
<span className="text-white/80">User ID:</span>
<span className="text-white">{user.sub}</span>
</div>
)}

{user.given_name && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-white/60" />
<span className="text-white/80">First Name:</span>
<span className="text-white">{user.given_name}</span>
</div>
)}

{user.family_name && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-white/60" />
<span className="text-white/80">Last Name:</span>
<span className="text-white">{user.family_name}</span>
</div>
)}

{user.nickname && (
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-white/60" />
<span className="text-white/80">Nickname:</span>
<span className="text-white">{user.nickname}</span>
</div>
)}

{user.org_id && (
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-white/60" />
<span className="text-white/80">Organization ID:</span>
<span className="text-white">{user.org_id}</span>
</div>
)}
</div>
</div>
</div>
</div>
);
}
Loading