Skip to content

Commit a3db9f7

Browse files
committed
feat: IPFS API draft
1 parent 24dc268 commit a3db9f7

File tree

11 files changed

+802
-44
lines changed

11 files changed

+802
-44
lines changed

backend/ipfs/TaskBoardIPFS.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,31 @@ const dotenv = require('dotenv')
1313
// Load env variables
1414
dotenv.config()
1515

16+
// Singletone pattern
1617
export class TaskBoardIPFS {
18+
private static instance: TaskBoardIPFS
1719
private ipfs: HeliaIPFSClient
1820

19-
constructor () {
21+
private constructor () {
2022
this.ipfs = new HeliaIPFSClient({
2123
mode: (process.env.IPFS_STORAGE_MODE! as IPFSMode) || IPFSMode.DEVELOPMENT,
2224
localIPFSApiUrl: process.env.IPFS_LOCAL_API_URL!,
2325
localIPFSGatewayUrl: process.env.IPFS_LOCAL_GATEWAY_URL!
2426
})
2527
}
2628

29+
static getInstance(): TaskBoardIPFS {
30+
if (!TaskBoardIPFS.instance) {
31+
TaskBoardIPFS.instance = new TaskBoardIPFS()
32+
}
33+
return TaskBoardIPFS.instance
34+
}
35+
2736
/**
2837
* Upload task to IPFS
2938
* @param task task to be uploaded. This mutates `ipfsCid` and `createdAt` attributes of the task!
3039
* @returns identifier of task
40+
* @throws if upload was not successful.
3141
*/
3242
public async publishTask(task: Task): Promise<string> {
3343
try {
@@ -45,6 +55,12 @@ export class TaskBoardIPFS {
4555
}
4656
}
4757

58+
/**
59+
* Download task from IPFS
60+
* @param cid identifier of task
61+
* @returns object representing the received task.
62+
* @throws if download was not successful.
63+
*/
4864
public async loadTaskFromIPFS(cid: string): Promise<Task> {
4965
try {
5066
const taskBytes = await this.ipfs.download(cid)
@@ -60,4 +76,6 @@ export class TaskBoardIPFS {
6076
throw new Error(`Failed to load task from IPFS ${cid}: ${error}`)
6177
}
6278
}
79+
80+
/// TODO: Unpin task from IPFS
6381
}

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"license": "ISC",
1313
"description": "",
1414
"dependencies": {
15+
"@fastify/swagger": "^9.5.1",
16+
"@fastify/swagger-ui": "^5.2.3",
1517
"@helia/http": "^2.2.1",
1618
"@helia/unixfs": "^5.1.0",
1719
"@sinclair/typebox": "^0.34.39",

backend/src/TaskManager.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TaskBoardIPFS } from "../ipfs/TaskBoardIPFS"
2+
import { Task } from "../../objects/BoardTask"
3+
4+
5+
class TaskManager{
6+
private ipfs: TaskBoardIPFS
7+
8+
constructor() {
9+
this.ipfs = TaskBoardIPFS.getInstance()
10+
}
11+
12+
async addTask(task: Task): Promise<string> {
13+
// Here: actually upload to IPFS using Helia/Web3.Storage/etc.
14+
// return the CID
15+
const cid = 'QmFakeCID' + Date.now() // placeholder
16+
task.ipfsCid = cid
17+
return cid
18+
}
19+
20+
async getTasks(limit: number, offset: number, filters?: Partial<Task>): Promise<Task[]> {
21+
// TODO: Later, hook into DB/cache/Graph
22+
return []
23+
}
24+
25+
async deleteTask(cid: string): Promise<void> {
26+
// TODO: Call unpin API
27+
}
28+
29+
}
30+
31+
export default TaskManager

backend/src/app.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Fastify from "fastify";
2+
import fastifySwagger from '@fastify/swagger'
3+
import fastifySwaggerUi from '@fastify/swagger-ui'
24
const dotenv = require('dotenv')
35

46
// Load env variables
@@ -7,9 +9,26 @@ dotenv.config()
79

810
const app = Fastify({ logger: true })
911

12+
// Swagger for API documentation
13+
app.register(fastifySwagger, {
14+
swagger: {
15+
info: {
16+
title: 'CTF Backend API',
17+
description: 'API docs for CTF challenge backend',
18+
version: '0.1.0'
19+
}
20+
}
21+
})
22+
23+
app.register(fastifySwaggerUi, {
24+
routePrefix: '/docs',
25+
uiConfig: { docExpansion: 'full', deepLinking: false }
26+
})
27+
28+
1029
// Register routes
11-
import ipfsRoutes from "./routes/ipfsRoutes";
12-
app.register(ipfsRoutes, { prefix: '/api/ipfs' })
30+
import taskRoutes from "./routes/taskRoutes";
31+
app.register(taskRoutes, { prefix: '/api' })
1332

1433
// Start server
1534
const start = async () => {

backend/src/controllers/ipfsController.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { type FastifyReply, type FastifyRequest } from 'fastify';
2+
import { type Static } from '@sinclair/typebox'
3+
import { TaskSchema } from '../schemas/taskSchema'
4+
import TaskManager from '../TaskManager'
5+
import { Task } from '../../../objects/BoardTask';
6+
7+
type TaskBody = Static<typeof TaskSchema>
8+
9+
const IPFS = new TaskManager()
10+
11+
export async function createTaskController(
12+
request: FastifyRequest<{ Body: TaskBody }>,
13+
reply: FastifyReply
14+
) {
15+
const task = new Task(request.body)
16+
const cid = await IPFS.addTask(task)
17+
reply.code(201).send({ ...task.toJSON(), createdAt: task.createdAt, ipfsCid: cid })
18+
}
19+
20+
21+
export async function getTasksController(
22+
request: FastifyRequest<{ Querystring: { limit?: number; offset?: number; difficulty?: string } }>,
23+
reply: FastifyReply
24+
) {
25+
const { limit = 10, offset = 0, difficulty } = request.query
26+
const filters: any = {}
27+
if (difficulty) filters.difficulty = difficulty
28+
29+
const tasks = await IPFS.getTasks(limit, offset, filters)
30+
reply.send(tasks.map(t => ({ ...t.toJSON(), createdAt: t.createdAt, ipfsCid: t.ipfsCid })))
31+
}
32+
33+
34+
export async function deleteTaskController(
35+
request: FastifyRequest<{ Params: { cid: string } }>,
36+
reply: FastifyReply
37+
) {
38+
await IPFS.deleteTask(request.params.cid)
39+
reply.code(204).send()
40+
}

backend/src/routes/ipfsRoutes.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

backend/src/routes/taskRoutes.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { type FastifyPluginAsync } from 'fastify';
2+
import { TaskResponseSchema, TaskSchema } from '../schemas/taskSchema'
3+
import { createTaskController, deleteTaskController, getTasksController } from '../controllers/taskController'
4+
5+
const taskRoutes: FastifyPluginAsync = async (app) => {
6+
7+
app.get('/tasks', {
8+
schema: {
9+
querystring: {
10+
type: 'object',
11+
properties: {
12+
limit: { type: 'integer', minimum: 1, maximum: 50 },
13+
offset: { type: 'integer', minimum: 0 },
14+
difficulty: { type: 'string' }
15+
}
16+
},
17+
response: {
18+
200: { type: 'array', items: TaskResponseSchema }
19+
}
20+
},
21+
handler: getTasksController
22+
})
23+
24+
app.post('/tasks', {
25+
schema: {
26+
body: TaskSchema,
27+
response: {
28+
201: TaskResponseSchema
29+
}
30+
},
31+
handler: createTaskController
32+
})
33+
34+
app.delete('/tasks/:cid', {
35+
schema: {
36+
params: {
37+
type: 'object',
38+
required: ['cid'],
39+
properties: { cid: { type: 'string' } }
40+
}
41+
},
42+
handler: deleteTaskController
43+
})
44+
}
45+
46+
export default taskRoutes;

backend/src/schemas/ipfsSchema.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

backend/src/schemas/taskSchema.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Type } from '@sinclair/typebox'
2+
3+
export const TaskDifficultySchema = Type.Union([
4+
Type.Literal('Beginner'),
5+
Type.Literal('Easy'),
6+
Type.Literal('Medium'),
7+
Type.Literal('Hard'),
8+
Type.Literal('Expert')
9+
])
10+
11+
export const TaskSchema = Type.Object({
12+
title: Type.String({ minLength: 3 }),
13+
description: Type.String({ minLength: 0 }),
14+
difficulty: TaskDifficultySchema,
15+
tags: Type.Array(Type.String(), { default: [] }),
16+
createdBy: Type.String(),
17+
requirements: Type.Optional(Type.Array(Type.String())),
18+
resources: Type.Optional(Type.Array(Type.String()))
19+
})
20+
21+
export const TaskResponseSchema = Type.Intersect([
22+
TaskSchema,
23+
Type.Object({
24+
createdAt: Type.Number(),
25+
ipfsCid: Type.Optional(Type.String())
26+
})
27+
])

0 commit comments

Comments
 (0)