feat: add initial server setup
This commit is contained in:
parent
b7a64260e8
commit
dcf9ab4deb
18
README.md
18
README.md
@ -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.
|
15
package.json
15
package.json
@ -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
2
server/.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
DB_FILE_NAME=src/db/prod.db
|
||||
PORT=3000
|
5
.gitignore → server/.gitignore
vendored
5
.gitignore → server/.gitignore
vendored
@ -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
11
server/drizzle.config.ts
Normal 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
25
server/package.json
Normal 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
32
server/src/db/schema.ts
Normal 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
170
server/src/index.ts
Normal 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}`)
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user