.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
|
/logs
|
||||||
|
*.db
|
||||||
|
|||||||
1074
Cargo.lock
generated
1074
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
@@ -1,15 +1,24 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nir"
|
edition = "2021"
|
||||||
version = "0.1.0"
|
name = "nir"
|
||||||
edition = "2021"
|
version = "0.1.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.5"
|
anyhow = "1.0.82"
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
axum = "0.7.5"
|
||||||
jwt = "0.16.0"
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
serde = { version = "1.0.198", features = ["derive"] }
|
figment = { version = "0.10.18", features = ["env", "toml"] }
|
||||||
serde_json = "1.0.116"
|
jsonwebtoken = "9.3.0"
|
||||||
sqlx = { version = "0.7.4", features = ["chrono"] }
|
serde = { version = "1.0.198", features = ["derive"] }
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
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
6
config.toml
Normal 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
31
src/config.rs
Normal 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
6
src/context.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use crate::database::Database;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Context<D: Database> {
|
||||||
|
pub database: D,
|
||||||
|
}
|
||||||
@@ -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
6
src/entity/log.rs
Normal 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
8
src/entity/message.rs
Normal 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,
|
||||||
|
}
|
||||||
@@ -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
6
src/entity/secret.rs
Normal 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
7
src/entity/user.rs
Normal 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
37
src/jwt.rs
Normal 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
40
src/log.rs
Normal 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))
|
||||||
|
}
|
||||||
19
src/main.rs
19
src/main.rs
@@ -1,6 +1,21 @@
|
|||||||
|
use database::Database;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod context;
|
||||||
|
mod database;
|
||||||
|
mod entity;
|
||||||
|
mod jwt;
|
||||||
|
mod log;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Hello, world!");
|
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
0
src/web/error.rs
Normal file
5
src/web/middlware/auth.rs
Normal file
5
src/web/middlware/auth.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod auth;
|
||||||
|
mod response_map;
|
||||||
|
|
||||||
|
pub use auth::auth;
|
||||||
|
pub use response_map::response_map;
|
||||||
|
|||||||
5
src/web/middlware/response_map.rs
Normal file
5
src/web/middlware/response_map.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -1,2 +1,34 @@
|
|||||||
|
use crate::{config, context, database};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
pub mod middlware;
|
pub mod middlware;
|
||||||
pub mod routes;
|
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
5
src/web/routes/login.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
|
pub async fn login() -> impl IntoResponse {
|
||||||
|
"login"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod login;
|
||||||
|
|
||||||
|
pub use login::login;
|
||||||
Reference in New Issue
Block a user