Implemented public galaxy viewing feature that allows unauthenticated
users to view public thought galaxies via the ?user={did} parameter,
while maintaining privacy controls for node-level visibility.
Changes:
- Updated /api/galaxy/route.ts to support public access:
* Accept ?user={did} query parameter for viewing specific user's galaxy
* Show all nodes (including private) for authenticated user viewing own galaxy
* Filter to only public nodes when viewing someone else's galaxy
* Return empty state with helpful message when not authenticated
* Filter links to only show connections between visible nodes
- Added is_public field to database schema:
* Updated db/schema.surql with DEFAULT true (public by default)
* Created migration script scripts/add-is-public-field.ts
* Aligns with ATproto's public-by-default philosophy
- Enhanced ThoughtGalaxy component:
* Support viewing galaxies via ?user={did} parameter
* Display user info banner when viewing public galaxy
* Show appropriate empty state messages based on context
* Refetch data when user parameter changes
- Created comprehensive Magnitude tests:
* Test public galaxy viewing without authentication
* Verify private nodes are hidden from public view
* Test own galaxy access requires authentication
* Validate invalid user DID handling
* Test user info display and navigation between galaxies
- Documented implementation plan in plans/10-public-galaxy-viewing.md
This implements the "public by default" model while allowing future
node-level privacy controls. All canonical data remains on the user's
ATproto PDS, with SurrealDB serving as a high-performance cache.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
318 lines
8.9 KiB
Markdown
318 lines
8.9 KiB
Markdown
# Plan: Make Galaxy Viewable Without Login Requirement
|
|
|
|
## Status
|
|
- **Priority**: HIGH (User-requested)
|
|
- **Status**: In Progress
|
|
- **Created**: 2025-01-10
|
|
|
|
## Problem Statement
|
|
|
|
Currently, the galaxy visualization (`/galaxy`) requires user authentication via JWT cookie. This prevents:
|
|
1. Public sharing of thought galaxies
|
|
2. First-time visitors from seeing example galaxies
|
|
3. Social media link previews from working properly
|
|
4. Search engines from indexing public thought networks
|
|
|
|
The galaxy should be publicly viewable while still respecting user privacy preferences.
|
|
|
|
## Current Implementation Analysis
|
|
|
|
### Authentication Check
|
|
`app/api/galaxy/route.ts` (lines 27-38):
|
|
```typescript
|
|
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
|
|
|
if (!surrealJwt) {
|
|
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
}
|
|
|
|
const userSession = verifySurrealJwt(surrealJwt);
|
|
if (!userSession) {
|
|
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
|
}
|
|
```
|
|
|
|
### Data Query
|
|
Currently queries `WHERE user_did = $userDid` (line 49), showing only the authenticated user's nodes.
|
|
|
|
## Design Decisions
|
|
|
|
### Option 1: Public by Default (RECOMMENDED)
|
|
**All galaxies are publicly viewable**, but users can mark individual nodes as private.
|
|
|
|
**Pros:**
|
|
- Simple implementation
|
|
- Encourages public knowledge sharing
|
|
- Better for SEO and discovery
|
|
- Aligns with "decentralized social" vision
|
|
|
|
**Cons:**
|
|
- Users might accidentally share private thoughts
|
|
- Requires clear UI indicators for node visibility
|
|
|
|
**URL Structure:**
|
|
- `/galaxy` - current user's galaxy (if logged in) or landing page
|
|
- `/galaxy/{user_did}` or `/galaxy?user={user_did}` - specific user's public galaxy
|
|
|
|
### Option 2: Opt-in Public Galleries
|
|
Galaxies are private by default, users must explicitly make them public.
|
|
|
|
**Pros:**
|
|
- More privacy-conscious
|
|
- Users have full control
|
|
|
|
**Cons:**
|
|
- Reduces discovery and sharing
|
|
- More complex implementation
|
|
- Goes against ATproto's "public by default" philosophy
|
|
|
|
**Decision: We'll implement Option 1** - Public by default, with optional private nodes.
|
|
|
|
## Implementation Plan
|
|
|
|
### Phase 1: Update API to Support Public Access
|
|
|
|
#### 1.1 Modify `/api/galaxy/route.ts`
|
|
```typescript
|
|
export async function GET(request: NextRequest) {
|
|
const { searchParams } = new URL(request.url);
|
|
const targetUserDid = searchParams.get('user');
|
|
|
|
const cookieStore = await cookies();
|
|
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
|
|
|
// Determine which user's galaxy to show
|
|
let userDid: string;
|
|
let isOwnGalaxy = false;
|
|
|
|
if (targetUserDid) {
|
|
// Viewing someone else's public galaxy
|
|
userDid = targetUserDid;
|
|
} else if (surrealJwt) {
|
|
// Viewing own galaxy (authenticated)
|
|
const userSession = verifySurrealJwt(surrealJwt);
|
|
if (!userSession) {
|
|
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
|
}
|
|
userDid = userSession.did;
|
|
isOwnGalaxy = true;
|
|
} else {
|
|
// No target user and not authenticated - return empty galaxy with message
|
|
return NextResponse.json({
|
|
nodes: [],
|
|
links: [],
|
|
message: 'Log in to view your galaxy, or visit a public galaxy via ?user={did}'
|
|
});
|
|
}
|
|
|
|
// Query nodes
|
|
const nodesQuery = `
|
|
SELECT id, title, body, user_did, atp_uri, coords_3d
|
|
FROM node
|
|
WHERE user_did = $userDid
|
|
AND coords_3d != NONE
|
|
${isOwnGalaxy ? '' : 'AND is_public = true'}
|
|
`;
|
|
|
|
// ... rest of implementation
|
|
}
|
|
```
|
|
|
|
#### 1.2 Add `is_public` Field to Node Schema
|
|
- Default: `true` (public by default)
|
|
- Users can mark individual nodes as private
|
|
- Private nodes are only visible to the owner
|
|
|
|
### Phase 2: Update Database Schema
|
|
|
|
#### 2.1 Add `is_public` Column to `node` Table
|
|
```sql
|
|
DEFINE FIELD is_public ON TABLE node TYPE bool DEFAULT true;
|
|
```
|
|
|
|
#### 2.2 Create Migration Script
|
|
`scripts/add-is-public-field.ts`:
|
|
```typescript
|
|
import { connectToDB } from '@/lib/db';
|
|
|
|
async function migrate() {
|
|
const db = await connectToDB();
|
|
|
|
// Add field definition
|
|
await db.query(`
|
|
DEFINE FIELD is_public ON TABLE node TYPE bool DEFAULT true;
|
|
`);
|
|
|
|
// Set existing nodes to public
|
|
await db.query(`
|
|
UPDATE node SET is_public = true WHERE is_public = NONE;
|
|
`);
|
|
|
|
console.log('Migration complete: Added is_public field');
|
|
}
|
|
|
|
migrate();
|
|
```
|
|
|
|
### Phase 3: Update Frontend Components
|
|
|
|
#### 3.1 Update `ThoughtGalaxy.tsx`
|
|
```typescript
|
|
// Support viewing other users' galaxies
|
|
const { searchParams } = useSearchParams();
|
|
const targetUser = searchParams.get('user');
|
|
|
|
useEffect(() => {
|
|
async function fetchData() {
|
|
const url = targetUser
|
|
? `/api/galaxy?user=${targetUser}`
|
|
: '/api/galaxy';
|
|
|
|
const response = await fetch(url, {
|
|
credentials: 'include',
|
|
});
|
|
|
|
// ... rest of implementation
|
|
}
|
|
|
|
fetchData();
|
|
}, [targetUser]);
|
|
```
|
|
|
|
#### 3.2 Add User Info Display
|
|
When viewing another user's galaxy, show:
|
|
- User's Bluesky handle
|
|
- Link to their profile
|
|
- Number of public nodes
|
|
|
|
#### 3.3 Update `/galaxy` Page
|
|
Add support for URL parameter: `/galaxy?user=did:plc:xxxxx`
|
|
|
|
### Phase 4: Navigation & User Experience
|
|
|
|
#### 4.1 Landing Experience for Non-Authenticated Users
|
|
When visiting `/galaxy` without login:
|
|
- Show a sample/demo galaxy (could be a curated example)
|
|
- Display call-to-action: "Create your own thought galaxy"
|
|
- Provide login button
|
|
|
|
#### 4.2 Add "Share Galaxy" Feature
|
|
Add button to copy shareable link:
|
|
```typescript
|
|
const shareUrl = `${window.location.origin}/galaxy?user=${userDid}`;
|
|
```
|
|
|
|
### Phase 5: Privacy Controls (Future Enhancement)
|
|
|
|
#### 5.1 Node-Level Privacy Toggle
|
|
In node editor, add checkbox:
|
|
```typescript
|
|
<Checkbox
|
|
label="Make this node public"
|
|
checked={isPublic}
|
|
onChange={(e) => setIsPublic(e.currentTarget.checked)}
|
|
/>
|
|
```
|
|
|
|
#### 5.2 Bulk Privacy Management
|
|
Settings page to:
|
|
- Make all nodes private/public
|
|
- Set default for new nodes
|
|
- Filter and update specific nodes
|
|
|
|
## Security Considerations
|
|
|
|
### 1. Data Exposure
|
|
- **Risk**: Users accidentally share sensitive information
|
|
- **Mitigation**:
|
|
- Clear visual indicators for public/private nodes
|
|
- Confirmation dialog when publishing nodes
|
|
- Easy way to make nodes private retroactively
|
|
|
|
### 2. API Abuse
|
|
- **Risk**: Scraping or excessive requests to public galaxies
|
|
- **Mitigation**:
|
|
- Rate limiting on `/api/galaxy`
|
|
- Caching layer for public galaxies
|
|
- Consider CDN for popular galaxies
|
|
|
|
### 3. Privacy Violations
|
|
- **Risk**: Viewing history tracking or surveillance
|
|
- **Mitigation**:
|
|
- No analytics on public galaxy views
|
|
- No "who viewed my galaxy" feature
|
|
- Respect DNT headers
|
|
|
|
## Testing Plan
|
|
|
|
### Magnitude Tests
|
|
|
|
#### Test 1: Public Galaxy Viewing Without Auth
|
|
```typescript
|
|
test('Unauthenticated users can view public galaxies', async (agent) => {
|
|
await agent.act('Navigate to /galaxy?user=did:plc:example123');
|
|
await agent.check('The galaxy visualization is displayed');
|
|
await agent.check('Public nodes are visible');
|
|
await agent.act('Click on a public node');
|
|
await agent.check('Node details are displayed');
|
|
});
|
|
```
|
|
|
|
#### Test 2: Private Nodes Hidden from Public View
|
|
```typescript
|
|
test('Private nodes are not visible in public galaxy', async (agent) => {
|
|
// ... implementation
|
|
});
|
|
```
|
|
|
|
#### Test 3: Own Galaxy Requires Auth
|
|
```typescript
|
|
test('Accessing own galaxy without target user requires authentication', async (agent) => {
|
|
await agent.act('Navigate to /galaxy');
|
|
await agent.check('Login prompt or empty state is displayed');
|
|
});
|
|
```
|
|
|
|
### Manual Testing Checklist
|
|
- [ ] Visit `/galaxy` without login → see landing page
|
|
- [ ] Visit `/galaxy?user={valid_did}` → see public nodes
|
|
- [ ] Visit `/galaxy?user={invalid_did}` → see error message
|
|
- [ ] Log in and visit `/galaxy` → see own galaxy (including private nodes)
|
|
- [ ] Share galaxy link → recipient can view public nodes
|
|
- [ ] Mark node as private → confirm it disappears from public view
|
|
|
|
## Implementation Steps
|
|
|
|
1. **Create database migration** for `is_public` field
|
|
2. **Update API route** to support public access
|
|
3. **Update ThoughtGalaxy component** to handle URL parameters
|
|
4. **Add user info display** for public galaxies
|
|
5. **Test with manual checks**
|
|
6. **Write Magnitude tests**
|
|
7. **Update documentation**
|
|
8. **Create PR with changes**
|
|
|
|
## Acceptance Criteria
|
|
|
|
✅ Unauthenticated users can view public galaxies via `?user=` parameter
|
|
✅ Authenticated users see their own galaxy at `/galaxy` (no param)
|
|
✅ Private nodes are only visible to the owner
|
|
✅ Public nodes are visible to everyone
|
|
✅ Clear error messages for invalid user DIDs
|
|
✅ Shareable URLs work correctly
|
|
✅ All tests pass
|
|
|
|
## Notes
|
|
|
|
- This aligns with ATproto's philosophy of public-by-default, user-controlled data
|
|
- Future enhancement: Node-level privacy controls in UI
|
|
- Consider adding Open Graph meta tags for social media previews
|
|
- May want to add a "featured galaxies" page for discovery
|
|
|
|
## Related Files
|
|
|
|
- `app/api/galaxy/route.ts` - Galaxy API endpoint
|
|
- `components/ThoughtGalaxy.tsx` - 3D visualization component
|
|
- `app/galaxy/page.tsx` - Galaxy page component
|
|
- `lib/db/schema.surql` - Database schema
|