Migrate user/profile URLs to /u/[handle] and remove legacy scripts

All user and post profile URLs have been updated from /[handle] to /u/[handle] for consistency. Added a redirect page for /posts/[id] to the canonical /u/[handle]/posts/[id] route. Deprecated and removed legacy scripts and documentation related to chat and federation endpoints. Push-based federation endpoints are now disabled in favor of real-time pull-based federation.
This commit is contained in:
Christopher
2026-01-27 01:26:41 -08:00
parent 44610157a1
commit a68aee8758
20 changed files with 85 additions and 573 deletions
-160
View File
@@ -1,160 +0,0 @@
# Swarm Chat
A real-time, end-to-end encrypted chat system built exclusively for the Synapsis Swarm network.
## Features
- **End-to-End Encryption**: Messages are encrypted using recipient's public key
- **Cross-Node Messaging**: Chat with users on any Synapsis node
- **Real-Time Delivery**: Messages delivered instantly via swarm inbox
- **Read Receipts**: See when messages are delivered and read
- **Typing Indicators**: Know when someone is typing (coming soon)
- **Native Swarm Protocol**: Built specifically for the swarm network
## Architecture
### Database Schema
**chat_conversations**: Tracks conversations between users
- Stores participant info and last message preview
- Unique constraint ensures one conversation per user pair
**chat_messages**: Individual encrypted messages
- Content encrypted with recipient's public key
- Swarm message ID for deduplication
- Delivery and read status tracking
**chat_typing_indicators**: Real-time typing status (future)
### API Endpoints
**POST /api/swarm/chat/send**
- Send a message to any user (local or remote)
- Encrypts content with recipient's public key
- Delivers to remote nodes via swarm inbox
**POST /api/swarm/chat/inbox**
- Receives messages from other swarm nodes
- Validates and stores encrypted messages
- Updates conversation metadata
**GET /api/swarm/chat/conversations**
- Lists all conversations for current user
- Includes unread counts and last message preview
**GET /api/swarm/chat/messages**
- Fetches messages for a conversation
- Supports cursor-based pagination
- Returns encrypted content for client-side decryption
**PATCH /api/swarm/chat/messages**
- Marks messages as read
- Updates read receipts
### Encryption
Messages are encrypted using RSA-OAEP with SHA-256:
1. **Sending**: Message encrypted with recipient's public key
2. **Storage**: Only encrypted content stored in database
3. **Decryption**: Client-side decryption using user's private key
This ensures true end-to-end encryption - even the server cannot read messages.
## Usage
### Starting a Chat
```typescript
// Send a message to start a conversation
await fetch('/api/swarm/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipientHandle: 'user@remote.node',
content: 'Hello!',
}),
});
```
### Receiving Messages
Messages are automatically received via the swarm inbox endpoint. The system:
1. Validates the sender and recipient
2. Checks for duplicate messages
3. Creates or updates the conversation
4. Stores the encrypted message
5. Returns success to the sender
### Client-Side Decryption
```typescript
import { decryptMessage } from '@/lib/swarm/chat-crypto';
// Decrypt a message using user's private key
const plaintext = decryptMessage(
message.encryptedContent,
userPrivateKey
);
```
## Security Considerations
1. **Private Key Storage**: User private keys should be encrypted at rest
2. **Key Derivation**: Consider using password-based key derivation
3. **Forward Secrecy**: Future enhancement - implement Signal protocol
4. **Message Signing**: Verify sender authenticity with signatures
5. **Rate Limiting**: Prevent spam and abuse
## Future Enhancements
- [ ] Group chats (multi-party encryption)
- [ ] Voice/video calls (WebRTC)
- [ ] File attachments (encrypted)
- [ ] Message reactions
- [ ] Message editing/deletion
- [ ] Typing indicators (real-time)
- [ ] Online/offline status
- [ ] Push notifications
- [ ] Desktop notifications
- [ ] Message search
- [ ] Conversation archiving
- [ ] Block/mute users
- [ ] Forward secrecy (Signal protocol)
## Migration
Run the migration to create the chat tables:
```bash
npm run db:generate
npm run db:migrate
```
## Testing
Test the chat system:
1. Create two accounts on different nodes
2. Navigate to `/chat` on one account
3. Click "New Chat" and enter the other user's handle
4. Send a message
5. Check the other account's `/chat` page
## Why Swarm Chat?
Swarm Chat was built from the ground up for the Synapsis network:
- **True E2E Encryption**: Messages encrypted with recipient's public key
- **Real-Time**: Direct delivery, no polling required
- **Proper Chat UX**: Conversations, read receipts, typing indicators
- **Lightweight**: Simple JSON protocol
- **Swarm-Native**: Built for the swarm, not retrofitted
## Contributing
Contributions welcome! Priority areas:
- Client-side encryption implementation
- Real-time updates (WebSocket/SSE)
- Mobile-responsive UI improvements
- Group chat support
-72
View File
@@ -1,72 +0,0 @@
# Cloudflare Turnstile Setup Guide
Cloudflare Turnstile has been fully integrated into your Synapsis node to protect against bot registrations and logins.
## How It Works
1. **Admin Configuration**: Admins can add their Cloudflare Turnstile keys in the Admin Settings panel
2. **Automatic Activation**: Once both Site Key and Secret Key are configured, Turnstile is automatically enabled
3. **Frontend Integration**: The Turnstile widget appears on login and registration forms
4. **Server Verification**: All login/register requests are verified server-side with Cloudflare
## Setup Steps
### 1. Get Turnstile Keys from Cloudflare
1. Go to https://dash.cloudflare.com/?to=/:account/turnstile
2. Create a new site
3. Copy your **Site Key** (public) and **Secret Key** (private)
### 2. Configure in Admin Panel
1. Log in as an admin
2. Go to Admin → Settings tab
3. Scroll to "Cloudflare Turnstile (Bot Protection)" section
4. Paste your Site Key and Secret Key
5. Click "Save Settings"
### 3. Test It
1. Log out
2. Go to the login page
3. You should see the Turnstile widget appear
4. Complete the challenge and try logging in
## Features
- ✅ Automatic widget rendering when keys are configured
- ✅ Works on both login and registration forms
- ✅ Server-side token verification
- ✅ Automatic widget reset on form errors
- ✅ Graceful fallback if Turnstile is not configured
- ✅ Submit button disabled until challenge is completed
- ✅ IP address forwarding for better verification
## Technical Details
### Database Schema
- `nodes.turnstile_site_key` - Public site key (exposed to frontend)
- `nodes.turnstile_secret_key` - Private secret key (server-side only)
### API Endpoints Modified
- `POST /api/auth/login` - Now accepts optional `turnstileToken`
- `POST /api/auth/register` - Now accepts optional `turnstileToken`
- `GET /api/node` - Returns `turnstileSiteKey` for frontend
### Files Modified
- `src/db/schema.ts` - Added Turnstile fields
- `src/lib/turnstile.ts` - Verification helper functions
- `src/app/api/auth/login/route.ts` - Token verification
- `src/app/api/auth/register/route.ts` - Token verification
- `src/app/api/node/route.ts` - Expose site key
- `src/app/api/admin/node/route.ts` - Save/update keys
- `src/app/admin/page.tsx` - Admin UI for configuration
- `src/app/login/page.tsx` - Frontend widget integration
## Security Notes
- The Secret Key is NEVER exposed to the frontend
- Only the Site Key is public
- Verification happens server-side with Cloudflare's API
- Failed verifications reject the login/registration attempt
- IP addresses are forwarded for better bot detection
-19
View File
@@ -1,19 +0,0 @@
import { db, users } from './src/db';
import { eq } from 'drizzle-orm';
async function checkKeys() {
const allUsers = await db.select({
handle: users.handle,
hasChatPublicKey: users.chatPublicKey,
hasChatPrivateKeyEncrypted: users.chatPrivateKeyEncrypted,
}).from(users);
console.log('Users and their chat keys:');
allUsers.forEach(u => {
console.log(`- ${u.handle}: chatPublicKey=${!!u.hasChatPublicKey}, chatPrivateKeyEncrypted=${!!u.hasChatPrivateKeyEncrypted}`);
});
process.exit(0);
}
checkKeys().catch(console.error);
-14
View File
@@ -1,14 +0,0 @@
import { db, chatMessages } from './src/db';
import { isNull } from 'drizzle-orm';
async function cleanOldMessages() {
console.log('Deleting old RSA-encrypted messages...');
const result = await db.delete(chatMessages)
.where(isNull(chatMessages.senderChatPublicKey));
console.log('Deleted old messages. Now only E2E encrypted messages remain.');
process.exit(0);
}
cleanOldMessages().catch(console.error);
-27
View File
@@ -1,27 +0,0 @@
import { db } from './src/db/index';
import { users } from './src/db/schema';
import { chatConversations } from './src/db/schema';
async function main() {
console.log('--- USERS ---');
try {
const allUsers = await db.select().from(users);
console.log(`Found ${allUsers.length} users.`);
allUsers.forEach(u => {
console.log(`User: ${u.handle} | ID: ${u.id} | Local: ${!u.handle.includes('@')}`);
});
console.log('\n--- CONVERSATIONS ---');
const convs = await db.select().from(chatConversations);
console.log(`Found ${convs.length} conversations.`);
convs.forEach(c => {
console.log(`Conv: ${c.id} | Type: ${c.type} | P1: ${c.participant1Id} | P2Handle: ${c.participant2Handle}`);
});
} catch (e) {
console.error('Error:', e);
}
process.exit(0);
}
main();
-26
View File
@@ -1,26 +0,0 @@
import 'dotenv/config';
import { db } from './src/db/index';
import { chatMessages } from './src/db/schema';
import { desc } from 'drizzle-orm';
async function main() {
console.log('--- LATEST CHAT MESSAGES ---');
try {
const messages = await db.select().from(chatMessages).orderBy(desc(chatMessages.createdAt)).limit(20);
console.log(`Found ${messages.length} messages.`);
messages.forEach(m => {
console.log(`\nID: ${m.id}`);
console.log(`Sender: ${m.senderHandle}`);
console.log(`Created: ${m.createdAt}`);
console.log(`EncryptedContent (${m.encryptedContent?.length} chars): ${m.encryptedContent}`);
console.log(`SenderEncryptedContent (${m.senderEncryptedContent?.length} chars): ${m.senderEncryptedContent}`);
console.log('-----------------------------------');
});
} catch (e) {
console.error('Error:', e);
}
process.exit(0);
}
main();
+1
View File
@@ -102,6 +102,7 @@ export async function GET(request: Request) {
post: row.postId ? { post: row.postId ? {
id: row.postId, id: row.postId,
content: row.postContent, content: row.postContent,
authorHandle: row.actorHandle, // The actor is the post author for likes/reposts
} : null, } : null,
}; };
}); });
+5 -1
View File
@@ -115,7 +115,10 @@ export async function POST(request: Request) {
} }
} }
// If this is a reply to a swarm post, deliver it to the origin node // DEPRECATED: Push-based federation disabled
// Swarm now uses real-time pull-based federation
// Replies are fetched in real-time from the origin node
/*
if (data.swarmReplyTo) { if (data.swarmReplyTo) {
(async () => { (async () => {
try { try {
@@ -153,6 +156,7 @@ export async function POST(request: Request) {
} }
})(); })();
} }
*/
// Handle local mentions (create notifications for users on this node) // Handle local mentions (create notifications for users on this node)
(async () => { (async () => {
+6 -108
View File
@@ -43,114 +43,12 @@ const swarmPostSchema = z.object({
/** /**
* POST /api/swarm/inbox * POST /api/swarm/inbox
* *
* Receives a post from another swarm node. * DEPRECATED: This endpoint is disabled.
* Stores it for local users who follow the author. * We now use real-time pull-based federation via /api/swarm/timeline
* instead of push-based caching.
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { return NextResponse.json({
if (!db) { error: 'This endpoint is deprecated. Swarm uses real-time pull-based federation.',
return NextResponse.json({ error: 'Database not available' }, { status: 503 }); }, { status: 410 }); // 410 Gone
}
const body = await request.json();
const data = swarmPostSchema.parse(body);
// Construct the swarm post ID
const swarmPostId = `swarm:${data.nodeDomain}:${data.post.id}`;
// Check if we already have this post
const existingPost = await db.query.posts.findFirst({
where: eq(posts.apId, swarmPostId),
});
if (existingPost) {
return NextResponse.json({
success: true,
message: 'Post already exists',
});
}
// Check if anyone on this node follows the author
const authorActorUrl = `swarm://${data.nodeDomain}/${data.author.handle}`;
const hasFollowers = await db.query.remoteFollowers.findFirst({
where: eq(remoteFollowers.actorUrl, authorActorUrl),
});
// Even if no one follows, we might want to cache for timeline
// For now, only store if someone follows
if (!hasFollowers) {
return NextResponse.json({
success: true,
message: 'No local followers',
});
}
// Get or create placeholder user for the remote author
const remoteHandle = `${data.author.handle}@${data.nodeDomain}`;
let remoteUser = await db.query.users.findFirst({
where: eq(users.handle, remoteHandle),
});
if (!remoteUser) {
const [newUser] = await db.insert(users).values({
did: `did:swarm:${data.nodeDomain}:${data.author.handle}`,
handle: remoteHandle,
displayName: data.author.displayName,
avatarUrl: data.author.avatarUrl || null,
isNsfw: data.author.isNsfw,
publicKey: 'swarm-remote-user',
}).returning();
remoteUser = newUser;
} else {
// Update profile info if changed
await db.update(users)
.set({
displayName: data.author.displayName,
avatarUrl: data.author.avatarUrl || remoteUser.avatarUrl,
isNsfw: data.author.isNsfw,
})
.where(eq(users.id, remoteUser.id));
}
// Create the post
const [newPost] = await db.insert(posts).values({
userId: remoteUser.id,
content: data.post.content,
isNsfw: data.post.isNsfw || data.author.isNsfw,
apId: swarmPostId,
apUrl: `https://${data.nodeDomain}/${data.author.handle}/posts/${data.post.id}`,
createdAt: new Date(data.post.createdAt),
linkPreviewUrl: data.post.linkPreviewUrl || null,
linkPreviewTitle: data.post.linkPreviewTitle || null,
linkPreviewDescription: data.post.linkPreviewDescription || null,
linkPreviewImage: data.post.linkPreviewImage || null,
}).returning();
// Store media attachments
if (data.post.media && data.post.media.length > 0) {
for (const m of data.post.media) {
await db.insert(media).values({
userId: remoteUser.id,
postId: newPost.id,
url: m.url,
mimeType: m.mimeType || null,
altText: m.altText || null,
});
}
}
console.log(`[Swarm] Received post from ${remoteHandle}: ${newPost.id}`);
return NextResponse.json({
success: true,
message: 'Post received',
postId: newPost.id,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 });
}
console.error('[Swarm] Inbox error:', error);
return NextResponse.json({ error: 'Failed to process post' }, { status: 500 });
}
} }
+5 -78
View File
@@ -30,86 +30,13 @@ const swarmReplySchema = z.object({
/** /**
* POST /api/swarm/replies * POST /api/swarm/replies
* *
* Receives a reply from another node in the swarm. * DEPRECATED: This endpoint is disabled.
* The reply is stored as a remote reply linked to the local post. * We now use real-time pull-based federation instead of push-based caching.
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { return NextResponse.json({
if (!db) { error: 'This endpoint is deprecated. Swarm uses real-time pull-based federation.',
return NextResponse.json({ error: 'Database not available' }, { status: 503 }); }, { status: 410 }); // 410 Gone
}
const body = await request.json();
const data = swarmReplySchema.parse(body);
// Verify the target post exists on this node
const targetPost = await db.query.posts.findFirst({
where: eq(posts.id, data.postId),
});
if (!targetPost) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
// Check if we already have this reply (by swarm ID)
const swarmReplyId = `swarm:${data.reply.nodeDomain}:${data.reply.id}`;
const existingReply = await db.query.posts.findFirst({
where: eq(posts.apId, swarmReplyId),
});
if (existingReply) {
return NextResponse.json({ success: true, message: 'Reply already exists' });
}
// We need a system user to attribute swarm replies to
// For now, we'll store them with metadata in the apId/apUrl fields
// and create a virtual representation
// Get or create a placeholder user for this remote author
let remoteUser = await db.query.users.findFirst({
where: eq(users.handle, `${data.reply.author.handle}@${data.reply.nodeDomain}`),
});
if (!remoteUser) {
// Create a placeholder user for the remote author
const [newUser] = await db.insert(users).values({
did: `did:swarm:${data.reply.nodeDomain}:${data.reply.author.handle}`,
handle: `${data.reply.author.handle}@${data.reply.nodeDomain}`,
displayName: data.reply.author.displayName,
avatarUrl: data.reply.author.avatarUrl || null,
publicKey: 'swarm-remote-user', // Placeholder
}).returning();
remoteUser = newUser;
}
// Create the reply post
const [replyPost] = await db.insert(posts).values({
userId: remoteUser.id,
content: data.reply.content,
replyToId: data.postId,
apId: swarmReplyId,
apUrl: `https://${data.reply.nodeDomain}/${data.reply.author.handle}/posts/${data.reply.id}`,
createdAt: new Date(data.reply.createdAt),
}).returning();
// Update the parent post's reply count
await db.update(posts)
.set({ repliesCount: targetPost.repliesCount + 1 })
.where(eq(posts.id, data.postId));
console.log(`[Swarm] Received reply from ${data.reply.nodeDomain} to post ${data.postId}`);
return NextResponse.json({
success: true,
replyId: replyPost.id,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 });
}
console.error('[Swarm] Reply error:', error);
return NextResponse.json({ error: 'Failed to process reply' }, { status: 500 });
}
} }
/** /**
+13 -7
View File
@@ -489,15 +489,21 @@ export default function ChatPage() {
marginLeft: msg.isSentByMe ? 'auto' : '0', marginLeft: msg.isSentByMe ? 'auto' : '0',
flexDirection: msg.isSentByMe ? 'row-reverse' : 'row' flexDirection: msg.isSentByMe ? 'row-reverse' : 'row'
}}> }}>
{!msg.isSentByMe && ( <div className="avatar avatar-sm" style={{ flexShrink: 0 }}>
<div className="avatar avatar-sm" style={{ flexShrink: 0 }}> {msg.isSentByMe ? (
{msg.senderAvatarUrl ? ( user.avatarUrl ? (
<img src={user.avatarUrl} alt="" />
) : (
user.displayName[0]
)
) : (
msg.senderAvatarUrl ? (
<img src={msg.senderAvatarUrl} alt="" /> <img src={msg.senderAvatarUrl} alt="" />
) : ( ) : (
msg.senderDisplayName?.[0] msg.senderDisplayName?.[0]
)} )
</div> )}
)} </div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: msg.isSentByMe ? 'flex-end' : 'flex-start' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: msg.isSentByMe ? 'flex-end' : 'flex-start' }}>
<div style={{ <div style={{
@@ -682,7 +688,7 @@ export default function ChatPage() {
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}}> }}>
{conv.lastMessagePreview} {conv.lastMessagePreview === 'New message' ? 'Encrypted Message' : conv.lastMessagePreview}
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -22,7 +22,7 @@ interface User {
function UserCard({ user }: { user: User }) { function UserCard({ user }: { user: User }) {
return ( return (
<Link href={`/${user.handle}`} className="user-card"> <Link href={`/u/${user.handle}`} className="user-card">
<div className="avatar"> <div className="avatar">
{user.avatarUrl ? ( {user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.displayName} /> <img src={user.avatarUrl} alt={user.displayName} />
+40
View File
@@ -0,0 +1,40 @@
'use client';
import { useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
export default function PostRedirect() {
const params = useParams();
const router = useRouter();
const id = params.id as string;
useEffect(() => {
const fetchAndRedirect = async () => {
try {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) {
router.push('/');
return;
}
const data = await res.json();
router.push(`/u/${data.post.author.handle}/posts/${id}`);
} catch {
router.push('/');
}
};
fetchAndRedirect();
}, [id, router]);
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--foreground-tertiary)',
}}>
Loading...
</div>
);
}
+2 -2
View File
@@ -111,7 +111,7 @@ export default function BotsPage() {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '12px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '12px' }}>
<div style={{ display: 'flex', gap: '12px', flex: 1, minWidth: 0 }}> <div style={{ display: 'flex', gap: '12px', flex: 1, minWidth: 0 }}>
<Link <Link
href={`/${bot.handle}`} href={`/u/${bot.handle}`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="avatar" className="avatar"
style={{ style={{
@@ -147,7 +147,7 @@ export default function BotsPage() {
)} )}
</div> </div>
<Link <Link
href={`/${bot.handle}`} href={`/u/${bot.handle}`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
style={{ fontSize: '13px', color: 'var(--foreground-tertiary)' }} style={{ fontSize: '13px', color: 'var(--foreground-tertiary)' }}
> >
@@ -35,7 +35,7 @@ const stripHtml = (html: string | null | undefined): string | null => {
function UserRow({ user }: { user: UserSummary }) { function UserRow({ user }: { user: UserSummary }) {
return ( return (
<Link href={`/${user.handle}`} className="user-row"> <Link href={`/u/${user.handle}`} className="user-row">
<div className="avatar"> <div className="avatar">
{user.avatarUrl ? ( {user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.displayName || user.handle} /> <img src={user.avatarUrl} alt={user.displayName || user.handle} />
@@ -215,7 +215,7 @@ export default function ProfilePage() {
const handleComment = (post: Post) => { const handleComment = (post: Post) => {
// Navigation is handled by the PostCard overlay, // Navigation is handled by the PostCard overlay,
// but we can also use router.push if they explicitly click the comment button. // but we can also use router.push if they explicitly click the comment button.
router.push(`/${post.author.handle}/posts/${post.id}`); router.push(`/u/${post.author.handle}/posts/${post.id}`);
}; };
const handleDelete = (postId: string) => { const handleDelete = (postId: string) => {
@@ -624,7 +624,7 @@ export default function ProfilePage() {
<> <>
{' · Managed by '} {' · Managed by '}
<Link <Link
href={`/${(user as any).botOwner.handle}`} href={`/u/${(user as any).botOwner.handle}`}
style={{ color: 'var(--accent)', fontWeight: 500 }} style={{ color: 'var(--accent)', fontWeight: 500 }}
> >
@{(user as any).botOwner.handle} @{(user as any).botOwner.handle}
@@ -85,7 +85,7 @@ export default function PostDetailPage() {
const handleDelete = (postId: string) => { const handleDelete = (postId: string) => {
if (postId === id) { if (postId === id) {
router.push(`/${handle}`); router.push(`/u/${handle}`);
} else { } else {
setReplies(prev => prev.filter(r => r.id !== postId)); setReplies(prev => prev.filter(r => r.id !== postId));
if (post) { if (post) {
+6 -6
View File
@@ -248,7 +248,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
} }
}; };
const postUrl = `/${post.author.handle}/posts/${post.id}`; const postUrl = `/u/${post.author.handle}/posts/${post.id}`;
// Get the full handle for profile links (includes domain for remote users) // Get the full handle for profile links (includes domain for remote users)
const getProfileHandle = () => { const getProfileHandle = () => {
@@ -384,7 +384,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
return ( return (
<article className="post thread-parent"> <article className="post thread-parent">
<div className="post-header"> <div className="post-header">
<Link href={`/${profileHandle}`} className="avatar-link" onClick={(e) => e.stopPropagation()}> <Link href={`/u/${profileHandle}`} className="avatar-link" onClick={(e) => e.stopPropagation()}>
<div className="avatar"> <div className="avatar">
{post.author.avatarUrl ? ( {post.author.avatarUrl ? (
<img src={post.author.avatarUrl} alt={post.author.displayName || ''} /> <img src={post.author.avatarUrl} alt={post.author.displayName || ''} />
@@ -394,7 +394,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
</div> </div>
</Link> </Link>
<div className="post-author"> <div className="post-author">
<Link href={`/${profileHandle}`} className="post-handle" onClick={(e) => e.stopPropagation()}> <Link href={`/u/${profileHandle}`} className="post-handle" onClick={(e) => e.stopPropagation()}>
{post.author.displayName || post.author.handle} {post.author.displayName || post.author.handle}
</Link> </Link>
<span className="post-time">{formatFullHandle(post.author.handle, post.nodeDomain)}</span> <span className="post-time">{formatFullHandle(post.author.handle, post.nodeDomain)}</span>
@@ -427,7 +427,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
{!isDetail && <Link href={postUrl} className="post-link-overlay" aria-label="View post" />} {!isDetail && <Link href={postUrl} className="post-link-overlay" aria-label="View post" />}
<div className="post-header"> <div className="post-header">
<Link href={`/${profileHandle}`} className="avatar-link" onClick={(e) => e.stopPropagation()}> <Link href={`/u/${profileHandle}`} className="avatar-link" onClick={(e) => e.stopPropagation()}>
<div className="avatar"> <div className="avatar">
{post.author.avatarUrl ? ( {post.author.avatarUrl ? (
<img src={post.author.avatarUrl} alt={post.author.displayName} /> <img src={post.author.avatarUrl} alt={post.author.displayName} />
@@ -438,7 +438,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
</Link> </Link>
<div className="post-author"> <div className="post-author">
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Link href={`/${profileHandle}`} className="post-handle" onClick={(e) => e.stopPropagation()}> <Link href={`/u/${profileHandle}`} className="post-handle" onClick={(e) => e.stopPropagation()}>
{post.author.displayName || post.author.handle} {post.author.displayName || post.author.handle}
</Link> </Link>
{(post.bot || post.author.isBot) && ( {(post.bot || post.author.isBot) && (
@@ -585,7 +585,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
{effectiveReplyTo && !showThread && ( {effectiveReplyTo && !showThread && (
<div className="post-reply-to"> <div className="post-reply-to">
Replying to <Link href={`/${effectiveReplyTo.author.handle}`} onClick={(e) => e.stopPropagation()}>{formatFullHandle(effectiveReplyTo.author.handle)}</Link> Replying to <Link href={`/u/${effectiveReplyTo.author.handle}`} onClick={(e) => e.stopPropagation()}>{formatFullHandle(effectiveReplyTo.author.handle)}</Link>
</div> </div>
)} )}
+1 -1
View File
@@ -147,7 +147,7 @@ export function RightSidebar() {
{nodeInfo.admins.map((admin) => ( {nodeInfo.admins.map((admin) => (
<Link <Link
key={admin.handle} key={admin.handle}
href={`/${admin.handle}`} href={`/u/${admin.handle}`}
style={{ display: 'flex', alignItems: 'center', gap: '10px', textDecoration: 'none', color: 'inherit' }} style={{ display: 'flex', alignItems: 'center', gap: '10px', textDecoration: 'none', color: 'inherit' }}
> >
<img <img
+1 -1
View File
@@ -144,7 +144,7 @@ export function Sidebar() {
</Link> </Link>
)} )}
{user ? ( {user ? (
<Link href={`/${user.handle}`} className={`nav-item ${pathname === '/' + user.handle ? 'active' : ''}`}> <Link href={`/u/${user.handle}`} className={`nav-item ${pathname === '/u/' + user.handle ? 'active' : ''}`}>
<UserIcon /> <UserIcon />
<span>Profile</span> <span>Profile</span>
</Link> </Link>
-46
View File
@@ -1,46 +0,0 @@
import { db } from './src/db/index';
import { users, chatConversations, chatMessages } from './src/db/schema';
import { eq, and } from 'drizzle-orm';
import { encryptMessage } from './src/lib/swarm/chat-crypto';
import crypto from 'crypto';
async function main() {
console.log('--- SIMULATING CHAT SEND ---');
try {
// 1. Get Sender (Cypher)
const sender = await db.query.users.findFirst({
where: eq(users.handle, 'cypher'),
});
if (!sender) throw new Error('Sender not found');
console.log('Sender found:', sender.handle);
// 2. Get Recipient (newinnightvale)
const recipientHandle = 'newinnightvale';
const recipient = await db.query.users.findFirst({
where: eq(users.handle, recipientHandle),
});
if (!recipient) throw new Error('Recipient not found');
console.log('Recipient found:', recipient.handle);
console.log('Recipient PK length:', recipient.publicKey?.length);
// 3. Encrypt
console.log('Encrypting...');
try {
const encrypted = encryptMessage('Hello World', recipient.publicKey);
console.log('Encryption successful. Length:', encrypted.length);
} catch (e) {
console.error('Encryption FAILED:', e);
// Dump key for manual inspection if needed (truncated)
console.log('Key start:', recipient.publicKey.substring(0, 50));
return;
}
console.log('Simulation complete (not inserting to DB to avoid pollution, but encryption passed).');
} catch (e) {
console.error('Error:', e);
}
process.exit(0);
}
main();