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:
@@ -21,42 +21,71 @@ interface LinkData {
|
||||
* GET /api/galaxy
|
||||
*
|
||||
* Fetches nodes with 3D coordinates and their links for visualization.
|
||||
* Automatically triggers graph calculation if needed.
|
||||
* Supports public viewing via ?user={did} parameter.
|
||||
* If no user parameter is provided and user is authenticated, shows their own galaxy.
|
||||
* If no user parameter and not authenticated, returns empty state with guidance.
|
||||
*/
|
||||
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;
|
||||
|
||||
if (!surrealJwt) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
// Determine which user's galaxy to show
|
||||
let userDid: string;
|
||||
let isOwnGalaxy = false;
|
||||
|
||||
// Verify JWT to get user's DID
|
||||
const userSession = verifySurrealJwt(surrealJwt);
|
||||
if (!userSession) {
|
||||
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
||||
if (targetUserDid) {
|
||||
// Viewing someone else's public galaxy
|
||||
userDid = targetUserDid;
|
||||
console.log(`[Galaxy API] Fetching public galaxy for user: ${userDid}`);
|
||||
} 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;
|
||||
console.log(`[Galaxy API] Fetching own galaxy for user: ${userDid}`);
|
||||
} else {
|
||||
// No target user and not authenticated - return empty galaxy with message
|
||||
console.log('[Galaxy API] No user specified and not authenticated');
|
||||
return NextResponse.json({
|
||||
nodes: [],
|
||||
links: [],
|
||||
message: 'Log in to view your galaxy, or visit a public galaxy via ?user={did}',
|
||||
});
|
||||
}
|
||||
|
||||
const { did: userDid } = userSession;
|
||||
|
||||
try {
|
||||
const db = await connectToDB();
|
||||
|
||||
// Fetch nodes that have 3D coordinates
|
||||
// When viewing own galaxy, show all nodes (including private)
|
||||
// When viewing public galaxy, only show nodes where is_public = true
|
||||
const nodesQuery = `
|
||||
SELECT id, title, body, user_did, atp_uri, coords_3d
|
||||
FROM node
|
||||
WHERE user_did = $userDid AND coords_3d != NONE
|
||||
WHERE user_did = $userDid
|
||||
AND coords_3d != NONE
|
||||
${isOwnGalaxy ? '' : 'AND is_public = true'}
|
||||
`;
|
||||
const nodeResults = await db.query<[NodeData[]]>(nodesQuery, { userDid });
|
||||
const nodes = nodeResults[0] || [];
|
||||
|
||||
// Fetch links between nodes
|
||||
// Fetch links between visible nodes
|
||||
// Extract node IDs for filtering links
|
||||
const nodeIds = nodes.map((n) => n.id);
|
||||
|
||||
// Only fetch links where both endpoints are in our visible nodes
|
||||
const linksQuery = `
|
||||
SELECT in, out
|
||||
FROM links_to
|
||||
WHERE in IN $nodeIds AND out IN $nodeIds
|
||||
`;
|
||||
const linkResults = await db.query<[LinkData[]]>(linksQuery);
|
||||
const linkResults = await db.query<[LinkData[]]>(linksQuery, { nodeIds });
|
||||
const links = linkResults[0] || [];
|
||||
|
||||
// Note: Coordinate calculation is now triggered automatically when nodes are created
|
||||
|
||||
@@ -95,18 +95,25 @@ export function ThoughtGalaxy() {
|
||||
const [nodes, setNodes] = useState<NodeData[]>([]);
|
||||
const [links, setLinks] = useState<LinkData[]>([]);
|
||||
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
|
||||
const [emptyMessage, setEmptyMessage] = useState<string | null>(null);
|
||||
const cameraControlsRef = useRef<CameraControls>(null);
|
||||
const hasFitCamera = useRef(false);
|
||||
const hasFocusedNode = useRef<string | null>(null);
|
||||
|
||||
// Get selectedNodeId from URL query params
|
||||
// Get query params
|
||||
const selectedNodeId = searchParams.get('node');
|
||||
const targetUserDid = searchParams.get('user'); // For viewing someone else's galaxy
|
||||
|
||||
// Fetch data from API on mount and poll for updates
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch('/api/galaxy', {
|
||||
// Build URL with optional user parameter
|
||||
const url = targetUserDid
|
||||
? `/api/galaxy?user=${encodeURIComponent(targetUserDid)}`
|
||||
: '/api/galaxy';
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include', // Include cookies for authentication
|
||||
});
|
||||
|
||||
@@ -119,13 +126,17 @@ export function ThoughtGalaxy() {
|
||||
|
||||
if (data.message) {
|
||||
console.log('[ThoughtGalaxy]', data.message);
|
||||
setEmptyMessage(data.message);
|
||||
// If calculating, poll again in 2 seconds
|
||||
setTimeout(fetchData, 2000);
|
||||
if (data.message.includes('calculating')) {
|
||||
setTimeout(fetchData, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setNodes(data.nodes || []);
|
||||
setLinks(data.links || []);
|
||||
setEmptyMessage(null);
|
||||
|
||||
console.log(`[ThoughtGalaxy] Loaded ${data.nodes?.length || 0} nodes and ${data.links?.length || 0} links`);
|
||||
} catch (error) {
|
||||
@@ -134,7 +145,7 @@ export function ThoughtGalaxy() {
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [targetUserDid]);
|
||||
|
||||
// Function to fit camera to all nodes
|
||||
const fitCameraToNodes = () => {
|
||||
@@ -286,17 +297,49 @@ export function ThoughtGalaxy() {
|
||||
return (
|
||||
<Stack align="center" justify="center" style={{ height: '100vh', width: '100vw' }}>
|
||||
<MantineText size="lg" c="dimmed">
|
||||
Create at least 3 nodes to visualize your thought galaxy
|
||||
</MantineText>
|
||||
<MantineText size="sm" c="dimmed">
|
||||
Nodes with content will automatically generate embeddings and 3D coordinates
|
||||
{emptyMessage || 'Create at least 3 nodes to visualize your thought galaxy'}
|
||||
</MantineText>
|
||||
{!emptyMessage && (
|
||||
<MantineText size="sm" c="dimmed">
|
||||
Nodes with content will automatically generate embeddings and 3D coordinates
|
||||
</MantineText>
|
||||
)}
|
||||
{targetUserDid && (
|
||||
<MantineText size="sm" c="dimmed" mt="xs">
|
||||
Viewing galaxy for user: {targetUserDid}
|
||||
</MantineText>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* User info banner when viewing someone else's galaxy */}
|
||||
{targetUserDid && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
right: '10px',
|
||||
zIndex: 999,
|
||||
maxWidth: '300px',
|
||||
}}
|
||||
>
|
||||
<Paper p="sm" radius="md" withBorder shadow="md">
|
||||
<MantineText size="sm" fw={600}>
|
||||
Public Galaxy
|
||||
</MantineText>
|
||||
<MantineText size="xs" c="dimmed">
|
||||
Viewing {nodes.length} public {nodes.length === 1 ? 'node' : 'nodes'}
|
||||
</MantineText>
|
||||
<MantineText size="xs" c="dimmed" style={{ wordBreak: 'break-all' }}>
|
||||
{targetUserDid}
|
||||
</MantineText>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Floating content overlay for selected node */}
|
||||
{selectedNode && (
|
||||
<Box
|
||||
|
||||
@@ -67,6 +67,11 @@ DEFINE FIELD coords_3d ON TABLE node TYPE option<array<number>>
|
||||
-- Must be NONE or a 3-point array [x, y, z].
|
||||
ASSERT $value = NONE OR array::len($value) = 3;
|
||||
|
||||
-- Privacy setting: Whether this node is publicly visible.
|
||||
-- Defaults to true (public by default, aligns with ATproto philosophy).
|
||||
-- When false, node is only visible to the owner.
|
||||
DEFINE FIELD is_public ON TABLE node TYPE bool DEFAULT true;
|
||||
|
||||
-- Define the vector search index.
|
||||
-- We use MTREE (or HNSW) for high-performance k-NN search.
|
||||
-- The dimension (3072) MUST match the output of the
|
||||
|
||||
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
|
||||
44
scripts/add-is-public-field.ts
Normal file
44
scripts/add-is-public-field.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import Surreal from 'surrealdb';
|
||||
|
||||
/**
|
||||
* Migration: Add is_public field to node table
|
||||
*
|
||||
* This script adds the is_public field to all existing nodes,
|
||||
* defaulting them to public (true) to align with the public-by-default
|
||||
* philosophy of ATproto.
|
||||
*/
|
||||
async function migrate() {
|
||||
const db = new Surreal();
|
||||
|
||||
try {
|
||||
await db.connect('ws://localhost:8000/rpc');
|
||||
await db.signin({ username: 'root', password: 'root' });
|
||||
await db.use({ namespace: 'ponderants', database: 'main' });
|
||||
|
||||
console.log('Adding is_public field definition to node table...');
|
||||
|
||||
// Define the field (should already be in schema.surql, but ensuring it's applied)
|
||||
await db.query(`
|
||||
DEFINE FIELD is_public ON TABLE node TYPE bool DEFAULT true;
|
||||
`);
|
||||
|
||||
console.log('✓ Field definition added');
|
||||
|
||||
console.log('Setting existing nodes to public...');
|
||||
|
||||
// Set all existing nodes where is_public is NONE to true (public by default)
|
||||
const result = await db.query(`
|
||||
UPDATE node SET is_public = true WHERE is_public = NONE;
|
||||
`);
|
||||
|
||||
console.log('Result:', result);
|
||||
console.log('✓ Migration complete: All existing nodes are now public by default');
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
133
tests/magnitude/03-public-galaxy.mag.ts
Normal file
133
tests/magnitude/03-public-galaxy.mag.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { test } from 'magnitude-test';
|
||||
|
||||
/**
|
||||
* Public Galaxy Viewing Tests
|
||||
*
|
||||
* These tests verify that:
|
||||
* 1. Galaxies can be viewed publicly via ?user={did} parameter
|
||||
* 2. Private nodes are hidden from public view
|
||||
* 3. Own galaxy requires authentication
|
||||
* 4. Invalid user DIDs are handled gracefully
|
||||
*/
|
||||
|
||||
test('Unauthenticated users can view public galaxies via user parameter', async (agent) => {
|
||||
// Navigate to a public galaxy using the user parameter
|
||||
// Note: This test assumes there's at least one user with public nodes
|
||||
await agent.act('Navigate to /galaxy?user=did:plc:example123');
|
||||
|
||||
// Check: The galaxy visualization should be displayed
|
||||
await agent.check('The 3D galaxy visualization is visible');
|
||||
|
||||
// Check: Public nodes should be visible
|
||||
await agent.check('At least one node sphere is visible in the 3D space');
|
||||
|
||||
// Check: A "Public Galaxy" banner should be shown
|
||||
await agent.check('A banner or indicator shows this is a public galaxy');
|
||||
|
||||
// Check: Can interact with nodes
|
||||
await agent.act('Click on a visible node sphere');
|
||||
await agent.check('A modal or panel displays the node title and content');
|
||||
});
|
||||
|
||||
test('Private nodes are not visible in public galaxy view', async (agent) => {
|
||||
// This test requires a user with both public and private nodes
|
||||
// Navigate to their public galaxy
|
||||
await agent.act('Navigate to /galaxy?user=did:plc:user-with-private-nodes');
|
||||
|
||||
// Check: Only public nodes are visible
|
||||
await agent.check('The galaxy shows only public nodes');
|
||||
|
||||
// Check: Private nodes are not visible in the visualization
|
||||
await agent.check('Private nodes are not rendered in the 3D space');
|
||||
});
|
||||
|
||||
test('Accessing own galaxy without user parameter shows authenticated galaxy', async (agent) => {
|
||||
// First, log in
|
||||
await agent.act('Navigate to /');
|
||||
await agent.act('Click the login button');
|
||||
await agent.act('Complete the Bluesky OAuth flow');
|
||||
|
||||
// Navigate to /galaxy without user parameter
|
||||
await agent.act('Navigate to /galaxy');
|
||||
|
||||
// Check: Should see own galaxy (including private nodes)
|
||||
await agent.check('The galaxy visualization shows all nodes including private ones');
|
||||
|
||||
// Check: No "Public Galaxy" banner should be shown (it's your own galaxy)
|
||||
await agent.check('There is no "Public Galaxy" banner visible');
|
||||
});
|
||||
|
||||
test('Accessing own galaxy without authentication shows empty state', async (agent) => {
|
||||
// Ensure user is logged out
|
||||
await agent.act('Navigate to /');
|
||||
await agent.act('If logged in, click logout');
|
||||
|
||||
// Navigate to /galaxy without user parameter
|
||||
await agent.act('Navigate to /galaxy');
|
||||
|
||||
// Check: Should see a message about logging in or viewing a public galaxy
|
||||
await agent.check('A message is displayed about logging in or visiting a public galaxy');
|
||||
|
||||
// Check: No nodes are visible
|
||||
await agent.check('No node spheres are visible in the visualization');
|
||||
});
|
||||
|
||||
test('Invalid user DID shows appropriate error or empty state', async (agent) => {
|
||||
// Navigate to galaxy with an invalid/non-existent user DID
|
||||
await agent.act('Navigate to /galaxy?user=did:plc:invalid-nonexistent-user');
|
||||
|
||||
// Check: Should handle gracefully (empty state or error message)
|
||||
await agent.check('An appropriate message is shown for invalid or empty galaxy');
|
||||
|
||||
// Check: No crash or loading spinner stuck
|
||||
await agent.check('The page is in a stable state (not stuck loading)');
|
||||
});
|
||||
|
||||
test('Public galaxy displays user information', async (agent) => {
|
||||
// Navigate to a public galaxy
|
||||
await agent.act('Navigate to /galaxy?user=did:plc:example123');
|
||||
|
||||
// Check: User DID or handle is displayed
|
||||
await agent.check('The user DID is visible in the UI');
|
||||
|
||||
// Check: Node count is displayed
|
||||
await agent.check('The number of public nodes is shown');
|
||||
});
|
||||
|
||||
test('Public galaxy node details show Bluesky link', async (agent) => {
|
||||
// Navigate to a public galaxy
|
||||
await agent.act('Navigate to /galaxy?user=did:plc:example123');
|
||||
|
||||
// Act: Click on a node
|
||||
await agent.act('Click on a visible node sphere');
|
||||
|
||||
// Check: Node details modal is shown
|
||||
await agent.check('A modal displays the node title and body');
|
||||
|
||||
// Check: "View on Bluesky" link is present
|
||||
await agent.check('A "View on Bluesky" link is visible');
|
||||
|
||||
// Check: Link points to Bluesky
|
||||
await agent.check('The link URL includes "bsky.app"');
|
||||
});
|
||||
|
||||
test('Can switch between own galaxy and public galaxy', async (agent) => {
|
||||
// Log in
|
||||
await agent.act('Navigate to /');
|
||||
await agent.act('Click the login button');
|
||||
await agent.act('Complete the Bluesky OAuth flow');
|
||||
|
||||
// View own galaxy
|
||||
await agent.act('Navigate to /galaxy');
|
||||
await agent.check('Own galaxy is displayed');
|
||||
|
||||
// Navigate to someone else's public galaxy
|
||||
await agent.act('Navigate to /galaxy?user=did:plc:other-user');
|
||||
await agent.check('The "Public Galaxy" banner is shown');
|
||||
await agent.check('A different set of nodes is visible');
|
||||
|
||||
// Navigate back to own galaxy
|
||||
await agent.act('Navigate to /galaxy');
|
||||
await agent.check('Own galaxy is displayed again');
|
||||
await agent.check('No "Public Galaxy" banner is shown');
|
||||
});
|
||||
Reference in New Issue
Block a user