This commit is contained in:
Gvidas Juknevičius 2024-11-02 19:34:53 +02:00
commit f7fefe300d
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
32 changed files with 5835 additions and 0 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[env]
RUST_LOG="debug,sqlx=info"

1
.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL="sqlite://db.sqlite?mode=rwc"

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
/**/target
/db.sqite
/src/db/models

3010
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "rtmc-be"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.92"
argon2 = { version = "0.5.3", features = ["simple"] }
axum = { version = "0.7.7", features = ["ws"] }
axum-extra = { version = "0.9.4", features = ["typed-header"] }
chrono = "0.4.38"
clap = "4.5.20"
env_logger = "0.11.5"
futures-util = "0.3.31"
headers = "0.4.0"
lazy_static = "1.5.0"
log = "0.4.22"
sea-orm = { version = "1.1.0", features = ["macros", "runtime-tokio-rustls", "sqlx-sqlite", "with-json", "with-uuid"] }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
tokio = { version = "1.41.0", features = ["full"] }
uuid = { version = "1.11.0", features = ["serde", "v4"] }

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# setup
```sh
cargo install sea-orm-cli
sea-orm-cli generate entity -o src/db/models # do this every time you edit the migrations, or first time on clone
cargo build
```
# api docs
u-token = user token
p-token = pc token
a-token = u-token or p-token
pid = cc:t pc id
GET - /ws (nothing required on first req, see ws docs for more info)
POST - /api/user/login (no token (duh), json body with email, pwd, returns json with token)
POST - /api/user/add (u-token in header, json body with full user info)
POST - /api/user/:id/edit (u-token in header, json body with data to replace with)
POST - /api/user/:id/remove (u-token in header, no body required)
GET - /api/user/:id/info (u-token in header, get back json)
GET - /api/cct/ (a-token in header, gets a list of all groups)
GET - /api/cct/:group/ (a-token in header, gets a list of all pid's)
GET - /api/cct/:group/:pid/ (a-token in header, gets a list of all values for that pc)
GET - /api/cct/:group/:pid/:val (a-token in header, gets a value from a pc in a group)
POST - /api/cct/group/add (a-token in header, adds a group)
POST - /api/cct/:group/edit (a-token in header, edits a group)
POST - /api/cct/:group/pc/add (a-token in header, adds a pc to a group)
POST - /api/cct/:group/:pid/edit (a-token in header, edits a pc in a group)
POST - /api/cct/:group/:pid/val/add (a-token in header, adds a value to a pc in a group)
POST - /api/cct/:group/:pid/:val/edit (a-token in header, edits value info in a pc in a group)
POST - /api/cct/:group/:pid/:val/set (a-token in header, sets a value in a pc in a group)
==========
ws
==========
`ws_msg_t`:
0: heartbeat ping
1: server to client
2: client to server
`msg_t`:
TODO!
```json
{ // low level message
"ws_msg_t": 0, // `ws_msg_t`
"ws_msg": { // high level message
"auth": "Token", // Auth token
"msg": { // Data field
"keypad": {
"DoorOpenEv": { // "DataType": "Value, can be a struct or anything"
"user": "uid",
"timestamp": 123456,
"door_id": "1234-abcd-1234-abcd"
}
}
},
}
}
```

BIN
db.sqlite Normal file

Binary file not shown.

2201
migration/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
migration/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-sqlite", # `DATABASE_DRIVER` feature
]

41
migration/README.md Normal file
View File

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

19
migration/src/lib.rs Normal file
View File

@ -0,0 +1,19 @@
pub use sea_orm_migration::prelude::*;
mod m20241102_133640_users;
mod m20241102_133644_computers;
mod m20241102_150432_pc_groups;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20241102_133640_users::Migration),
Box::new(m20241102_133644_computers::Migration),
Box::new(m20241102_150432_pc_groups::Migration),
]
}
}

View File

