feat: add initial server setup

This commit is contained in:
Aidan 2025-04-12 22:08:22 -04:00
parent b7a64260e8
commit dcf9ab4deb
10 changed files with 259 additions and 20 deletions

View File

@ -1,6 +1,8 @@
# beesrv-server
# BeeSrv
To install dependencies:
## Server
### Installing Dependencies
```bash
bun install
@ -10,4 +12,14 @@ To run:
```bash
bun run index.ts
```
```
### Applying DB Changes
```
bunx drizzle-kit push
```
### Serving Files
A `beebox.xml` file should be placed the `server/serve/` directory. You will have to create this directory.

View File

@ -1 +0,0 @@
console.log("Hello via Bun!");

View File

@ -1,15 +0,0 @@
{
"name": "beesrv-server",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"express": "^5.1.0"
}
}

2
server/.env.example Normal file
View File

@ -0,0 +1,2 @@
DB_FILE_NAME=src/db/prod.db
PORT=3000

View File

@ -34,4 +34,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store
# Bun
bun.lock*
bun.lock*
# Files
serve/

11
server/drizzle.config.ts Normal file
View File

@ -0,0 +1,11 @@
import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
out: './src/db/drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DB_FILE_NAME!,
},
});

25
server/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "beesrv-server",
"module": "src/index.ts",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"start": "bun run src/index.ts"
},
"devDependencies": {
"@types/bun": "^1.2.9",
"drizzle-kit": "^0.30.6"
},
"peerDependencies": {
"typescript": "^5.8.3"
},
"dependencies": {
"@types/express": "^5.0.1",
"better-sqlite3": "^11.9.1",
"chalk": "^5.4.1",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.41.0",
"express": "^5.1.0"
}
}

32
server/src/db/schema.ts Normal file
View File

@ -0,0 +1,32 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'
import { sql } from 'drizzle-orm'
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
})
export const requests = sqliteTable('requests', {
id: text('id').primaryKey(),
deviceId: text('device_id').notNull(),
version: text('version').notNull(),
status: text('status').notNull().default('pending'),
userId: text('user_id').notNull().references(() => users.id),
bbxHash: text('bbx_hash').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
})
export const usersRelations = relations(users, ({ many }) => ({
requests: many(requests),
}))
export const requestsRelations = relations(requests, ({ one }) => ({
user: one(users, {
fields: [requests.userId],
references: [users.id],
}),
}))

170
server/src/index.ts Normal file
View File

