add support for users/JWT

This commit is contained in:
Franek 2025-04-17 13:16:03 +02:00
parent 981750d0b7
commit 8a1d0cf0bb
13 changed files with 593 additions and 6 deletions

372
Cargo.lock generated
View File

@ -29,7 +29,7 @@ dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"base64",
"base64 0.22.1",
"bitflags",
"brotli",
"bytes",
@ -224,17 +224,48 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "api"
version = "1.0.0"
dependencies = [
"actix-web",
"chrono",
"diesel",
"jsonwebtoken",
"rust-argon2",
"serde",
"serde_json",
"tokio",
"toml",
]
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "autocfg"
version = "1.4.0"
@ -256,6 +287,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@ -268,6 +305,17 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "blake2b_simd"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -298,6 +346,12 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -336,6 +390,27 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "cookie"
version = "0.16.2"
@ -347,6 +422,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -448,6 +529,7 @@ checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a"
dependencies = [
"bitflags",
"byteorder",
"chrono",
"diesel_derives",
"itoa",
"pq-sys",
@ -603,6 +685,19 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.1"
@ -675,6 +770,30 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
@ -851,6 +970,31 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.22.1",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "language-tags"
version = "0.3.2"
@ -935,12 +1079,40 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.36.7"
@ -979,6 +1151,16 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "pem"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
dependencies = [
"base64 0.22.1",
"serde",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -1084,7 +1266,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
"getrandom 0.3.1",
]
[[package]]
@ -1131,12 +1313,43 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.15",
"libc",
"untrusted",
"windows-sys",
]
[[package]]
name = "rust-argon2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8"
dependencies = [
"base64 0.21.7",
"blake2b_simd",
"constant_time_eq",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
@ -1237,6 +1450,18 @@ dependencies = [
"libc",
]
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -1296,6 +1521,26 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.39"
@ -1463,6 +1708,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
@ -1513,6 +1764,123 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -1,3 +1,5 @@
cargo-features = ["edition2024"]
[package]
name = "api"
description = "API for sador.me website"
@ -7,6 +9,13 @@ edition = "2024"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
diesel = { version = "2.1", features = ["postgres", "r2d2"] }
diesel = { version = "2.1", features = ["postgres", "r2d2", "chrono"] }
rust-argon2 = "2"
jsonwebtoken = "9"
serde_json = "1"
actix-web = "4"
toml = "0.8"
[dependencies.chrono]
version = "0.4"
features = ["serde"]

View File

@ -0,0 +1 @@
DROP TABLE users;

View File

@ -0,0 +1,11 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL,
power_level SMALLINT NOT NULL,
is_owner BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX idx_username ON users(username);

View File

@ -4,6 +4,7 @@ use serde::Deserialize;
pub struct Config {
pub database_url: String,
pub api_version: String,
pub jwt_secret: String,
pub host: String,
pub port: u16
}

View File

@ -1 +1,2 @@
pub mod post;
pub mod user;

View File

@ -0,0 +1,22 @@
use serde::{Serialize, Deserialize};
use chrono::NaiveDateTime;
use diesel::prelude::*;
#[derive(Queryable, Serialize, Deserialize)]
pub struct User {
pub id: i32,
pub username: String,
pub password: String,
pub power_level: i16,
pub is_owner: bool,
pub created_at: NaiveDateTime,
pub updated_at: Option<NaiveDateTime>
}
#[derive(Insertable)]
#[diesel(table_name = crate::schema::users)]
pub struct NewUser<'a> {
pub username: &'a str,
pub password: &'a str
}

View File

