Add tokens routes
This commit is contained in:
parent
3081757536
commit
544513d118
28
moneymgr_backend/Cargo.lock
generated
28
moneymgr_backend/Cargo.lock
generated
@ -1598,6 +1598,9 @@ name = "ipnet"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
@ -1660,6 +1663,29 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex"
|
||||
version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126"
|
||||
dependencies = [
|
||||
"lazy-regex-proc_macros",
|
||||
"once_cell",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex-proc_macros"
|
||||
version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@ -1826,6 +1852,8 @@ dependencies = [
|
||||
"diesel_migrations",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"ipnet",
|
||||
"lazy-regex",
|
||||
"lazy_static",
|
||||
"light-openid",
|
||||
"log",
|
||||
|
@ -24,3 +24,5 @@ futures-util = "0.3.31"
|
||||
serde_json = "1.0.140"
|
||||
light-openid = "1.0.4"
|
||||
rand = "0.9.0"
|
||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||
lazy-regex = "3.4.1"
|
@ -10,20 +10,19 @@ CREATE TABLE users
|
||||
CREATE TABLE token
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
label VARCHAR(150) NOT NULL,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_update BIGINT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
token_value VARCHAR(150) NOT NULL,
|
||||
time_used BIGINT NOT NULL,
|
||||
max_inactivity INTEGER,
|
||||
ip_restriction VARCHAR(50),
|
||||
max_inactivity INTEGER NOT NULL,
|
||||
ip_net VARCHAR(50),
|
||||
read_only BOOLEAN NOT NULL DEFAULT true,
|
||||
right_account BOOLEAN NOT NULL DEFAULT false,
|
||||
right_movement BOOLEAN NOT NULL DEFAULT false,
|
||||
right_inbox BOOLEAN NOT NULL DEFAULT false,
|
||||
right_attachment BOOLEAN NOT NULL DEFAULT false,
|
||||
right_user BOOLEAN NOT NULL DEFAULT false
|
||||
right_auth BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
CREATE TABLE attachment
|
||||
@ -33,7 +32,7 @@ CREATE TABLE attachment
|
||||
mime_type VARCHAR(150) NOT NULL,
|
||||
sha512 VARCHAR(130) NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE RESTRICT
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE account
|
||||
|
@ -1,3 +1,8 @@
|
||||
//! # Project constants
|
||||
|
||||
/// Length of generated tokens
|
||||
pub const TOKENS_LEN: usize = 50;
|
||||
|
||||
/// Header used to authenticate API requests made using a token
|
||||
pub const API_TOKEN_HEADER: &str = "X-Auth-Token";
|
||||
|
||||
|
@ -2,7 +2,7 @@ use crate::app_config::AppConfig;
|
||||
use crate::controllers::{HttpFailure, HttpResult};
|
||||
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||
use crate::extractors::money_session::MoneySession;
|
||||
use crate::services::users_service;
|
||||
use crate::services::{tokens_service, users_service};
|
||||
use actix_remote_ip::RemoteIP;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use light_openid::primitives::OpenIDConfig;
|
||||
@ -118,6 +118,10 @@ pub async fn sign_out(auth: AuthExtractor, session: MoneySession) -> HttpResult
|
||||
session.unset_current_user()?;
|
||||
}
|
||||
|
||||
AuthenticatedMethod::Token(token) => {
|
||||
tokens_service::delete(token.user_id(), token.id()).await?;
|
||||
}
|
||||
|
||||
AuthenticatedMethod::Dev => {
|
||||
// Nothing to be done, user is always authenticated
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use std::error::Error;
|
||||
|
||||
pub mod auth_controller;
|
||||
pub mod server_controller;
|
||||
pub mod tokens_controller;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum HttpFailure {
|
||||
|
@ -8,15 +8,45 @@ pub async fn robots_txt() -> HttpResponse {
|
||||
.body("User-agent: *\nDisallow: /\n")
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct LenConstraints {
|
||||
min: usize,
|
||||
max: usize,
|
||||
}
|
||||
|
||||
impl LenConstraints {
|
||||
pub fn new(min: usize, max: usize) -> Self {
|
||||
Self { min, max }
|
||||
}
|
||||
pub fn not_empty(max: usize) -> Self {
|
||||
Self { min: 1, max }
|
||||
}
|
||||
pub fn max_only(max: usize) -> Self {
|
||||
Self { min: 0, max }
|
||||
}
|
||||
|
||||
pub fn check_str(&self, s: &str) -> bool {
|
||||
s.len() >= self.min && s.len() <= self.max
|
||||
}
|
||||
|
||||
pub fn check_u32(&self, v: u32) -> bool {
|
||||
v >= self.min as u32 && v <= self.max as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ServerConstraints {
|
||||
// TODO
|
||||
pub token_name: LenConstraints,
|
||||
pub token_ip_net: LenConstraints,
|
||||
pub token_max_inactivity: LenConstraints,
|
||||
}
|
||||
|
||||
impl Default for ServerConstraints {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// TODO
|
||||
token_name: LenConstraints::new(5, 255),
|
||||
token_ip_net: LenConstraints::max_only(44),
|
||||
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
moneymgr_backend/src/controllers/tokens_controller.rs
Normal file
87
moneymgr_backend/src/controllers/tokens_controller.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::controllers::server_controller::ServerConstraints;
|
||||
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||
use crate::models::tokens::{Token, TokenID};
|
||||
use crate::services::tokens_service;
|
||||
use crate::services::tokens_service::NewTokenInfo;
|
||||
use actix_web::{HttpResponse, web};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateTokenBody {
|
||||
name: String,
|
||||
ip_net: Option<ipnet::IpNet>,
|
||||
max_inactivity: u32,
|
||||
read_only: bool,
|
||||
right_account: bool,
|
||||
right_movement: bool,
|
||||
right_inbox: bool,
|
||||
right_attachment: bool,
|
||||
right_auth: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct CreateTokenResult {
|
||||
#[serde(flatten)]
|
||||
info: Token,
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// Create a new token
|
||||
pub async fn create(auth: AuthExtractor, req: web::Json<CreateTokenBody>) -> HttpResult {
|
||||
if matches!(auth.method, AuthenticatedMethod::Token(_)) {
|
||||
return Ok(HttpResponse::Forbidden()
|
||||
.json("It is not allowed to create a token using another token!"));
|
||||
}
|
||||
|
||||
let constraints = ServerConstraints::default();
|
||||
|
||||
if !lazy_regex::regex!("^[a-zA-Z0-9 :-]+$").is_match(&req.name) {
|
||||
return Ok(HttpResponse::BadRequest().json("Token name contains invalid characters!"));
|
||||
}
|
||||
|
||||
if !constraints.token_name.check_str(&req.name) {
|
||||
return Ok(HttpResponse::BadRequest().json("Invalid token name length!"));
|
||||
}
|
||||
|
||||
if !constraints
|
||||
.token_max_inactivity
|
||||
.check_u32(req.max_inactivity)
|
||||
{
|
||||
return Ok(HttpResponse::BadRequest().json("Invalid token max inactivity!"));
|
||||
}
|
||||
|
||||
let token = tokens_service::create(NewTokenInfo {
|
||||
user_id: auth.user_id(),
|
||||
max_inactivity: req.max_inactivity,
|
||||
ip_net: req.ip_net,
|
||||
name: req.name.clone(),
|
||||
read_only: req.read_only,
|
||||
right_account: req.right_account,
|
||||
right_movement: req.right_movement,
|
||||
right_inbox: req.right_inbox,
|
||||
right_attachment: req.right_attachment,
|
||||
right_auth: req.right_auth,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(CreateTokenResult {
|
||||
token: token.token_value.to_string(),
|
||||
info: token,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the list of tokens of the user
|
||||
pub async fn get_list(auth: AuthExtractor) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(tokens_service::get_list_user(auth.user_id()).await?))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct TokenIDInPath {
|
||||
id: TokenID,
|
||||
}
|
||||
|
||||
/// Delete an API access token
|
||||
pub async fn delete(auth: AuthExtractor, path: web::Path<TokenIDInPath>) -> HttpResult {
|
||||
tokens_service::delete(auth.user_id(), path.id).await?;
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::extractors::money_session::MoneySession;
|
||||
use crate::models::users::User;
|
||||
use crate::models::tokens::Token;
|
||||
use crate::models::users::{User, UserID};
|
||||
use crate::services::users_service;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::error::ErrorPreconditionFailed;
|
||||
@ -12,6 +13,8 @@ pub enum AuthenticatedMethod {
|
||||
Cookie,
|
||||
/// User is authenticated through command line, for debugging purposes only
|
||||
Dev,
|
||||
// TODO : token implementation
|
||||
Token(Token),
|
||||
}
|
||||
|
||||
/// Authentication extractor. Extract authentication information from request
|
||||
@ -20,6 +23,13 @@ pub struct AuthExtractor {
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
impl AuthExtractor {
|
||||
/// Get current user ID
|
||||
pub fn user_id(&self) -> UserID {
|
||||
self.user.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for AuthExtractor {
|
||||
type Error = Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
@ -9,7 +9,7 @@ use actix_web::middleware::Logger;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use moneymgr_backend::app_config::AppConfig;
|
||||
use moneymgr_backend::connections::{db_connection, s3_connection};
|
||||
use moneymgr_backend::controllers::{auth_controller, server_controller};
|
||||
use moneymgr_backend::controllers::*;
|
||||
use moneymgr_backend::services::users_service;
|
||||
use moneymgr_backend::{constants, routines};
|
||||
|
||||
@ -90,6 +90,16 @@ async fn main() -> std::io::Result<()> {
|
||||
"/api/auth/sign_out",
|
||||
web::get().to(auth_controller::sign_out),
|
||||
)
|
||||
// Tokens controller
|
||||
.route("/api/tokens", web::post().to(tokens_controller::create))
|
||||
.route(
|
||||
"/api/tokens/list",
|
||||
web::get().to(tokens_controller::get_list),
|
||||
)
|
||||
.route(
|
||||
"/api/tokens/{id}",
|
||||
web::delete().to(tokens_controller::delete),
|
||||
)
|
||||
})
|
||||
.bind(AppConfig::get().listen_address.as_str())?
|
||||
.run()
|
||||
|
@ -1 +1,2 @@
|
||||
pub mod tokens;
|
||||
pub mod users;
|
||||
|
72
moneymgr_backend/src/models/tokens.rs
Normal file
72
moneymgr_backend/src/models/tokens.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use crate::models::users::UserID;
|
||||
use crate::schema::*;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
use std::cmp::min;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
pub struct TokenID(pub i32);
|
||||
|
||||
#[derive(Default, Queryable, Debug, Clone, serde::Serialize)]
|
||||
pub struct Token {
|
||||
id: i32,
|
||||
pub name: String,
|
||||
pub time_create: i64,
|
||||
user_id: i32,
|
||||
#[serde(skip_serializing)]
|
||||
pub token_value: String,
|
||||
pub time_used: i64,
|
||||
pub max_inactivity: i32,
|
||||
ip_net: Option<String>,
|
||||
pub read_only: bool,
|
||||
pub right_account: bool,
|
||||
pub right_movement: bool,
|
||||
pub right_inbox: bool,
|
||||
pub right_attachment: bool,
|
||||
pub right_auth: bool,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn id(&self) -> TokenID {
|
||||
TokenID(self.id)
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> UserID {
|
||||
UserID(self.user_id)
|
||||
}
|
||||
|
||||
pub fn ip_net(&self) -> Option<ipnet::IpNet> {
|
||||
self.ip_net
|
||||
.as_ref()
|
||||
.map(|i| ipnet::IpNet::from_str(i).unwrap())
|
||||
}
|
||||
|
||||
pub fn shall_update_time_used(&self) -> bool {
|
||||
let refresh_interval = min(600, self.max_inactivity / 10);
|
||||
|
||||
(self.time_used as u64) < time() - refresh_interval as u64
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
(self.time_used + self.max_inactivity as i64) < time() as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = token)]
|
||||
pub struct NewToken<'a> {
|
||||
pub name: &'a str,
|
||||
pub user_id: i32,
|
||||
pub time_create: i64,
|
||||
pub time_used: i64,
|
||||
pub max_inactivity: i32,
|
||||
pub ip_net: Option<&'a str>,
|
||||
pub token_value: &'a str,
|
||||
pub read_only: bool,
|
||||
pub right_account: bool,
|
||||
pub right_movement: bool,
|
||||
pub right_inbox: bool,
|
||||
pub right_attachment: bool,
|
||||
pub right_auth: bool,
|
||||
}
|
@ -19,6 +19,7 @@ pub async fn main_routine() {
|
||||
}
|
||||
|
||||
async fn exec_routine() -> anyhow::Result<()> {
|
||||
// TODO
|
||||
// TODO : remove orphan attachment
|
||||
// TODO : remove outdated tokens
|
||||
Ok(())
|
||||
}
|
||||
|
@ -55,22 +55,21 @@ diesel::table! {
|
||||
token (id) {
|
||||
id -> Int4,
|
||||
#[max_length = 150]
|
||||
label -> Varchar,
|
||||
name -> Varchar,
|
||||
time_create -> Int8,
|
||||
time_update -> Int8,
|
||||
user_id -> Int4,
|
||||
#[max_length = 150]
|
||||
token_value -> Varchar,
|
||||
time_used -> Int8,
|
||||
max_inactivity -> Nullable<Int4>,
|
||||
max_inactivity -> Int4,
|
||||
#[max_length = 50]
|
||||
ip_restriction -> Nullable<Varchar>,
|
||||
ip_net -> Nullable<Varchar>,
|
||||
read_only -> Bool,
|
||||
right_account -> Bool,
|
||||
right_movement -> Bool,
|
||||
right_inbox -> Bool,
|
||||
right_attachment -> Bool,
|
||||
right_user -> Bool,
|
||||
right_auth -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
pub mod tokens_service;
|
||||
pub mod users_service;
|
||||
|
108
moneymgr_backend/src/services/tokens_service.rs
Normal file
108
moneymgr_backend/src/services/tokens_service.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::connections::db_connection::db;
|
||||
use crate::constants;
|
||||
use crate::models::tokens::{NewToken, Token, TokenID};
|
||||
use crate::models::users::UserID;
|
||||
use crate::schema::token;
|
||||
use crate::utils::rand_utils::rand_string;
|
||||
use crate::utils::time_utils::time;
|
||||
|
||||
pub struct NewTokenInfo {
|
||||
pub user_id: UserID,
|
||||
pub name: String,
|
||||
pub max_inactivity: u32,
|
||||
pub ip_net: Option<ipnet::IpNet>,
|
||||
pub read_only: bool,
|
||||
pub right_account: bool,
|
||||
pub right_movement: bool,
|
||||
pub right_inbox: bool,
|
||||
pub right_attachment: bool,
|
||||
pub right_auth: bool,
|
||||
}
|
||||
|
||||
/// Create a new token
|
||||
pub async fn create(new_token: NewTokenInfo) -> anyhow::Result<Token> {
|
||||
let ip_net = new_token.ip_net.map(|i| i.to_string());
|
||||
let token = rand_string(constants::TOKENS_LEN);
|
||||
let t = NewToken {
|
||||
name: &new_token.name,
|
||||
user_id: new_token.user_id.0,
|
||||
time_create: time() as i64,
|
||||
time_used: time() as i64,
|
||||
max_inactivity: new_token.max_inactivity as i32,
|
||||
read_only: new_token.read_only,
|
||||
ip_net: ip_net.as_deref(),
|
||||
token_value: &token,
|
||||
right_auth: new_token.right_auth,
|
||||
right_account: new_token.right_account,
|
||||
right_movement: new_token.right_movement,
|
||||
right_inbox: new_token.right_inbox,
|
||||
right_attachment: new_token.right_attachment,
|
||||
};
|
||||
|
||||
let res = diesel::insert_into(token::table)
|
||||
.values(&t)
|
||||
.get_result(&mut db()?)?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get a single token by its id
|
||||
pub async fn get_by_id(token_id: TokenID) -> anyhow::Result<Token> {
|
||||
Ok(token::table
|
||||
.filter(token::dsl::id.eq(token_id.0))
|
||||
.get_result(&mut db()?)?)
|
||||
}
|
||||
|
||||
/// Get a single token by its name
|
||||
pub fn get_by_name(name: &str) -> anyhow::Result<Token> {
|
||||
Ok(token::table
|
||||
.filter(token::dsl::name.eq(name))
|
||||
.get_result(&mut db()?)?)
|
||||
}
|
||||
|
||||
/// Get a single token by its value
|
||||
pub async fn get_by_value(value: &str) -> anyhow::Result<Token> {
|
||||
Ok(token::table
|
||||
.filter(token::dsl::token_value.eq(value))
|
||||
.get_result(&mut db()?)?)
|
||||
}
|
||||
|
||||
/// Get the token of a user
|
||||
pub async fn get_list_user(id: UserID) -> anyhow::Result<Vec<Token>> {
|
||||
Ok(token::table
|
||||
.filter(token::dsl::user_id.eq(id.0))
|
||||
.get_results(&mut db()?)?)
|
||||
}
|
||||
|
||||
/// Update last used value of a token
|
||||
pub async fn update_time_used(token: &Token) -> anyhow::Result<()> {
|
||||
diesel::update(token::dsl::token.filter(token::dsl::id.eq(token.id().0)))
|
||||
.set(token::dsl::time_used.eq(time() as i64))
|
||||
.execute(&mut db()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete the token of a user
|
||||
pub async fn delete(user_id: UserID, token_id: TokenID) -> anyhow::Result<()> {
|
||||
diesel::delete(
|
||||
token::dsl::token.filter(
|
||||
token::dsl::id
|
||||
.eq(token_id.0)
|
||||
.and(token::dsl::user_id.eq(user_id.0)),
|
||||
),
|
||||
)
|
||||
.execute(&mut db()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove outdated token
|
||||
pub async fn cleanup() -> anyhow::Result<()> {
|
||||
let query = format!(
|
||||
"DELETE from token where last_used + max_inactivity < {};",
|
||||
time()
|
||||
);
|
||||
diesel::sql_query(query).execute(&mut db()?)?;
|
||||
Ok(())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user