Switch IPFS from standalone kubo to rSpace collab-server endpoints

- Update IPFS_API_URL to ipfs-api.rspace.online (was kubo:5001)
- Update IPFS_GATEWAY_URL to ipfs.rspace.online (was ipfs.jeffemmett.com)
- Enhance image button: file upload via /api/uploads with IPFS encryption
  instead of manual URL prompt, with fallback on failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-31 18:53:27 -07:00
parent f71f9e6303
commit b55362032d
3 changed files with 45 additions and 11 deletions

View File

@ -12,10 +12,10 @@ services:
- INFISICAL_ENV=prod
- INFISICAL_URL=http://infisical:8080
- DATABASE_URL=postgresql://rnotes:${DB_PASSWORD}@rnotes-postgres:5432/rnotes
# IPFS integration (encrypted file storage)
# IPFS integration (encrypted file storage via rSpace collab-server kubo)
- IPFS_ENABLED=true
- IPFS_API_URL=http://kubo:5001
- IPFS_GATEWAY_URL=https://ipfs.jeffemmett.com
- IPFS_API_URL=https://ipfs-api.rspace.online
- IPFS_GATEWAY_URL=https://ipfs.rspace.online
volumes:
- uploads_data:/app/uploads
labels:

View File

@ -1,6 +1,6 @@
'use client';
import { useCallback } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
@ -45,6 +45,9 @@ function ToolbarButton({
}
function RichEditor({ value, onChange, valueJson, placeholder: placeholderText }: Omit<NoteEditorProps, 'type'>) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const editor = useEditor({
extensions: [
StarterKit.configure({
@ -83,9 +86,33 @@ function RichEditor({ value, onChange, valueJson, placeholder: placeholderText }
const addImage = useCallback(() => {
if (!editor) return;
const url = window.prompt('Image URL');
if (url) {
editor.chain().focus().setImage({ src: url }).run();
fileInputRef.current?.click();
}, [editor]);
const handleFileUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!editor || !e.target.files?.length) return;
const file = e.target.files[0];
e.target.value = ''; // reset input
setUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/uploads', { method: 'POST', body: formData });
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
const data = await res.json();
// Prefer IPFS proxy URL (decrypts on demand), fall back to local
const src = data.ipfs
? `/api/ipfs/${data.ipfs.cid}?key=${encodeURIComponent(data.ipfs.encKey)}`
: data.url;
editor.chain().focus().setImage({ src, alt: file.name }).run();
} catch (err) {
console.error('Image upload failed:', err);
// Fall back to URL prompt
const url = window.prompt('Upload failed. Enter image URL manually:');
if (url) editor.chain().focus().setImage({ src: url }).run();
} finally {
setUploading(false);
}
}, [editor]);
@ -200,10 +227,17 @@ function RichEditor({ value, onChange, valueJson, placeholder: placeholderText }
</ToolbarButton>
<ToolbarButton
onClick={addImage}
title="Add Image"
title="Upload Image"
>
Img
{uploading ? '...' : 'Img'}
</ToolbarButton>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
/>
<div className="w-px h-5 bg-slate-700 mx-1" />

View File

@ -68,8 +68,8 @@ export interface FileMetadata {
// ─── IPFS Client ───
const IPFS_API_URL = process.env.IPFS_API_URL || 'http://kubo:5001'
const IPFS_GATEWAY_URL = process.env.IPFS_GATEWAY_URL || 'https://ipfs.jeffemmett.com'
const IPFS_API_URL = process.env.IPFS_API_URL || 'https://ipfs-api.rspace.online'
const IPFS_GATEWAY_URL = process.env.IPFS_GATEWAY_URL || 'https://ipfs.rspace.online'
export function isIPFSEnabled(): boolean {
return process.env.IPFS_ENABLED === 'true'