@ -0,0 +1,170 @@
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { Database } from 'bun:sqlite'
import express from "express"
import type { RequestHandler } from "express"
import { randomUUID } from "crypto"
import { eq, and, gte } from "drizzle-orm"
import chalk from "chalk"
import * as schema from './db/schema'
import path from 'path'
const app = express()
const sqlite = new Database(process.env.DB_FILE_NAME || 'dev.db')
const db = drizzle(sqlite, { schema })
app.use(express.json())
// Rate limiting
const RATE_LIMIT_WINDOW_MS = process.env.RATE_LIMIT_WINDOW_MS ? parseInt(process.env.RATE_LIMIT_WINDOW_MS) : 60 * 60 * 1000 // 1h default
const MAX_REQUESTS_PER_WINDOW = process.env.MAX_REQUESTS_PER_WINDOW ? parseInt(process.env.MAX_REQUESTS_PER_WINDOW) : 10 // 10 rph default
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
enum ReqStatus {
PENDING = "pending",
SENT = "sent",
FAIL = "fail",
}
function checkRateLimit(email: string): boolean {
const now = Date.now()
const userLimit = rateLimitMap.get(email)
if (!userLimit || now > userLimit.resetTime) {
rateLimitMap.set(email, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS })
return true
}
if (userLimit.count >= MAX_REQUESTS_PER_WINDOW) {
return false
}
userLimit.count++
return true
}
app.post("/bee-req", (async (req, res) => {
const { deviceID, version, email } = req.body
const requestID = randomUUID()
if (!deviceID || !version || !email) {
console.log(chalk.red(`[REQ | ${requestID}] Missing deviceID, version, or email`))
return res.status(400).send("Missing deviceID, version, or email")
}
// Check rate limit
if (!checkRateLimit(email as string)) {
console.log(chalk.red(`[REQ | ${requestID}] Rate limit exceeded for email: ${email}`))
return res.status(429).send("Rate limit exceeded. Please try again later.")
}
// Log request
console.log("┌────────────────────┐")
console.log("│ NEW BEEBOX REQUEST │")
console.log("├────────────────────┘")
console.log(`│ Request ID: ${requestID}`)
console.log(`│ Device ID: ${deviceID}`)
console.log(`│ Version: ${version}`)
console.log(`│ Email: ${email}`)
console.log("└─────────────────────")
// Check for existing user
const existingUser = await db.query.users.findFirst({
where: eq(schema.users.email, email as string),
})
console.log(chalk.yellow(`[REQ | ${requestID}]`) + ` Existing user: ${existingUser ? "true" : "false"}`)
let userId: string;
if (!existingUser) {
try {
// Create new user
const newUser = await db.insert(schema.users).values({
id: randomUUID(),
email: email as string,
}).returning()
if (!newUser[0]) {
throw new Error("Failed to create user")
}
userId = newUser[0].id
console.log(chalk.green(`[REQ | ${requestID}]`) + ` Created new user for request: ${userId}`)
} catch (error) {
console.log(chalk.red(`[REQ | ${requestID}]`) + ` Error creating user: ${error}`)
return res.status(500).send("Error creating user")
}
} else {
userId = existingUser.id
}
// Verify file
const beebox = await Bun.file(path.join(__dirname, '../serve/beebox.xml'))
const bbxContent = await beebox.text()
if (!bbxContent) {
console.log(chalk.red(`[REQ | ${requestID}]`) + ` Error reading beebox file`)
return res.status(500).send("Unable to find beebox")
}
// Get hash
const hasher = new Bun.CryptoHasher("sha256")
const bbxHash = await hasher.update(bbxContent).digest()
const bbxHashHex = bbxHash.toString('hex')
// Check for duplicate request resulting in same file hash
const recentDuplicate = await db.query.requests.findFirst({
where: and(
eq(schema.requests.userId, userId),
eq(schema.requests.bbxHash, bbxHashHex),
gte(schema.requests.createdAt, new Date(Date.now() - RATE_LIMIT_WINDOW_MS))
),
})
if (recentDuplicate) {
console.log(chalk.yellow(`[REQ | ${requestID}]`) + ` Duplicate request detected for user ${userId}`)
// Log failed request
await db.insert(schema.requests).values({
id: requestID,
deviceId: deviceID as string,
version: version as string,
status: ReqStatus.FAIL,
userId: userId,
bbxHash: bbxHashHex,
})
return res.status(409).send("No new beebox")
}
// Create new request
try {
await db.insert(schema.requests).values({
id: requestID,
deviceId: deviceID as string,
version: version as string,
status: ReqStatus.PENDING,
userId: userId,
bbxHash: bbxHashHex,
})
console.log(chalk.green(`[REQ | ${requestID}]`) + ` Created request in database`)
} catch (error) {
console.log(chalk.red(`[REQ | ${requestID}]`) + ` Error creating request: ${error}`)
return res.status(500).send("Error creating request")
}
// Before send, write completed
await db.update(schema.requests).set({
status: ReqStatus.SENT,
updatedAt: new Date(),
}).where(eq(schema.requests.id, requestID))
console.log(chalk.green(`[REQ | ${requestID}]`) + ` Updated request status, sending to user`)
// Send beebox to user
res.json(bbxContent)
}) as RequestHandler)
app.listen(process.env.PORT || 3000, async () => {
const pkgJson = Bun.file(path.join(__dirname, '../package.json'))
const version = JSON.parse(await pkgJson.text()).version
console.log(chalk.bgBlue("BEESRV") + " v" + version + "\n")
console.log(chalk.green(`[SERVER] `) + `Running on port ${process.env.PORT || 3000}`)
})