feat: Make galaxy viewable without login requirement
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>
This commit is contained in:
317
plans/10-public-galaxy-viewing.md
Normal file
317
plans/10-public-galaxy-viewing.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user