Files
herolib/lib/data/ourdb_syncer/server.v
2025-03-09 21:17:32 +01:00

227 lines
6.6 KiB
V

module ourdb
import freeflowuniverse.herolib.ui.console
import veb
import rand
import time
import json
// Represents the server context, extending the veb.Context
pub struct ServerContext {
veb.Context
}
// Represents the OurDB server instance
@[heap]
pub struct OurDBServer {
veb.Middleware[ServerContext]
pub mut:
db &OurDB // Reference to the database instance
port int // Port on which the server runs
allowed_hosts []string // List of allowed hostnames
allowed_operations []string // List of allowed operations (e.g., set, get, delete)
secret_key string // Secret key for authentication
}
// Represents the arguments required to initialize the OurDB server
@[params]
pub struct OurDBServerArgs {
pub mut:
port int = 3000 // Server port, default is 3000
allowed_hosts []string = ['localhost'] // Allowed hosts
allowed_operations []string = ['set', 'get', 'delete'] // Allowed operations
secret_key string = rand.string_from_set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
32) // Generated secret key
config OurDBConfig // Database configuration parameters
}
// Creates a new instance of the OurDB server
pub fn new_server(args OurDBServerArgs) !OurDBServer {
mut db := new(
record_nr_max: args.config.record_nr_max
record_size_max: args.config.record_size_max
file_size: args.config.file_size
path: args.config.path
incremental_mode: args.config.incremental_mode
reset: args.config.reset
) or { return error('Failed to create ourdb: ${err}') }
mut server := OurDBServer{
port: args.port
allowed_hosts: args.allowed_hosts
allowed_operations: args.allowed_operations
secret_key: args.secret_key
db: &db
}
server.use(handler: server.logger_handler)
server.use(handler: server.allowed_hosts_handler)
server.use(handler: server.allowed_operations_handler)
return server
}
// Middleware for logging incoming requests and responses
fn (self &OurDBServer) logger_handler(mut ctx ServerContext) bool {
start_time := time.now()
request := ctx.req
method := request.method.str().to_upper()
client_ip := ctx.req.header.get(.x_forwarded_for) or { ctx.req.host.str().split(':')[0] }
user_agent := ctx.req.header.get(.user_agent) or { 'Unknown' }
console.print_header('${start_time.format()} | [Request] IP: ${client_ip} | Method: ${method} | Path: ${request.url} | User-Agent: ${user_agent}')
return true
}
// Middleware to check if the client host is allowed
fn (self &OurDBServer) allowed_hosts_handler(mut ctx ServerContext) bool {
client_host := ctx.req.host.str().split(':')[0].to_lower()
if !self.allowed_hosts.contains(client_host) {
ctx.request_error('403 Forbidden: Host not allowed')
console.print_stderr('Unauthorized host: ${client_host}')
return false
}
return true
}
// Middleware to check if the requested operation is allowed
fn (self &OurDBServer) allowed_operations_handler(mut ctx ServerContext) bool {
url_parts := ctx.req.url.split('/')
operation := url_parts[1]
if operation !in self.allowed_operations {
ctx.request_error('403 Forbidden: Operation not allowed')
console.print_stderr('Unauthorized operation: ${operation}')
return false
}
return true
}
// Parameters for running the server
@[params]
pub struct RunParams {
pub mut:
background bool // If true, the server runs in the background
}
// Starts the OurDB server
pub fn (mut self OurDBServer) run(params RunParams) {
if params.background {
spawn veb.run[OurDBServer, ServerContext](mut self, self.port)
} else {
veb.run[OurDBServer, ServerContext](mut self, self.port)
}
}
// Represents a generic success response
@[params]
struct SuccessResponse[T] {
message string // Success message
data T // Response data
}
// Represents an error response
@[params]
struct ErrorResponse {
error string @[required] // Error type
message string @[required] // Error message
}
// Returns an error response
fn (server OurDBServer) error(args ErrorResponse) ErrorResponse {
return args
}
// Returns a success response
fn (server OurDBServer) success[T](args SuccessResponse[T]) SuccessResponse[T] {
return args
}
// Request body structure for the `/set` endpoint
pub struct KeyValueData {
pub mut:
id u32 // Record ID
value string // Value to store
}
// API endpoint to set a key-value pair in the database
@['/set'; post]
pub fn (mut server OurDBServer) set(mut ctx ServerContext) veb.Result {
request_body := ctx.req.data.str()
mut decoded_body := json.decode(KeyValueData, request_body) or {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Invalid request body'
))
}
if server.db.incremental_mode && decoded_body.id > 0 {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Cannot set id when incremental mode is enabled'
))
}
mut record := if server.db.incremental_mode {
server.db.set(data: decoded_body.value.bytes()) or {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Failed to set key: ${err}'
))
}
} else {
server.db.set(id: decoded_body.id, data: decoded_body.value.bytes()) or {
ctx.res.set_status(.bad_request)
return ctx.json[ErrorResponse](server.error(
error: 'bad_request'
message: 'Failed to set key: ${err}'
))
}
}
decoded_body.id = record
ctx.res.set_status(.created)
return ctx.json(server.success(message: 'Successfully set the key', data: decoded_body))
}
// API endpoint to retrieve a record by ID
@['/get/:id'; get]
pub fn (mut server OurDBServer) get(mut ctx ServerContext, id string) veb.Result {
id_ := id.u32()
record := server.db.get(id_) or {
ctx.res.set_status(.not_found)
return ctx.json[ErrorResponse](server.error(
error: 'not_found'
message: 'Record does not exist: ${err}'
))
}
data := KeyValueData{
id: id_
value: record.bytestr()
}
ctx.res.set_status(.ok)
return ctx.json(server.success(message: 'Successfully get record', data: data))
}
// API endpoint to delete a record by ID
@['/delete/:id'; delete]
pub fn (mut server OurDBServer) delete(mut ctx ServerContext, id string) veb.Result {
id_ := id.u32()
server.db.delete(id_) or {
ctx.res.set_status(.not_found)
return ctx.json[ErrorResponse](server.error(
error: 'not_found'
message: 'Failed to delete key: ${err}'
))
}
ctx.res.set_status(.no_content)
return ctx.json({
'message': 'Successfully deleted record'
})
}