| require('dotenv').config() |
| var cors = require('cors') |
| var { I } = require('ipfsio') |
| var express = require('express') |
| var multer = require('multer') |
| const fs = require('fs') |
| const fsp = require('fs').promises |
| const { randomUUID } = require('crypto'); |
| const axios = require('axios') |
| const jose = require('jose') |
| var app = express() |
| var i = new I(process.env.NFT_STORAGE_KEY) |
|
|
| let institutionKeyPair; |
| const KEY_FILE = 'keys.json'; |
|
|
| async function initializeKeys() { |
| try { |
| if (fs.existsSync(KEY_FILE)) { |
| const keyFileContent = await fsp.readFile(KEY_FILE, 'utf-8'); |
| const jwk = JSON.parse(keyFileContent); |
| institutionKeyPair = { |
| publicKey: await jose.importJWK(jwk.publicKey, 'ES256'), |
| privateKey: await jose.importJWK(jwk.privateKey, 'ES256'), |
| }; |
| console.log('Loaded institutional key pair from file.'); |
| } else { |
| const { publicKey, privateKey } = await jose.generateKeyPair('ES256'); |
| institutionKeyPair = { publicKey, privateKey }; |
| const jwk = { |
| publicKey: await jose.exportJWK(publicKey), |
| privateKey: await jose.exportJWK(privateKey), |
| }; |
| await fsp.writeFile(KEY_FILE, JSON.stringify(jwk, null, 2)); |
| console.log('Generated new institutional key pair and saved to file.'); |
| } |
| } catch (error) { |
| console.error('Failed to initialize keys:', error); |
| } |
| } |
|
|
| initializeKeys(); |
|
|
| const recentUploads = [] |
| const MAX_RECENT_UPLOADS = 10 |
|
|
| const UPLOAD_DIR = 'uploads/' |
| if (!fs.existsSync(UPLOAD_DIR)) { |
| fs.mkdirSync(UPLOAD_DIR) |
| } |
|
|
| const allowed = (process.env.ALLOWED ? process.env.ALLOWED.split(",") : []) |
| const port = (process.env.PORT ? process.env.PORT : 3000) |
| const issuerDid = process.env.ISSUER_DID || 'did:web:example.com'; |
| const storage = multer.diskStorage({ |
| destination: function (req, file, cb) { |
| cb(null, UPLOAD_DIR) |
| }, |
| filename: function (req, file, cb) { |
| const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) |
| cb(null, file.fieldname + '-' + uniqueSuffix) |
| } |
| }) |
| const upload = multer({ |
| storage: storage, |
| limits: { |
| fileSize: 1024 * 1024 * 5, |
| files: 10 |
| } |
| }) |
| app.use(express.json()) |
| app.use(express.static('public')) |
| if (allowed && allowed.length > 0) { |
| app.use(cors({ origin: allowed })) |
| } else { |
| app.use(cors()) |
| } |
| app.use(express.urlencoded({ extended: true })); |
| app.post('/add', async (req, res) => { |
| let cid; |
| if (req.body.url) { |
| cid = await i.url(req.body.url) |
| } else if (req.body.object) { |
| cid = await i.object(req.body.object) |
| } |
| console.log("cid", cid) |
| res.json({ success: cid }) |
| }) |
| app.get('/.well-known/jwks.json', async (req, res) => { |
| if (!institutionKeyPair || !institutionKeyPair.publicKey) { |
| return res.status(500).json({ error: 'Key pair not generated yet.' }); |
| } |
| const jwk = await jose.exportJWK(institutionKeyPair.publicKey); |
| res.json({ keys: [jwk] }); |
| }); |
|
|
| app.post('/issue-credential', async (req, res) => { |
| if (!institutionKeyPair || !institutionKeyPair.privateKey) { |
| return res.status(500).json({ error: 'Key pair not generated yet.' }); |
| } |
|
|
| const { studentDid, proofOfWorkCID, claims } = req.body; |
| if (!studentDid || !claims) { |
| return res.status(400).json({ error: 'Missing studentDid or claims.' }); |
| } |
|
|
| try { |
| const vcPayload = { |
| '@context': [ |
| 'https://www.w3.org/2018/credentials/v1', |
| 'https://www.w3.org/2018/credentials/examples/v1' |
| ], |
| id: `urn:uuid:${randomUUID()}`, |
| type: ['VerifiableCredential', 'AcademicCredential'], |
| issuer: issuerDid, |
| issuanceDate: new Date().toISOString(), |
| credentialSubject: { |
| id: studentDid, |
| proofOfWorkCID: proofOfWorkCID, |
| ...claims |
| } |
| }; |
|
|
| const jwt = await new jose.SignJWT(vcPayload) |
| .setProtectedHeader({ alg: 'ES256' }) |
| .setIssuedAt() |
| .setIssuer(issuerDid) |
| .setSubject(studentDid) |
| .sign(institutionKeyPair.privateKey); |
|
|
| res.json({ vcJwt: jwt }); |
| } catch (error) { |
| console.error('Failed to issue credential:', error); |
| res.status(500).json({ error: 'Failed to issue credential.' }); |
| } |
| }); |
| app.post('/batch-archive', async (req, res) => { |
| const { items } = req.body; |
|
|
| if (!items || !Array.isArray(items) || items.length === 0) { |
| return res.status(400).json({ error: 'Invalid or empty "items" array in request body.' }); |
| } |
|
|
| try { |
| const processingPromises = items.map(async (item) => { |
| const cid = await i.url(item.url); |
| return { cid, metadata: item.metadata }; |
| }); |
|
|
| const processedItems = await Promise.all(processingPromises); |
|
|
| const manifest = { |
| archiveDate: new Date().toISOString(), |
| items: processedItems, |
| }; |
|
|
| const manifestCid = await i.object(manifest); |
| res.json({ success: true, manifestCid }); |
|
|
| } catch (error) { |
| console.error('Batch archive failed:', error); |
| res.status(500).json({ error: 'Failed to process batch archive.' }); |
| } |
| }); |
| app.get('/recent', (req, res) => { |
| res.json(recentUploads) |
| }) |
|
|
| app.get('/status/:cid', async (req, res) => { |
| const { cid } = req.params |
| try { |
| const response = await axios.get(`https://api.nft.storage/check/${cid}`, { |
| headers: { |
| 'Authorization': `Bearer ${process.env.NFT_STORAGE_KEY}` |
| } |
| }) |
| res.json(response.data) |
| } catch (error) { |
| if (error.response) { |
| res.status(error.response.status).json({ error: error.response.data }) |
| } else { |
| res.status(500).json({ error: 'Failed to check CID status' }) |
| } |
| } |
| }) |
|
|
| app.post('/add-encrypted-object', upload.single('file'), async (req, res) => { |
| const file = req.file |
| if (!file) { |
| return res.status(400).json({ error: 'No file uploaded' }) |
| } |
|
|
| try { |
| const cid = await i.path(file.path) |
| res.json({ success: cid }) |
| } catch (error) { |
| console.error('Failed to pin encrypted object:', error) |
| res.status(500).json({ error: 'Failed to pin encrypted object' }) |
| } finally { |
| |
| await fsp.unlink(file.path) |
| } |
| }) |
|
|
| app.post('/add-learning-object', upload.array('files'), async (req, res) => { |
| const files = req.files |
| if (!files || files.length === 0) { |
| return res.status(400).json({ error: 'No files uploaded' }) |
| } |
|
|
| const { title, author, subject, gradeLevel, license, description } = req.body |
|
|
| try { |
| const cids = [] |
| for (const file of files) { |
| const cid = await i.path(file.path) |
| cids.push({ |
| filename: file.originalname, |
| cid: cid |
| }) |
| } |
|
|
| const manifest = { |
| title: title || 'Untitled Learning Object', |
| author: author || 'Unknown', |
| subject: subject || 'Uncategorized', |
| gradeLevel: gradeLevel || 'N/A', |
| license: license || 'CC-BY-4.0', |
| description: description || '', |
| files: cids |
| } |
|
|
| const manifestCid = await i.object(manifest) |
|
|
| recentUploads.unshift(manifestCid) |
| if (recentUploads.length > MAX_RECENT_UPLOADS) { |
| recentUploads.pop() |
| } |
|
|
| res.json({ success: manifestCid }) |
| } catch (error) { |
| console.error('Failed to process dataset:', error) |
| res.status(500).json({ error: 'Failed to process dataset' }) |
| } finally { |
| const unlinkPromises = files.map(file => fsp.unlink(file.path)) |
| await Promise.all(unlinkPromises) |
| } |
| }) |
| app.listen(port) |
|
|