@ -0,0 +1,68 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(ColumnDef::new(User::Id)
.uuid()
.unique_key()
.not_null()
.primary_key()
)
.col(ColumnDef::new(User::Username)
.string()
.unique_key()
.not_null()
)
.col(ColumnDef::new(User::Email)
.string()
.unique_key()
.not_null()
)
.col(ColumnDef::new(User::PwdHash)
.string()
.not_null()
)
.col(ColumnDef::new(User::PwdSalt)
.string()
.not_null()
)
.col(ColumnDef::new(User::IsAdministrator)
.boolean()
.not_null()
)
.col(ColumnDef::new(User::CreatedAt)
.date_time()
.not_null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum User {
Table,
Id,
Username,
Email,
PwdHash,
PwdSalt,
IsAdministrator,
CreatedAt
}

View File

@ -0,0 +1,63 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
manager
.create_table(
Table::create()
.table(Computer::Table)
.if_not_exists()
.col(ColumnDef::new(Computer::Id)
.uuid()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Computer::Name)
.string()
.unique_key()
.not_null()
)
.col(ColumnDef::new(Computer::Group)
.string()
.not_null()
)
.col(ColumnDef::new(Computer::Type)
.string()
.not_null()
)
.col(ColumnDef::new(Computer::AccessToken)
.string()
.unique_key()
.not_null()
)
.col(ColumnDef::new(Computer::CreatedAt)
.date_time()
.not_null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Computer::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Computer {
Table,
Id,
Name,
Group,
Type,
AccessToken,
CreatedAt
}

View File

@ -0,0 +1,46 @@
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(PcGroup::Table)
.if_not_exists()
.col(ColumnDef::new(PcGroup::Id)
.uuid()
.unique_key()
.primary_key()
.not_null()
)
.col(ColumnDef::new(PcGroup::Name)
.string()
.not_null()
)
.col(ColumnDef::new(PcGroup::AvailableActions)
.json()
.not_null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(PcGroup::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum PcGroup {
Table,
Id,
Name,
AvailableActions,
}

6
migration/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

31
src/api/mod.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::context::AppContext;
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
pub mod routes;
pub mod ws;
async fn get_api_info() -> String {
format!("API v{}", env!("CARGO_PKG_VERSION"))
}
pub async fn start_api(ctx: AppContext) {
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.route("/api/", get(get_api_info))
.route("/ws/", get(ws::WsHandler::ws_handler))
.with_state(ctx);
// `POST /users` goes to `create_user`
// .route("/users", post(create_user));
// run our app with hyper, listening globally on port 3000
log::info!("Listening on http://0.0.0.0:3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

View File

View File

View File

View File

@ -0,0 +1,3 @@
mod login;
mod register;
mod event;

View File

View File

2
src/api/routes/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod cct;
pub mod user;

View File

View File

View File

View File

@ -0,0 +1,14 @@
use axum::extract::State;
use crate::context::AppContext;
pub mod add;
pub mod edit;
pub mod login;
pub mod remove;
async fn handler(State(ctx): State<AppContext>) -> String {
format!(":333")
}

View File

116
src/api/ws/mod.rs Normal file
View File

@ -0,0 +1,116 @@
use std::{collections::HashMap, sync::mpsc::{self, Receiver, Sender}};
use axum::{body::HttpBody, extract::{ws::WebSocket, State, WebSocketUpgrade}, http::StatusCode, response::Response};
use axum_extra::TypedHeader;
use chrono::{DateTime, Utc};
use futures_util::{stream::{SplitSink, SplitStream}, StreamExt};
use headers::{authorization::Bearer, Authorization};
use sea_orm::prelude::Uuid;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use crate::context::AppContext;
lazy_static::lazy_static!(
pub static ref WS_CLIENTS: Mutex<HashMap<Uuid, WsClient>> = Mutex::new(HashMap::new());
);
#[derive(Debug)]
pub struct WsClient {
rx: Receiver<WsMessage>,
tx: Sender<WsMessage>,
alive: bool,
last_heartbeat: DateTime<Utc>
}
impl WsClient {
pub fn new(rx: Receiver<WsMessage>, tx: Sender<WsMessage>) -> Self {
Self {
rx, tx,
alive: false,
last_heartbeat: chrono::Utc::now()
}
}
}
#[derive(Debug, Clone)]
pub struct WsHandler {
}
impl WsHandler {
pub fn new() -> Self {
Self {
}
}
pub async fn ws_handler(ws: WebSocketUpgrade, State(ctx): State<AppContext>, TypedHeader(token): TypedHeader<Authorization<Bearer>>) -> Response {
let token = token.0.token().to_string();
let Ok(token) = Uuid::parse_str(&token) else {
todo!()
// return Response::status(Body)
};
ws.on_upgrade(move |socket| Self::handle_socket(socket, token))
}
async fn handle_socket(socket: WebSocket, token: Uuid) {
let (sender, receiver) = socket.split();
let (r_tx, from_socket) = mpsc::channel();
let (to_socket, s_rx) = mpsc::channel();
async fn send(sender: SplitSink<WebSocket, axum::extract::ws::Message>, s_rx: mpsc::Receiver<WsMessage>) {
}
async fn recv(mut receiver: SplitStream<WebSocket>, tx: mpsc::Sender<WsMessage>) {
while let Some(msg) = receiver.next().await {
let Ok(msg) = msg else {
return;
};
let Ok(msg) = msg.to_text() else {
return;
};
}
}
tokio::spawn(recv(receiver, r_tx));
tokio::spawn(send(sender, s_rx));
Self::add_client(token, from_socket, to_socket).await;
}
async fn add_client(token: Uuid, rx: Receiver<WsMessage>, tx: Sender<WsMessage>) {
WS_CLIENTS.lock().await.insert(token, WsClient::new(rx, tx));
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WsMessage {
pub ws_msg_t: usize,
pub ws_msg: Message,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub auth: Uuid,
pub msg: MessageType
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageType {
Keypad(KeypadMessageType)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeypadMessageType {
DoorOpenEv {
user: Uuid,
timestamp: usize,
door_id: Uuid
}
}

26
src/context/mod.rs Normal file
View File

@ -0,0 +1,26 @@
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use crate::{api::ws::WsHandler, db::Database};
#[derive(Debug, Clone)]
pub struct AppContext {
db: Database,
ws: WsHandler
}
impl AppContext {
pub fn new(db: Database) -> Self {
Self {
db,
ws: WsHandler::new()
}
}
pub fn db(&mut self) -> &mut Database {
&mut self.db
}
pub fn ws(&mut self) -> &mut WsHandler {
&mut self.ws
}
}

36
src/db/mod.rs Normal file
View File

@ -0,0 +1,36 @@
use std::time::Duration;
use sea_orm::{ConnectOptions, Database as SODatabase, DatabaseConnection};
mod models;
// to update the models run `sea-orm-cli generate entity -o src/db/models`
#[derive(Debug, Clone)]
pub struct Database {
db: DatabaseConnection
}
impl Database {
pub fn new() -> Self {
Self {
db: DatabaseConnection::Disconnected
}
}
pub async fn connect(&mut self) -> anyhow::Result<()> {
log::info!("Connecting to SQlite database at ./db.sqlite");
let mut opt = ConnectOptions::new("sqlite://db.sqlite?mode=rwc");
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.acquire_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.max_lifetime(Duration::from_secs(8))
.sqlx_logging(true)
.sqlx_logging_level(log::LevelFilter::Debug);
self.db = SODatabase::connect(opt).await?;
log::info!("Connection successful");
Ok(())
}
}

38
src/main.rs Normal file
View File

@ -0,0 +1,38 @@
use api::ws::{KeypadMessageType, Message, WsMessage};
use sea_orm::prelude::Uuid;
mod api;
mod db;
mod context;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
let msg = WsMessage {
ws_msg_t: 0,
ws_msg: Message {
auth: Uuid::new_v4(),
msg: api::ws::MessageType::Keypad(
KeypadMessageType::DoorOpenEv {
user: Uuid::new_v4(),
timestamp: 0,
door_id: Uuid::new_v4()
}
)
}
};
dbg!(&msg);
let txt = serde_json::to_string_pretty(&msg)?;
println!("{txt}");
return Ok(());
// TODO: Start db
let mut db = db::Database::new();
db.connect().await?;
let ctx = context::AppContext::new(db);
api::start_api(ctx).await;
Ok(())
}