mirror of
https://github.com/danbulant/Portfolio
synced 2026-06-19 22:41:13 +00:00
commit
4473241db0
14 changed files with 299 additions and 1 deletions
33
api/src/guard/candidate_jwt.rs
Normal file
33
api/src/guard/candidate_jwt.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::outcome::Outcome;
|
||||
use rocket::request::{FromRequest, Request};
|
||||
|
||||
use portfolio_core::token::candidate_token::CandidateToken;
|
||||
use portfolio_core::token::decode_candidate_token;
|
||||
|
||||
pub struct TokenRequest(CandidateToken);
|
||||
|
||||
impl TokenRequest {
|
||||
pub fn to_token(self) -> CandidateToken {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for TokenRequest {
|
||||
type Error = Status;
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<TokenRequest, (Status, Status), ()> {
|
||||
if let Some(auth) = req.headers().get_one("Authorization") {
|
||||
let auth_string = auth.to_string();
|
||||
if auth_string.starts_with("Bearer") {
|
||||
let token = auth_string[6..auth_string.len()].trim();
|
||||
let token_data = decode_candidate_token(token.to_string());
|
||||
|
||||
if token_data.is_ok() {
|
||||
return Outcome::Success(TokenRequest(token_data.ok().unwrap().claims));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Outcome::Failure((Status::Unauthorized, Status::Unauthorized));
|
||||
}
|
||||
}
|
||||
1
api/src/guard/mod.rs
Normal file
1
api/src/guard/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod candidate_jwt;
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use guard::candidate_jwt::TokenRequest;
|
||||
use portfolio_core::error::ServiceError;
|
||||
use portfolio_core::services::candidate_service::CandidateService;
|
||||
use requests::LoginRequest;
|
||||
use rocket::http::Status;
|
||||
use rocket::{Rocket, Build};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::fairing::{self, AdHoc};
|
||||
|
|
@ -9,7 +15,11 @@ use portfolio_core::{Mutation};
|
|||
use migration::{MigratorTrait};
|
||||
use sea_orm_rocket::{Connection, Database};
|
||||
|
||||
|
||||
mod pool;
|
||||
mod guard;
|
||||
mod requests;
|
||||
|
||||
use pool::Db;
|
||||
|
||||
pub use entity::candidate;
|
||||
|
|
@ -17,6 +27,9 @@ pub use entity::candidate::Entity as Candidate;
|
|||
|
||||
use portfolio_core::crypto::random_8_char_string;
|
||||
|
||||
fn custom_err_from_service_err(service_err: ServiceError) -> Custom<String> {
|
||||
Custom(Status::from_code(service_err.0.code).unwrap_or_default(), service_err.1.to_string())
|
||||
}
|
||||
|
||||
#[post("/", data = "<post_form>")]
|
||||
async fn create(conn: Connection<'_, Db>, post_form: Json<candidate::Model>) -> Result<String, Custom<String>> {
|
||||
|
|
@ -32,6 +45,38 @@ async fn create(conn: Connection<'_, Db>, post_form: Json<candidate::Model>) ->
|
|||
Ok(plain_text_password)
|
||||
}
|
||||
|
||||
#[post("/login", data = "<login_form>")]
|
||||
async fn login(conn: Connection<'_, Db>, login_form: Json<LoginRequest>) -> Result<String, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
println!("{} {}", login_form.application_id, login_form.password);
|
||||
|
||||
let jwt = CandidateService::login(db,
|
||||
login_form.application_id,
|
||||
login_form.password.to_owned()).await;
|
||||
|
||||
if jwt.is_ok() {
|
||||
return Ok(
|
||||
jwt.ok().unwrap()
|
||||
);
|
||||
} else {
|
||||
return Err(
|
||||
custom_err_from_service_err(jwt.err().unwrap())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/whoami")]
|
||||
async fn whoami(conn: Connection<'_, Db>, token_req: Result<TokenRequest, Status>) -> Result<String, Custom<String>> {
|
||||
let db = conn.into_inner();
|
||||
let token = token_req.ok().unwrap().to_token();
|
||||
let user = CandidateService::authenticate_candidate(db, token).await;
|
||||
|
||||
match user {
|
||||
Ok(user) => Ok(format!("{} {}", user.name.unwrap(), user.surname.unwrap())),
|
||||
Err(e) => Err(custom_err_from_service_err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/hello")]
|
||||
async fn hello() -> &'static str {
|
||||
"Hello, world!"
|
||||
|
|
@ -49,7 +94,7 @@ async fn start() -> Result<(), rocket::Error> {
|
|||
.attach(Db::init())
|
||||
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
|
||||
//.mount("/", FileServer::from(relative!("/static")))
|
||||
.mount("/", routes![create, hello])
|
||||
.mount("/", routes![create, login, hello, whoami])
|
||||
.register("/", catchers![])
|
||||
.launch()
|
||||
.await
|
||||
|
|
|
|||
9
api/src/requests.rs
Normal file
9
api/src/requests.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use rocket::serde::{Serialize, Deserialize};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct LoginRequest {
|
||||
pub application_id: i32,
|
||||
pub password: String,
|
||||
}
|
||||
|
|
@ -4,10 +4,13 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
portfolio-entity = { path = "../entity" }
|
||||
rand = "0.8.5"
|
||||
argon2 = "0.4.1"
|
||||
chrono = "0.4.22"
|
||||
jsonwebtoken = "8.1.1"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
[dependencies.sea-orm]
|
||||
version = "^0.10.0"
|
||||
|
|
|
|||
18
core/src/error.rs
Normal file
18
core/src/error.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
pub struct Status {
|
||||
pub code: u16,
|
||||
}
|
||||
|
||||
pub const INVALID_CREDENTIALS_ERROR: ServiceError = ServiceError(Status { code: 401 },
|
||||
"Invalid credentials");
|
||||
pub const JWT_ERROR: ServiceError = ServiceError(Status { code: 500 },
|
||||
"Error while encoding JWT");
|
||||
|
||||
pub const USER_NOT_FOUND_ERROR: ServiceError = ServiceError(Status { code: 404 },
|
||||
"User not found");
|
||||
|
||||
pub const DB_ERROR: ServiceError = ServiceError(Status { code: 500 },
|
||||
"Database error");
|
||||
|
||||
pub const USER_NOT_FOUND_BY_JWT_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow
|
||||
"User not found, please contact technical support"); // Shouldn't ever happen
|
||||
pub struct ServiceError<'a>(pub Status, pub &'a str);
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
mod mutation;
|
||||
mod query;
|
||||
pub mod crypto;
|
||||
pub mod token;
|
||||
pub mod services;
|
||||
pub mod error;
|
||||
|
||||
pub use mutation::*;
|
||||
pub use query::*;
|
||||
|
|
|
|||
43
core/src/services/candidate_service.rs
Normal file
43
core/src/services/candidate_service.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use entity::candidate;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use crate::{crypto, Query, token::{generate_candidate_token, candidate_token::CandidateToken}, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID}};
|
||||
|
||||
pub struct CandidateService;
|
||||
|
||||
impl CandidateService {
|
||||
|
||||
pub async fn login(db: &DatabaseConnection, id: i32, password: String) -> Result<String, ServiceError> {
|
||||
let candidate = match Query::find_candidate_by_id(db, id).await {
|
||||
Ok(candidate) => match candidate {
|
||||
Some(candidate) => candidate,
|
||||
None => return Err(USER_NOT_FOUND_ERROR)
|
||||
},
|
||||
Err(_) => {return Err(DB_ERROR)}
|
||||
};
|
||||
|
||||
|
||||
let valid = crypto::verify_password(&password,&candidate.code )
|
||||
.expect("Invalid password");
|
||||
|
||||
if !valid {
|
||||
return Err(INVALID_CREDENTIALS_ERROR)
|
||||
}
|
||||
|
||||
let jwt = generate_candidate_token(candidate); // TODO better error handling
|
||||
Ok(jwt)
|
||||
|
||||
}
|
||||
|
||||
pub async fn authenticate_candidate(db: &DatabaseConnection, token: CandidateToken) -> Result<candidate::Model, ServiceError> {
|
||||
let candidate = match Query::find_candidate_by_id(db, token.application_id).await {
|
||||
Ok(candidate) => match candidate {
|
||||
Some(candidate) => candidate,
|
||||
None => return Err(USER_NOT_FOUND_BY_JWT_ID)
|
||||
},
|
||||
Err(_) => {return Err(DB_ERROR)}
|
||||
};
|
||||
|
||||
Ok(candidate)
|
||||
}
|
||||
}
|
||||
1
core/src/services/mod.rs
Normal file
1
core/src/services/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod candidate_service;
|
||||
9
core/src/token/admin_token.rs
Normal file
9
core/src/token/admin_token.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AdminToken {
|
||||
// issued at
|
||||
pub iat: i64,
|
||||
// expiration
|
||||
pub exp: i64,
|
||||
}
|
||||
26
core/src/token/candidate_token.rs
Normal file
26
core/src/token/candidate_token.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use chrono::Utc;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CandidateToken {
|
||||
// issued at
|
||||
pub iat: i64,
|
||||
// expiration
|
||||
pub exp: i64,
|
||||
pub application_id: i32,
|
||||
pub name: String,
|
||||
pub surname: String,
|
||||
}
|
||||
|
||||
impl CandidateToken {
|
||||
pub fn generate(application_id: i32, name: String, surname: String) -> Self {
|
||||
let now = Utc::now().timestamp();
|
||||
CandidateToken {
|
||||
iat: now,
|
||||
exp: now + 60 * 60, // 1 hour for now
|
||||
application_id,
|
||||
name,
|
||||
surname,
|
||||
}
|
||||
}
|
||||
}
|
||||
98
core/src/token/mod.rs
Normal file
98
core/src/token/mod.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
pub mod admin_token;
|
||||
pub mod candidate_token;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use entity::{admin, candidate};
|
||||
use jsonwebtoken::errors::Result;
|
||||
use jsonwebtoken::TokenData;
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey};
|
||||
use jsonwebtoken::{Header, Validation};
|
||||
|
||||
use admin_token::AdminToken;
|
||||
use candidate_token::CandidateToken;
|
||||
use serde::Deserialize;
|
||||
|
||||
const ONE_WEEK: i64 = 60 * 60 * 24 * 7;
|
||||
|
||||
pub fn generate_candidate_token(candidate: candidate::Model) -> String {
|
||||
let now = Utc::now().timestamp();
|
||||
let payload = CandidateToken {
|
||||
iat: now,
|
||||
exp: now + ONE_WEEK,
|
||||
application_id: candidate.application,
|
||||
name: candidate.name.unwrap_or_else(|| "".into()),
|
||||
surname: candidate.surname.unwrap_or_else(|| "".into()),
|
||||
};
|
||||
|
||||
jsonwebtoken::encode(
|
||||
&Header::default(),
|
||||
&payload,
|
||||
&EncodingKey::from_secret(include_bytes!("secret.key")),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn generate_admin_token(_admin: admin::Model) -> String {
|
||||
let now = Utc::now().timestamp();
|
||||
let payload = AdminToken {
|
||||
iat: now,
|
||||
exp: now + ONE_WEEK,
|
||||
};
|
||||
|
||||
jsonwebtoken::encode(
|
||||
&Header::default(),
|
||||
&payload,
|
||||
&EncodingKey::from_secret(include_bytes!("secret.key")),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn decode_token<T: for<'a> Deserialize<'a>>(token: String) -> Result<TokenData<T>> {
|
||||
jsonwebtoken::decode::<T>(
|
||||
&token,
|
||||
&DecodingKey::from_secret(include_bytes!("secret.key")),
|
||||
&Validation::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decode_candidate_token(token: String) -> Result<TokenData<CandidateToken>> {
|
||||
decode_token(token)
|
||||
}
|
||||
|
||||
pub fn decode_admin_token(token: String) -> Result<TokenData<AdminToken>> {
|
||||
decode_token(token)
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_token() {
|
||||
let candidate_model = candidate::Model {
|
||||
application: 101204,
|
||||
code: "random_code".to_string(),
|
||||
birth_surname: None,
|
||||
birthplace: None,
|
||||
birthdate: None,
|
||||
address: None,
|
||||
telephone: None,
|
||||
citizenship: None,
|
||||
sex: None,
|
||||
study: None,
|
||||
personal_identification_number: None,
|
||||
personal_identification_number_hash: None,
|
||||
public_key: "None".to_owned(),
|
||||
private_key: "None".to_owned(),
|
||||
created_at: Utc::now().naive_local(),
|
||||
updated_at: Utc::now().naive_local(),
|
||||
name: Some("Uplnej".to_string()),
|
||||
surname: Some("Magor".to_string()),
|
||||
email: Some("email.uchazece@centrum.cz".to_string()),
|
||||
};
|
||||
|
||||
let jwt = generate_candidate_token(candidate_model.clone());
|
||||
|
||||
let decoded = decode_candidate_token(jwt).unwrap();
|
||||
let token_claims = decoded.claims;
|
||||
assert_eq!(candidate_model.name.unwrap(), token_claims.name);
|
||||
assert_eq!(candidate_model.surname.unwrap(), token_claims.surname);
|
||||
}
|
||||
1
core/src/token/secret.key
Normal file
1
core/src/token/secret.key
Normal file
|
|
@ -0,0 +1 @@
|
|||
temp
|
||||
8
entity/src/mod.rs
Normal file
8
entity/src/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod admin;
|
||||
pub mod candidate;
|
||||
pub mod parent;
|
||||
pub mod session;
|
||||
Loading…
Reference in a new issue