Skip to content

Instantly share code, notes, and snippets.

@coderwithsense
Created November 12, 2025 06:43
Show Gist options
  • Select an option

  • Save coderwithsense/084fb6d01b8c11b8dbc9cb1580506658 to your computer and use it in GitHub Desktop.

Select an option

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)]
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
}
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)
}
}
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 }
)
}
}
// 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