@ -4,10 +4,11 @@ use actix_web::middleware::NormalizePath;
mod database;
mod config;
mod schema;
mod utils;
mod v1;
#[derive(Clone)]
struct State {
pub(crate) struct State {
db: database::DbPool,
config: config::Config
}

View File

@ -8,3 +8,20 @@ diesel::table! {
author_id -> Int4,
}
}
diesel::table! {
users (id) {
id -> Int4,
username -> Text,
password -> Text,
power_level -> Int2,
is_owner -> Bool,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
diesel::allow_tables_to_appear_in_same_query!(
posts,
users,
);

90
src/utils/jwt.rs Normal file
View File

@ -0,0 +1,90 @@
use actix_web::{
error::{ErrorBadRequest, ErrorInternalServerError},
web::Data, HttpRequest, dev::Payload,
FromRequest, Error
};
use jsonwebtoken::{Header, EncodingKey, DecodingKey, Validation};
use serde::{Serialize, Deserialize};
use std::future::{self, ready};
use chrono::Utc;
use crate::State;
static WEEK: i64 = 60 * 60 * 24 * 7;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
}
impl Claims {
pub fn generate_token(secret: &str, id: &str) -> String {
let now = Utc::now().timestamp();
let payload = Claims {
exp: (now + WEEK) as usize,
sub: id.to_string(),
};
jsonwebtoken::encode(
&Header::default(),
&payload,
&EncodingKey::from_secret(secret.as_ref())
).unwrap()
}
}
#[derive(Deserialize, Debug, Serialize)]
pub struct AuthorizedUser {
pub token: String,
pub sub: String,
}
impl FromRequest for AuthorizedUser {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let state = req.app_data::<Data<State>>();
let header = req.headers().get("Authorization");
if header.is_none() {
return future::ready(Err(ErrorBadRequest("no Authorization header specified")))
}
if state.is_none() {
return future::ready(Err(ErrorInternalServerError("cannot retrieve JWT token")))
}
let state = state.unwrap();
match header.unwrap().to_str() {
Ok(v) => {
let parts: Vec<&str> = v.split(" ").collect();
if parts.len() != 2 {
return ready(Err(ErrorBadRequest("Not Authorized")));
}
let claims = jsonwebtoken::decode::<Claims>(
parts[1],
&DecodingKey::from_secret(state.config.jwt_secret.as_ref()),
&Validation::default()
);
if claims.is_err() {
return ready(Err(ErrorBadRequest("Not Authorized")));
}
let claims = claims.unwrap().claims;
let authorized_user = AuthorizedUser {
sub: claims.sub,
token: v.to_string()
};
ready(Ok(authorized_user))
}
Err(e) => ready(Err(ErrorBadRequest(e))),
}
}
}

1
src/utils/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod jwt;

63
src/v1/auth.rs Normal file
View File

@ -0,0 +1,63 @@
use actix_web::{
web::{ServiceConfig, Data, Json, scope},
HttpResponse, Responder, post
};
use argon2::verify_encoded;
use diesel::{ExpressionMethods, QueryDsl, OptionalExtension, RunQueryDsl};
use serde::{Serialize, Deserialize};
use crate::{database::models::user::User, utils::jwt::Claims};
use crate::schema::users::dsl::*;
use crate::State;
#[derive(Serialize, Deserialize)]
pub struct UserBody {
pub username: String,
pub password: String
}
#[derive(Serialize)]
struct TokenResponse {
ok: bool,
token: String
}
#[post("/login")]
async fn login(
state: Data<State>,
login_data: Json<UserBody>,
) -> impl Responder {
let mut conn = match state.db.get() {
Ok(conn) => conn,
Err(why) => return HttpResponse::InternalServerError()
.body(format!("database connection error: {}", why)),
};
let secret = &state.config.jwt_secret;
let user_result = users
.filter(username.eq(&login_data.username))
.first::<User>(&mut conn)
.optional();
match user_result {
Ok(Some(user)) => match verify_encoded(&user.password, login_data.password.as_bytes()) {
Ok(true) => {
let token = Claims::generate_token(secret.as_str(), &user.id.to_string());
HttpResponse::Ok().json(TokenResponse {
ok: true,
token
})
}
Ok(false) => HttpResponse::Unauthorized().body("Invalid username or password"),
Err(err) => HttpResponse::InternalServerError().body(format!("Error verifying password: {}", err)),
},
Ok(None) | Err(_) => HttpResponse::Unauthorized().body("Invalid username or password"),
}
}
pub fn init_routes(cfg: &mut ServiceConfig) {
cfg.service(scope("/auth")
.service(login)
);
}

View File

@ -3,6 +3,7 @@ use serde::Serialize;
use crate::State;
mod posts;
mod auth;
#[derive(Serialize)]
struct PingResponse {
@ -23,5 +24,6 @@ pub fn init_routes(cfg: &mut ServiceConfig) {
.service(index)
.service(scope("/api/v1")
.configure(posts::init_routes)
.configure(auth::init_routes)
);
}