Created
November 12, 2025 06:43
-
-
Save coderwithsense/084fb6d01b8c11b8dbc9cb1580506658 to your computer and use it in GitHub Desktop.
Backend Implementation: API Route Handler [File Upload Route (app/api/upload/route.ts)]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useContractWrite, usePrepareContractWrite } from 'wagmi' | |
| import { parseAbi } from 'viem' | |
| const CONTRACT_ABI = parseAbi([ | |
| 'function registerFileMetadata(string cid, string fileName) external', | |
| 'function grantAccess(bytes32 fileHash, address recipient) external', | |
| 'function hasAccess(bytes32 fileHash, address requester) external view returns (bool)', | |
| 'function getFileMetadata(bytes32 fileHash) external view returns (tuple(address owner, string cid, uint256 timestamp, string fileName, bool exists))' | |
| ]) | |
| /** | |
| * Register file metadata on blockchain | |
| */ | |
| export function useRegisterFileMetadata( | |
| contractAddress: string, | |
| cid: string, | |
| fileName: string | |
| ) { | |
| const { config } = usePrepareContractWrite({ | |
| address: contractAddress as `0x${string}`, | |
| abi: CONTRACT_ABI, | |
| functionName: 'registerFileMetadata', | |
| args: [cid, fileName] | |
| }) | |
| const { write, isLoading, isSuccess, error } = useContractWrite(config) | |
| return { write, isLoading, isSuccess, error } | |
| } | |
| /** | |
| * Grant file access to another address | |
| */ | |
| export function useGrantAccess( | |
| contractAddress: string, | |
| fileHash: string, | |
| recipientAddress: string | |
| ) { | |
| const { config } = usePrepareContractWrite({ | |
| address: contractAddress as `0x${string}`, | |
| abi: CONTRACT_ABI, | |
| functionName: 'grantAccess', | |
| args: [fileHash as `0x${string}`, recipientAddress as `0x${string}`] | |
| }) | |
| const { write, isLoading, isSuccess, error } = useContractWrite(config) | |
| return { write, isLoading, isSuccess, error } | |
| } | |
| /** | |
| * Check if user has access to file | |
| */ | |
| export async function checkFileAccess( | |
| contractAddress: string, | |
| fileHash: string, | |
| userAddress: string, | |
| publicClient: any // Viem public client | |
| ) { | |
| const hasAccess = await publicClient.readContract({ | |
| address: contractAddress, | |
| abi: CONTRACT_ABI, | |
| functionName: 'hasAccess', | |
| args: [fileHash, userAddress] | |
| }) | |
| return hasAccess as boolean | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { secretbox, randomBytes } from 'tweetnacl' | |
| import { decodeUTF8, encodeBase64, decodeBase64 } from 'tweetnacl-util' | |
| /** | |
| * Generate random encryption key | |
| */ | |
| export function generateEncryptionKey(): Uint8Array { | |
| return randomBytes(secretbox.keyLength) // 32 bytes for secretbox | |
| } | |
| /** | |
| * Encrypt file using NaCl secretbox (AES-like) | |
| * @param plaintext File content as Uint8Array | |
| * @param key Encryption key | |
| * @returns Encrypted data with nonce prepended | |
| */ | |
| export function encryptFile( | |
| plaintext: Uint8Array, | |
| key: Uint8Array | |
| ): Uint8Array { | |
| const nonce = randomBytes(secretbox.nonceLength) // 24 bytes random nonce | |
| const encrypted = secretbox(plaintext, nonce, key) | |
| // Prepend nonce to encrypted data (nonce not secret) | |
| const result = new Uint8Array(nonce.length + encrypted.length) | |
| result.set(nonce) | |
| result.set(encrypted, nonce.length) | |
| return result | |
| } | |
| /** | |
| * Decrypt file using NaCl secretbox | |
| * @param ciphertext Encrypted data with nonce prepended | |
| * @param key Encryption key | |
| * @returns Decrypted plaintext | |
| */ | |
| export function decryptFile( | |
| ciphertext: Uint8Array, | |
| key: Uint8Array | |
| ): Uint8Array { | |
| // Extract nonce from ciphertext | |
| const nonce = ciphertext.slice(0, secretbox.nonceLength) | |
| const encrypted = ciphertext.slice(secretbox.nonceLength) | |
| // Decrypt | |
| const plaintext = secretbox.open(encrypted, nonce, key) | |
| if (!plaintext) { | |
| throw new Error('Decryption failed - invalid key or corrupted data') | |
| } | |
| return plaintext | |
| } | |
| /** | |
| * Serialize key for storage (base64) | |
| */ | |
| export function serializeKey(key: Uint8Array): string { | |
| return encodeBase64(key) | |
| } | |
| /** | |
| * Deserialize key from storage | |
| */ | |
| export function deserializeKey(keyString: string): Uint8Array { | |
| return decodeBase64(keyString) | |
| } | |
| /** | |
| * Encrypt file in browser | |
| * @param file File object from input | |
| * @returns Object with encrypted blob and encryption key | |
| */ | |
| export async function encryptFileInBrowser( | |
| file: File | |
| ): Promise<{ encryptedBlob: Blob; encryptionKey: string }> { | |
| // Generate random key | |
| const key = generateEncryptionKey() | |
| // Read file as ArrayBuffer | |
| const arrayBuffer = await file.arrayBuffer() | |
| const plaintext = new Uint8Array(arrayBuffer) | |
| // Encrypt | |
| const encrypted = encryptFile(plaintext, key) | |
| // Return encrypted blob and serialized key | |
| return { | |
| encryptedBlob: new Blob([encrypted], { type: 'application/octet-stream' }), | |
| encryptionKey: serializeKey(key) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { NextRequest, NextResponse } from 'next/server' | |
| async function uploadToApillon(file: File) { | |
| const apiKey = process.env.APILLON_API_KEY! | |
| const apiSecret = process.env.APILLON_API_SECRET! | |
| const bucketUuid = process.env.APILLON_BUCKET_UUID! | |
| // Create Basic Auth header for Apillon API | |
| const credentials = Buffer.from(`${apiKey}:${apiSecret}`).toString('base64') | |
| try { | |
| // Step 1: Request signed upload URL from Apillon | |
| console.log('Step 1: Requesting signed URL from Apillon...') | |
| const uploadRes = await fetch( | |
| `https://api.apillon.io/storage/buckets/${bucketUuid}/upload`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Basic ${credentials}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| files: [ | |
| { | |
| fileName: file.name, | |
| contentType: file.type || 'application/octet-stream' | |
| } | |
| ] | |
| }), | |
| signal: AbortSignal.timeout(15000) // 15s timeout | |
| } | |
| ) | |
| if (!uploadRes.ok) { | |
| const err = await uploadRes.text() | |
| throw new Error(`Step 1 failed (${uploadRes.status}): ${err}`) | |
| } | |
| const uploadData = await uploadRes.json() | |
| const sessionUuid = uploadData.data?.sessionUuid | |
| const signedUrl = uploadData.data?.files?.[0]?.url | |
| const fileUuid = uploadData.data?.files?.[0]?.fileUuid | |
| if (!signedUrl || !sessionUuid) { | |
| throw new Error('No signed URL or session UUID in response') | |
| } | |
| // Step 2: Upload encrypted file to signed S3 URL | |
| console.log('Step 2: Uploading encrypted file...') | |
| const fileBuffer = await file.arrayBuffer() | |
| const uploadFileRes = await fetch(signedUrl, { | |
| method: 'PUT', | |
| headers: { | |
| 'Content-Type': file.type || 'application/octet-stream' | |
| }, | |
| body: fileBuffer, | |
| signal: AbortSignal.timeout(30000) // 30s timeout | |
| }) | |
| if (!uploadFileRes.ok) { | |
| throw new Error( | |
| `Step 2 failed (${uploadFileRes.status}): File upload failed` | |
| ) | |
| } | |
| // Step 3: End session and trigger IPFS synchronization | |
| console.log('Step 3: Ending session - starting IPFS sync...') | |
| const endRes = await fetch( | |
| `https://api.apillon.io/storage/buckets/${bucketUuid}/upload/${sessionUuid}/end`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Basic ${credentials}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({}), | |
| signal: AbortSignal.timeout(15000) | |
| } | |
| ) | |
| if (!endRes.ok) { | |
| console.warn(`Step 3 warning: Session end returned ${endRes.status}`) | |
| } | |
| // Return immediately with fileUuid - CID becomes available after async IPFS sync | |
| return { | |
| id: fileUuid, | |
| fileUuid, | |
| sessionUuid, | |
| fileName: file.name, | |
| status: 'File uploaded. IPFS sync in progress...', | |
| message: 'Poll /api/get-cid for Content Identifier' | |
| } | |
| } catch (e: any) { | |
| console.error('Apillon upload error:', e) | |
| throw e | |
| } | |
| } | |
| export async function POST(req: NextRequest) { | |
| const form = await req.formData() | |
| const file = form.get('file') | |
| if (!(file instanceof File)) { | |
| return NextResponse.json({ error: 'file missing' }, { status: 400 }) | |
| } | |
| try { | |
| const out = await uploadToApillon(file) | |
| return NextResponse.json({ provider: 'apillon-ipfs', ...out }) | |
| } catch (e: any) { | |
| console.error('Upload error:', e.message) | |
| return NextResponse.json( | |
| { error: `Upload failed: ${e.message}` }, | |
| { status: 500 } | |
| ) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.0; | |
| /** | |
| * @title StorageMetadata | |
| * @dev Manages file ownership, metadata, and access control on-chain | |
| */ | |
| contract StorageMetadata { | |
| // File metadata structure | |
| struct FileRecord { | |
| address owner; | |
| string cid; // IPFS Content Identifier | |
| uint256 timestamp; // Upload timestamp | |
| string fileName; // Optional file name | |
| bool exists; // Flag to verify file exists | |
| } | |
| // Access control: fileHash => grantedAddress => bool | |
| mapping(bytes32 => mapping(address => bool)) public accessList; | |
| // File records: fileHash => FileRecord | |
| mapping(bytes32 => FileRecord) public fileRecords; | |
| // User's files: owner address => array of file hashes | |
| mapping(address => bytes32[]) public userFiles; | |
| // Events for logging | |
| event FileRegistered( | |
| address indexed owner, | |
| bytes32 indexed fileHash, | |
| string cid, | |
| uint256 timestamp | |
| ); | |
| event AccessGranted( | |
| bytes32 indexed fileHash, | |
| address indexed grantedTo | |
| ); | |
| event AccessRevoked( | |
| bytes32 indexed fileHash, | |
| address indexed revokedFrom | |
| ); | |
| /** | |
| * @dev Register file metadata on-chain | |
| * @param _cid IPFS Content Identifier | |
| * @param _fileName Optional file name | |
| */ | |
| function registerFileMetadata( | |
| string memory _cid, | |
| string memory _fileName | |
| ) external { | |
| require(bytes(_cid).length > 0, "CID cannot be empty"); | |
| // Create unique file hash from CID and owner | |
| bytes32 fileHash = keccak256(abi.encodePacked(msg.sender, _cid)); | |
| require(!fileRecords[fileHash].exists, "File already registered"); | |
| // Store metadata | |
| fileRecords[fileHash] = FileRecord({ | |
| owner: msg.sender, | |
| cid: _cid, | |
| timestamp: block.timestamp, | |
| fileName: _fileName, | |
| exists: true | |
| }); | |
| // Add to user's file list | |
| userFiles[msg.sender].push(fileHash); | |
| // Owner automatically has access | |
| accessList[fileHash][msg.sender] = true; | |
| emit FileRegistered(msg.sender, fileHash, _cid, block.timestamp); | |
| } | |
| /** | |
| * @dev Grant file access to another address | |
| * @param _fileHash Unique file identifier | |
| * @param _recipient Address to grant access to | |
| */ | |
| function grantAccess( | |
| bytes32 _fileHash, | |
| address _recipient | |
| ) external { | |
| require(fileRecords[_fileHash].exists, "File does not exist"); | |
| require( | |
| fileRecords[_fileHash].owner == msg.sender, | |
| "Only owner can grant access" | |
| ); | |
| require(_recipient != address(0), "Invalid recipient address"); | |
| accessList[_fileHash][_recipient] = true; | |
| emit AccessGranted(_fileHash, _recipient); | |
| } | |
| /** | |
| * @dev Revoke file access from an address | |
| * @param _fileHash Unique file identifier | |
| * @param _recipient Address to revoke access from | |
| */ | |
| function revokeAccess( | |
| bytes32 _fileHash, | |
| address _recipient | |
| ) external { | |
| require(fileRecords[_fileHash].exists, "File does not exist"); | |
| require( | |
| fileRecords[_fileHash].owner == msg.sender, | |
| "Only owner can revoke access" | |
| ); | |
| accessList[_fileHash][_recipient] = false; | |
| emit AccessRevoked(_fileHash, _recipient); | |
| } | |
| /** | |
| * @dev Check if address has access to file | |
| * @param _fileHash Unique file identifier | |
| * @param _requester Address checking access | |
| * @return bool True if requester has access | |
| */ | |
| function hasAccess( | |
| bytes32 _fileHash, | |
| address _requester | |
| ) external view returns (bool) { | |
| require(fileRecords[_fileHash].exists, "File does not exist"); | |
| return accessList[_fileHash][_requester]; | |
| } | |
| /** | |
| * @dev Verify if address is file owner | |
| * @param _fileHash Unique file identifier | |
| * @param _account Address to verify | |
| * @return bool True if account is owner | |
| */ | |
| function isOwner( | |
| bytes32 _fileHash, | |
| address _account | |
| ) external view returns (bool) { | |
| return fileRecords[_fileHash].owner == _account; | |
| } | |
| /** | |
| * @dev Get file metadata | |
| * @param _fileHash Unique file identifier | |
| * @return FileRecord Complete file record | |
| */ | |
| function getFileMetadata( | |
| bytes32 _fileHash | |
| ) external view returns (FileRecord memory) { | |
| require(fileRecords[_fileHash].exists, "File does not exist"); | |
| return fileRecords[_fileHash]; | |
| } | |
| /** | |
| * @dev Get user's files | |
| * @param _user User address | |
| * @return bytes32[] Array of file hashes owned by user | |
| */ | |
| function getUserFiles( | |
| address _user | |
| ) external view returns (bytes32[] memory) { | |
| return userFiles[_user]; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment