1
0
This commit is contained in:
2024-04-23 09:23:05 +03:00
parent ef0fa83f8b
commit 95719b299e
22 changed files with 1005 additions and 385 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/target
/logs
*.db

1074
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,24 @@
[package]
name = "nir"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
edition = "2021"
name = "nir"
version = "0.1.0"
[dependencies]
axum = "0.7.5"
chrono = { version = "0.4.38", features = ["serde"] }
jwt = "0.16.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
sqlx = { version = "0.7.4", features = ["chrono"] }
tokio = { version = "1.37.0", features = ["full"] }
anyhow = "1.0.82"
axum = "0.7.5"
chrono = { version = "0.4.38", features = ["serde"] }
figment = { version = "0.10.18", features = ["env", "toml"] }
jsonwebtoken = "9.3.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
sqlx = { version = "0.6.3", features = [
"any",
"chrono",
"postgres",
"runtime-tokio-rustls",
"sqlite"
] }
tokio = { version = "1.37.0", features = ["full"] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }

6
config.toml Normal file
View File

@@ -0,0 +1,6 @@
jwt_secret = "secret"
port = 6666
[database]
max_connections = 5
url = "sqlite://nir.db?mode=rwc"

31
src/config.rs Normal file
View File

@@ -0,0 +1,31 @@
use std::sync::OnceLock;
use serde::Deserialize;
pub fn config() -> &'static Config {
static INSTANCE: OnceLock<Config> = OnceLock::new();
INSTANCE.get_or_init(|| {
use figment::providers::*;
use figment::*;
Figment::new()
.merge(Toml::file("config.toml"))
.extract()
.inspect_err(|e| tracing::error!("Could not load config: {e}"))
.unwrap()
})
}
#[derive(Deserialize)]
pub struct Config {
pub port: u16,
pub jwt_secret: String,
pub database: DatabaseConfig,
}
#[derive(Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
}

6
src/context.rs Normal file
View File

@@ -0,0 +1,6 @@
use crate::database::Database;
#[derive(Clone)]
pub struct Context<D: Database> {
pub database: D,
}

View File

@@ -0,0 +1,46 @@
use crate::{config, entity};
pub trait Database {
async fn init() -> anyhow::Result<Self>
where
Self: Sized;
async fn get_user_by_id(&self, id: entity::ShortId) -> anyhow::Result<Option<entity::User>>;
async fn get_user_by_login(&self, login: &str) -> anyhow::Result<Option<entity::User>>;
}
impl Database for sqlx::AnyPool {
async fn init() -> anyhow::Result<Self>
where
Self: Sized,
{
let config = config::config();
let pool = sqlx::any::AnyPoolOptions::new()
.max_connections(config.database.max_connections)
.connect(&config.database.url)
.await
.inspect_err(|e| tracing::error!("Could not connect to database: {e}"))?;
Ok(pool)
}
async fn get_user_by_id(&self, id: entity::ShortId) -> anyhow::Result<Option<entity::User>> {
let user = sqlx::query_as("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(self)
.await?;
Ok(user)
}
async fn get_user_by_login(&self, login: &str) -> anyhow::Result<Option<entity::User>> {
let user = sqlx::query_as("SELECT * FROM users WHERE login = $1")
.bind(login)
.fetch_optional(self)
.await?;
Ok(user)
}
}

6
src/entity/log.rs Normal file
View File

@@ -0,0 +1,6 @@
pub struct Log {
pub id: super::LongId,
pub user_id: super::ShortId,
pub action: String,
pub created_at: chrono::NaiveDateTime,
}

8
src/entity/message.rs Normal file
View File

@@ -0,0 +1,8 @@
use super::ShortId;
pub struct Message {
pub id: super::LongId,
pub user_id: ShortId,
pub content: String,
pub created_at: chrono::NaiveDateTime,
}

View File

@@ -0,0 +1,14 @@
#![allow(unused)]
mod log;
mod message;
mod secret;
mod user;
pub use log::Log;
pub use message::Message;
pub use secret::Secret;
pub use user::User;
pub type ShortId = i32;
pub type LongId = i64;

6
src/entity/secret.rs Normal file
View File

@@ -0,0 +1,6 @@
pub struct Secret {
pub id: super::ShortId,
pub user_id: super::ShortId,
pub title: String,
pub content: String,
}

7
src/entity/user.rs Normal file
View File

@@ -0,0 +1,7 @@
#[derive(sqlx::FromRow)]
pub struct User {
pub id: super::ShortId,
pub username: String,
pub password: String,
pub created_at: chrono::NaiveDateTime,
}

37
src/jwt.rs Normal file
View File

@@ -0,0 +1,37 @@
#![allow(unused)]
use chrono::{Duration, Local};
use serde::{Deserialize, Serialize};
use crate::config;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub exp: usize,
pub user_id: i32,
}
pub fn generate_jwt(user_id: i32) -> anyhow::Result<String> {
let claims = Claims {
exp: (Local::now() + Duration::days(1)).timestamp() as usize,
user_id,
};
let token = jsonwebtoken::encode(
&jsonwebtoken::Header::default(),
&claims,
&jsonwebtoken::EncodingKey::from_secret(config::config().jwt_secret.as_ref()),
)?;
Ok(token)
}
pub fn verify_jwt(token: &str) -> anyhow::Result<i32> {
let token_data = jsonwebtoken::decode::<Claims>(
token,
&jsonwebtoken::DecodingKey::from_secret(config::config().jwt_secret.as_ref()),
&jsonwebtoken::Validation::default(),
)?;
Ok(token_data.claims.user_id)
}

