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
|
```bash
|
||||||
bun install
|
bun install
|
||||||
@ -10,4 +12,14 @@ To run:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run index.ts
|
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
|
.DS_Store
|
||||||
|
|
||||||
# Bun
|
# 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