Initial commit
This commit is contained in:
commit
1d7df1f152
15 changed files with 2906 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target/*
|
||||
db.sqlite
|
2558
Cargo.lock
generated
Normal file
2558
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"api/lib",
|
||||
"api/axum",
|
||||
"shared",
|
||||
]
|
||||
resolver = "2"
|
1
api/axum/.gitignore
vendored
Normal file
1
api/axum/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
17
api/axum/Cargo.toml
Normal file
17
api/axum/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "api-id-assigner"
|
||||
description = "Axum powered API to manage ids for hostnames of a sulrm cluster"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
api-id-assigner-lib = { path = "../lib" }
|
||||
axum = "0.6.18"
|
||||
axum-extra = { version = "0.7.7" }
|
||||
serde = { version = "1.0.188", features = ["serde_derive"] }
|
||||
sqlx = { version = "0.7.1", features = ["sqlite"] }
|
||||
tokio = {version = "1.28.2", features = ["full"]}
|
||||
toml = "0.8.0"
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.4.3", features = ["fs","cors"] }
|
||||
tracing = "0.1.37"
|
6
api/axum/migrations/20230910195018_schema.sql
Normal file
6
api/axum/migrations/20230910195018_schema.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
drop table if exists nodes;
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
ip string not null
|
||||
);
|
95
api/axum/src/main.rs
Normal file
95
api/axum/src/main.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use api_id_assigner_lib::AppState;
|
||||
use api_id_assigner_lib::router;
|
||||
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
|
||||
use axum::Router;
|
||||
use std::{net::SocketAddr, str::FromStr,fs, path::Path};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Config {
|
||||
ip: String,
|
||||
port: u16,
|
||||
db_location: String
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), sqlx::Error>{
|
||||
|
||||
|
||||
|
||||
|
||||
if !cfg!(debug_assertions){
|
||||
//check if running in production or debug env.
|
||||
if !std::path::Path::new("/var/assigner").exists() {
|
||||
println!("in release creating folder");
|
||||
match fs::create_dir_all("/var/assigner") {
|
||||
Ok(_res) => println!("Directory created succesfully"),
|
||||
Err(err) => panic!("cannot create dir {err}")
|
||||
}
|
||||
|
||||
}
|
||||
//check if config file exists, if not, write default values in it
|
||||
if !std::path::Path::new("/var/assigner/config.toml").exists() {
|
||||
println!("config file doesnt exist");
|
||||
let _ = match fs::write(Path::new("/var/assigner/config.toml"), b"ip = '0.0.0.0'\nport = 2000\ndb_location = '/var/assigner/db.sqlite'"){
|
||||
Ok(res) => res,
|
||||
Err(err) => panic!("error: {err}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// read the toml config file into a string, if file not found give default values
|
||||
let config_string = match fs::read_to_string("/var/assigner/config.toml") {
|
||||
Ok(res) => res,
|
||||
Err(_) => "
|
||||
ip = '0.0.0.0'
|
||||
port = 2000
|
||||
db_location = '/var/assigner/db.sqlite'".to_string()
|
||||
|
||||
};
|
||||
let config:Config = match toml::from_str(&config_string) {
|
||||
Ok(res) => res,
|
||||
Err(_) => Config { ip: "0.0.0.0".to_string(), port: 2000, db_location: "db.sqlite".to_string() }
|
||||
};
|
||||
// there is a better way but for now there isnt, if in development will be using a db.sqlite
|
||||
// file in the cargo run location, in release will be using a db located at the path specified.
|
||||
|
||||
|
||||
println!("Using db: {}",config.db_location);
|
||||
let options = SqliteConnectOptions::new()
|
||||
.filename(config.db_location)
|
||||
.create_if_missing(true);
|
||||
|
||||
let pool = SqlitePool::connect_with(options).await?;
|
||||
|
||||
sqlx::migrate!().run(&pool).await.expect("Migrations failed :(");
|
||||
|
||||
let domain:String = format!("{}:{}",config.ip,config.port);
|
||||
let state = AppState { domain,pool};
|
||||
let cors = CorsLayer::new().allow_origin(Any);
|
||||
|
||||
let api_router = router::create_api_router(state.clone());
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api", api_router)
|
||||
.layer(cors);
|
||||
|
||||
|
||||
//let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
let addr = SocketAddr::from_str(&state.domain).unwrap();
|
||||
println!("listening on {}", addr.to_string());
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
//app.into_make_service()
|
||||
app.into_make_service_with_connect_info::<SocketAddr>()
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
23
api/lib/Cargo.toml
Normal file
23
api/lib/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "api-id-assigner-lib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
shared = { path = "../../shared" ,features = ["backend"]}
|
||||
sqlx = { version = "0.7.1", default-features = false, features = [ "runtime-async-std-native-tls", "macros", "postgres", "uuid", "chrono", "json" ] }
|
||||
tracing = "0.1.37"
|
||||
async-traits = "0.0.0"
|
||||
uuid = { version = "1.4.1", features = ["serde", "v4", "js"] }
|
||||
async-trait = "0.1.73"
|
||||
axum = "0.6.20"
|
||||
tower-http = { version = "0.4.3", features = ["cors", "tower"] }
|
||||
http = "0.2.9"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.185", features = ["derive"] }
|
||||
serde_json = "1.0.105"
|
||||
chrono = "0.4.26"
|
||||
time = { version = "0.3.0", features = ["serde"] }
|
||||
|
13
api/lib/src/lib.rs
Normal file
13
api/lib/src/lib.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use sqlx::SqlitePool;
|
||||
|
||||
pub const API_VERSION: &str = "v0.0.1";
|
||||
|
||||
pub mod router;
|
||||
mod node;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: SqlitePool,
|
||||
pub domain: String,
|
||||
}
|
60
api/lib/src/node.rs
Normal file
60
api/lib/src/node.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use http::StatusCode;
|
||||
use std::net::SocketAddr;
|
||||
use axum::{Json,extract::{ConnectInfo,State},response::IntoResponse};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
use shared::models::{Node, DeleteNode};
|
||||
|
||||
pub async fn get_node_id(State(state):State<AppState>, ConnectInfo(addr): ConnectInfo<SocketAddr>) -> Result<Json<Node>, impl IntoResponse> {
|
||||
let node_from_db_result =match sqlx::query_as::<_,Node>
|
||||
("select * from nodes where ip = ?")
|
||||
.bind(addr.ip().to_string())
|
||||
.fetch_one(&state.pool).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(_err) => create_node_entry(State(&state),addr.ip().to_string()).await
|
||||
};
|
||||
|
||||
match node_from_db_result {
|
||||
Ok(node) => Ok(Json(node)),
|
||||
Err(err) => Err((StatusCode::INTERNAL_SERVER_ERROR, err.to_string().into_response()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//TODO add Json requirement to all api points for consistency
|
||||
async fn create_node_entry(State(state):State<&AppState>,ip:String) -> Result<Node,sqlx::Error> {
|
||||
|
||||
// finds the lowest free id in the db.
|
||||
match sqlx::query_as::<_,Node>("insert into nodes(id,ip) values ((SELECT (Min(id) + 1) AS NewIDToInsert
|
||||
FROM nodes T2A
|
||||
WHERE NOT EXISTS (SELECT id FROM nodes T2B WHERE T2A.id + 1 = T2B.id)),?) returning id, ip,created_at")
|
||||
.bind(ip)
|
||||
.fetch_one(&state.pool).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// requires json request with String ip of the device
|
||||
pub async fn del_node_id(State(state):State<AppState>,ConnectInfo(addr): ConnectInfo<SocketAddr>, Json(del_req): Json<DeleteNode>) -> Result<Json<Node>, impl IntoResponse> {
|
||||
if addr.ip().to_string() == del_req.ip {
|
||||
return Err((StatusCode::FORBIDDEN, "You cannot request node other then yourself to be removed from registry".to_string().into_response()));
|
||||
}
|
||||
match sqlx::query_as::<_,Node>(
|
||||
"delete from nodes where ip = ? returning id,ip,created_at")
|
||||
.bind(del_req.ip)
|
||||
.fetch_one(&state.pool).await
|
||||
{
|
||||
Ok(res) => Ok(Json(res)),
|
||||
Err(err) => Err((StatusCode::INTERNAL_SERVER_ERROR, err.to_string().into_response()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn apip(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
|
||||
format!("Hello {}", addr.ip().to_string())
|
||||
}
|
25
api/lib/src/router.rs
Normal file
25
api/lib/src/router.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use crate::AppState;
|
||||
use axum::{Router, routing::{post,get}, Json};
|
||||
use crate::node;
|
||||
|
||||
pub fn create_api_router(state:AppState) -> Router{
|
||||
|
||||
Router::new()
|
||||
.route("/test", get(root))
|
||||
.nest("/ip", create_api_ip_router(state))
|
||||
}
|
||||
|
||||
|
||||
pub fn create_api_ip_router(state:AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(node::apip))
|
||||
.route("/get", get(node::get_node_id))
|
||||
.route("/del", post(node::del_node_id))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn root() -> Json<String> {
|
||||
Json("bozo".to_string())
|
||||
}
|
||||
|
||||
|
68
api/lib/src/users.rs
Normal file
68
api/lib/src/users.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use crate::AppState;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::IntoResponse
|
||||
};
|
||||
|
||||
use shared::models::{CreateUser,User};
|
||||
|
||||
async fn get_user(State(state): State<AppState>, user_id: &uuid::Uuid) -> Result<User, impl IntoResponse> {
|
||||
sqlx::query_as::<_,User>(
|
||||
r#"
|
||||
SELECT id, name, email,password,created_at,ROLE
|
||||
FROM users
|
||||
WHERE user_id = $1
|
||||
"#,)
|
||||
.bind(&user_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn create_user(State(state): State<AppState>, create_user: &CreateUser) -> Result<User, impl IntoResponse> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
INSERT INTO users (name, email,password)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, name, email,password,created_at,ROLE
|
||||
"#,
|
||||
)
|
||||
.bind(&create_user.name)
|
||||
.bind(&create_user.email)
|
||||
.bind(&create_user.password)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn update_user(State(state): State<AppState>, user: &User) -> Result<User, impl IntoResponse> {
|
||||
sqlx::query_as::<_, User>(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET name, email,password, ROLE
|
||||
WHERE id = $1
|
||||
RETURNING id, name, email,password,created_at,ROLE
|
||||
"#,
|
||||
)
|
||||
.bind(&user.name)
|
||||
.bind(&user.email)
|
||||
.bind(&user.password)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn delete_user(State(state): State<AppState>, user_id: &uuid::Uuid) -> Result<uuid::Uuid, impl IntoResponse> {
|
||||
sqlx::query_scalar::<_, uuid::Uuid>(
|
||||
r#"
|
||||
DELETE FROM users
|
||||
WHERE id = $1
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
14
shared/Cargo.toml
Normal file
14
shared/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "shared"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[features]
|
||||
backend = ['sqlx']
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.26", features = ["serde"] }
|
||||
sqlx = { version = "0.7.1", default-features = false, features = [ "runtime-async-std-native-tls", "macros", "postgres", "uuid", "chrono", "json" ], optional = true }
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
serde_json = "1.0.105"
|
1
shared/src/lib.rs
Normal file
1
shared/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod models;
|
16
shared/src/models.rs
Normal file
16
shared/src/models.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use serde::{Deserialize,Serialize};
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
|
||||
#[derive(Serialize,Deserialize,Debug,Clone,PartialEq,Eq,PartialOrd,Ord)]
|
||||
pub struct Node {
|
||||
pub id: i32,
|
||||
pub ip: String,
|
||||
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
|
||||
#[derive(Serialize,Deserialize,Debug,Clone,PartialEq,Eq,PartialOrd,Ord,Default)]
|
||||
pub struct DeleteNode {
|
||||
pub ip: String,
|
||||
}
|
||||
|
Loading…
Reference in a new issue