40
src/log.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::{fs, io};
use chrono::Local;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[allow(unused)]
pub struct LogGuard {
stdout_guard: WorkerGuard,
file_guard: WorkerGuard,
}
pub fn initialize() -> io::Result<LogGuard> {
let file = create_file()?;
let (file, file_guard) = tracing_appender::non_blocking(file);
let file_logger = tracing_subscriber::fmt::layer()
.with_writer(file)
.with_ansi(false);
let (stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
let stdout_logger = tracing_subscriber::fmt::layer().with_writer(stdout);
tracing_subscriber::registry()
.with(file_logger)
.with(stdout_logger)
.with(tracing_subscriber::EnvFilter::from_default_env())
.init();
Ok(LogGuard {
stdout_guard,
file_guard,
})
}
fn create_file() -> io::Result<fs::File> {
let filename = Local::now().format("%FT%H-%M-%S%.6f%:::z.log").to_string();
fs::create_dir_all("./logs")?;
fs::File::create(format!("./logs/{}", filename))
}

View File

@@ -1,6 +1,21 @@
use database::Database;
mod config;
mod context;
mod database;
mod entity;
mod jwt;
mod log;
mod web;
#[tokio::main]
async fn main() {
println!("Hello, world!");
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _guard = log::initialize()?;
let database = sqlx::any::AnyPool::init().await?;
let context = context::Context { database };
web::run(context).await?;
Ok(())
}

0
src/web/error.rs Normal file
View File

View File

@@ -0,0 +1,5 @@
use axum::{extract::Request, middleware::Next, response::IntoResponse};
pub async fn auth(request: Request, next: Next) -> impl IntoResponse {
next.run(request).await
}

View File

@@ -0,0 +1,5 @@
mod auth;
mod response_map;
pub use auth::auth;
pub use response_map::response_map;

View File

@@ -0,0 +1,5 @@
use axum::{extract::Request, middleware::Next, response::IntoResponse};
pub async fn response_map(request: Request, next: Next) -> impl IntoResponse {
next.run(request).await
}

View File

@@ -1,2 +1,34 @@
use crate::{config, context, database};
pub mod error;
pub mod middlware;
pub mod routes;
pub async fn run<D: database::Database + Clone + Send + Sync + 'static>(
context: context::Context<D>,
) -> anyhow::Result<()> {
let config = config::config();
let addr: std::net::SocketAddr = ([0, 0, 0, 0], config.port).into();
tracing::info!("Listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, router(context))
.with_graceful_shutdown(shutdown_signal())
.await?;
Ok(())
}
fn router<D: database::Database + Clone + Send + Sync + 'static>(
context: context::Context<D>,
) -> axum::Router {
axum::Router::new()
.route("/login", axum::routing::post(routes::login))
.with_state(context)
.layer(axum::middleware::from_fn(middlware::response_map))
}
async fn shutdown_signal() {
_ = tokio::signal::ctrl_c().await;
}

5
src/web/routes/login.rs Normal file
View File

@@ -0,0 +1,5 @@
use axum::response::IntoResponse;
pub async fn login() -> impl IntoResponse {
"login"
}

View File

@@ -0,0 +1,3 @@
mod login;
pub use login::login;