.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/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]
|
||||
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
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;
|
||||
|
||||
#[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
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 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