Compare commits

..
Author SHA1 Message Date
phoenix 403a941ece cargo update
textsender_models PR / Rustfmt (pull_request) Successful in 1m1s
textsender_models PR / Clippy (pull_request) Successful in 1m14s
Release Tagging / release (pull_request) Successful in 27s
textsender_models PR / Check (pull_request) Successful in 1m42s
2026-07-01 22:58:27 -04:00
phoenix 904e50f981 Some refactoring
textsender_models PR / Rustfmt (pull_request) Successful in 32s
textsender_models PR / Check (pull_request) Has been cancelled
textsender_models PR / Clippy (pull_request) Has been cancelled
Release Tagging / release (pull_request) Successful in 35s
2026-07-01 22:57:45 -04:00
phoenix 6b84cc42d6 Removing async from functions
textsender_models PR / Rustfmt (pull_request) Successful in 35s
Release Tagging / release (pull_request) Successful in 39s
textsender_models PR / Clippy (pull_request) Successful in 1m23s
textsender_models PR / Check (pull_request) Successful in 1m29s
2026-07-01 22:53:09 -04:00
phoenix b25b651241 bump: textsender_models
textsender_models PR / Rustfmt (pull_request) Successful in 48s
textsender_models PR / Check (pull_request) Successful in 1m2s
Release Tagging / release (pull_request) Successful in 31s
textsender_models PR / Clippy (pull_request) Successful in 1m38s
2026-07-01 22:49:48 -04:00
phoenix cc6e6286c7 Adding test
textsender_models PR / Rustfmt (pull_request) Successful in 1m9s
textsender_models PR / Check (pull_request) Successful in 1m20s
textsender_models PR / Clippy (pull_request) Successful in 1m27s
Release Tagging / release (pull_request) Successful in 34s
2026-07-01 22:47:36 -04:00
13 changed files with 538 additions and 373 deletions
+7 -7
View File
@@ -1,4 +1,4 @@
name: schedtxt_models PR name: textsender_models PR
on: on:
pull_request: pull_request:
@@ -14,10 +14,10 @@ jobs:
name: Check name: Check
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check - run: cargo check
@@ -25,10 +25,10 @@ jobs:
name: Rustfmt name: Rustfmt
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt - run: rustup component add rustfmt
- run: cargo fmt --all -- --check - run: cargo fmt --all -- --check
@@ -37,10 +37,10 @@ jobs:
name: Clippy name: Clippy
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: rustup component add clippy - run: rustup component add clippy
- run: cargo clippy -- -D warnings - run: cargo clippy -- -D warnings
+2 -2
View File
@@ -13,14 +13,14 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
fetch-depth: 0 # Important for git describe --tags fetch-depth: 0 # Important for git describe --tags
- name: Install Rust - name: Install Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
components: cargo components: cargo
- name: Extract Version from Cargo.toml - name: Extract Version from Cargo.toml
+10 -10
View File
@@ -14,10 +14,10 @@ jobs:
name: Check name: Check
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check - run: cargo check
@@ -25,10 +25,10 @@ jobs:
name: Test Suite name: Test Suite
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test - run: cargo test
@@ -36,10 +36,10 @@ jobs:
name: Rustfmt name: Rustfmt
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: rustup component add rustfmt - run: rustup component add rustfmt
- run: cargo fmt --all -- --check - run: cargo fmt --all -- --check
@@ -48,10 +48,10 @@ jobs:
name: Clippy name: Clippy
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: rustup component add clippy - run: rustup component add clippy
- run: cargo clippy -- -D warnings - run: cargo clippy -- -D warnings
@@ -60,9 +60,9 @@ jobs:
name: build name: build
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.97 toolchain: 1.96
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo build - run: cargo build
Generated
+466 -191
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -1,17 +1,16 @@
[package] [package]
name = "schedtxt_models" name = "textsender_models"
version = "0.5.3" version = "0.4.11"
edition = "2024" edition = "2024"
rust-version = "1.97" rust-version = "1.96"
license = "MIT" description = "Models used for the textsender project"
description = "Models used for the schedtxt project"
[dependencies] [dependencies]
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150" } serde_json = { version = "1.0.150" }
time = { version = "0.3.53", features = ["formatting", "macros", "parsing", "serde"] } time = { version = "0.3.49", features = ["formatting", "macros", "parsing", "serde"] }
sqlx = { version = "0.9.0", features = ["runtime-tokio", "tls-native-tls", "postgres", "time", "uuid"] } sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
uuid = { version = "1.23.5", features = ["v4", "serde"] } uuid = { version = "1.23.3", features = ["v4", "serde"] }
dotenvy = { version = "0.15.7" } dotenvy = { version = "0.15.7" }
const_format = { version = "0.2.36" } const_format = { version = "0.2.36" }
josekit = { version = "0.10.3" } josekit = { version = "0.10.3" }
-22
View File
@@ -1,22 +0,0 @@
Copyright (c) 2026 Kun Deng.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
-2
View File
@@ -1,2 +0,0 @@
# schedtxt_models
Models library for the `schedtxt` project.
+3
View File
@@ -4,10 +4,13 @@
pub struct Contact { pub struct Contact {
#[serde(skip_serializing_if = "crate::init::is_uuid_nil")] #[serde(skip_serializing_if = "crate::init::is_uuid_nil")]
pub id: Option<uuid::Uuid>, pub id: Option<uuid::Uuid>,
// empty?
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub firstname: Option<String>, pub firstname: Option<String>,
// empty?
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub lastname: Option<String>, pub lastname: Option<String>,
// empty?
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>, pub nickname: Option<String>,
pub phone_number: String, pub phone_number: String,
+2 -2
View File
@@ -40,7 +40,7 @@ pub fn get_root_directory() -> crate::envy::EnvVar {
pub fn get_app_base_api_url() -> crate::envy::EnvVar { pub fn get_app_base_api_url() -> crate::envy::EnvVar {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let key = crate::envy::keys::SCHEDTXT_BASE_API_URL; let key = crate::envy::keys::TEXTSENDER_BASE_API_URL;
let value = std::env::var(key).expect(key); let value = std::env::var(key).expect(key);
crate::envy::init_envvar(key, &value) crate::envy::init_envvar(key, &value)
@@ -48,7 +48,7 @@ pub fn get_app_base_api_url() -> crate::envy::EnvVar {
pub fn get_app_auth_base_api_url() -> crate::envy::EnvVar { pub fn get_app_auth_base_api_url() -> crate::envy::EnvVar {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let key = crate::envy::keys::SCHEDTXT_AUTH_BASE_API_URL; let key = crate::envy::keys::TEXTSENDER_AUTH_BASE_API_URL;
let value = std::env::var(key).expect(key); let value = std::env::var(key).expect(key);
crate::envy::init_envvar(key, &value) crate::envy::init_envvar(key, &value)
+8 -8
View File
@@ -2,7 +2,7 @@
pub const DB_URL: &str = "DATABASE_URL"; pub const DB_URL: &str = "DATABASE_URL";
/// Environment key for secret main key /// Environment key for secret main key
/// Used for the schedtxt app /// Used for the textsender app
pub const SECRET_MAIN_KEY: &str = "SECRET_MAIN_KEY"; pub const SECRET_MAIN_KEY: &str = "SECRET_MAIN_KEY";
/// Environment key for service logins /// Environment key for service logins
@@ -15,11 +15,11 @@ pub const SECRET_KEY: &str = "SECRET_KEY";
/// Environment key for root directory for the textsender app /// Environment key for root directory for the textsender app
pub const ROOT_DIRECTORY: &str = "ROOT_DIRECTORY"; pub const ROOT_DIRECTORY: &str = "ROOT_DIRECTORY";
/// Environment key for schedtxt api url /// Environment key for textsender api url
pub const SCHEDTXT_BASE_API_URL: &str = "SCHEDTXT_BASE_API_URL"; pub const TEXTSENDER_BASE_API_URL: &str = "TEXTSENDER_BASE_API_URL";
/// Environment key for textsender auth api url /// Environment key for textsender auth api url
pub const SCHEDTXT_AUTH_BASE_API_URL: &str = "SCHEDTXT_AUTH_BASE_API_URL"; pub const TEXTSENDER_AUTH_BASE_API_URL: &str = "TEXTSENDER_AUTH_BASE_API_URL";
/// Environment key for App status /// Environment key for App status
pub const APP_ENV: &str = "APP_ENV"; pub const APP_ENV: &str = "APP_ENV";
@@ -41,10 +41,10 @@ pub mod error {
pub const SERVICE_LOGIN: &str = concatcp!(super::SERVICE_PASSPHRASE, " ", GENERAL_ERROR); pub const SERVICE_LOGIN: &str = concatcp!(super::SERVICE_PASSPHRASE, " ", GENERAL_ERROR);
pub const SECRET_KEY: &str = concatcp!(super::SECRET_KEY, " ", GENERAL_ERROR); pub const SECRET_KEY: &str = concatcp!(super::SECRET_KEY, " ", GENERAL_ERROR);
pub const ROOT_DIRECTORY: &str = concatcp!(super::ROOT_DIRECTORY, " ", GENERAL_ERROR); pub const ROOT_DIRECTORY: &str = concatcp!(super::ROOT_DIRECTORY, " ", GENERAL_ERROR);
pub const SCHEDTXT_BASE_API_URL: &str = pub const TEXTSENDER_BASE_API_URL: &str =
concatcp!(super::SCHEDTXT_BASE_API_URL, " ", GENERAL_ERROR); concatcp!(super::TEXTSENDER_BASE_API_URL, " ", GENERAL_ERROR);
pub const SCHEDTXT_AUTH_BASE_API_URL: &str = pub const TEXTSENDER_AUTH_BASE_API_URL: &str =
concatcp!(super::SCHEDTXT_AUTH_BASE_API_URL, " ", GENERAL_ERROR); concatcp!(super::TEXTSENDER_AUTH_BASE_API_URL, " ", GENERAL_ERROR);
pub const APP_ENV: &str = concatcp!(super::APP_ENV, " ", GENERAL_ERROR); pub const APP_ENV: &str = concatcp!(super::APP_ENV, " ", GENERAL_ERROR);
pub const BACKEND_PORT: &str = concatcp!(super::BACKEND_PORT, " ", GENERAL_ERROR); pub const BACKEND_PORT: &str = concatcp!(super::BACKEND_PORT, " ", GENERAL_ERROR);
pub const FRONTEND_URL: &str = concatcp!(super::FRONTEND_URL, " ", GENERAL_ERROR); pub const FRONTEND_URL: &str = concatcp!(super::FRONTEND_URL, " ", GENERAL_ERROR);
+3 -41
View File
@@ -2,52 +2,14 @@
/// extract it into some strings /// extract it into some strings
pub fn delimitize(var: &crate::envy::EnvVar) -> Result<Vec<String>, std::io::Error> { pub fn delimitize(var: &crate::envy::EnvVar) -> Result<Vec<String>, std::io::Error> {
if var.has_delimiter { if var.has_delimiter {
let values: Vec<String> = match var Ok(var
.value .value
.split(var.delimiter) .split(var.delimiter)
.map(|c| c.parse::<String>()) .map(|c| c.parse::<String>().unwrap())
.collect() .collect())
{
Ok(v) => v,
Err(err) => {
return Err(std::io::Error::other(err));
}
};
Ok(values)
} else { } else {
Err(std::io::Error::other( Err(std::io::Error::other(
"Environment variable does not have a delimiter", "Environment variable does not have a delimiter",
)) ))
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delimitize() {
let envvar = crate::envy::EnvVar {
key: "OCEANS".to_string(),
value: "Atlantic|Pacific|Indian|Arctic".to_string(),
has_delimiter: true,
delimiter: '|',
};
let all_values = vec![
"Atlantic".to_string(),
"Pacific".to_string(),
"Indian".to_string(),
"Arctic".to_string(),
];
match delimitize(&envvar) {
Ok(values) => {
assert_eq!(values, all_values, "Failure in delimitizing EnvVar values");
}
Err(err) => {
assert!(false, "Error delimitizing: {err:?}");
}
}
}
}
+26 -80
View File
@@ -78,32 +78,37 @@ pub struct CreateTokenResult {
} }
pub fn create_token( pub fn create_token(
key: &str, key: &String,
token_resource: TokenResource, token_resource: &TokenResource,
duration: time::Duration, duration: time::Duration,
) -> Result<CreateTokenResult, josekit::JoseError> { ) -> Result<CreateTokenResult, josekit::JoseError> {
let mut header = josekit::jws::JwsHeader::new(); let mut header = josekit::jws::JwsHeader::new();
header.set_token_type(TOKEN_TYPE); header.set_token_type(TOKEN_TYPE);
match init_payload(token_resource, duration) { let mut payload = josekit::jwt::JwtPayload::new();
Ok((payload, issued, expire)) => { payload.set_subject(&token_resource.message);
let signer = match josekit::jws::alg::hmac::HmacJwsAlgorithm::Hs256 payload.set_issuer(&token_resource.issuer);
payload.set_audience(token_resource.audiences.clone());
if !token_resource.user_id.is_nil() {
match payload.set_claim("user_id", Some(serde_json::json!(token_resource.user_id))) {
Ok(_) => {}
Err(err) => {
return Err(err);
}
}
}
match get_issued() {
Ok(issued) => {
let expire = issued + duration;
payload.set_issued_at(&util::time_to_std_time(&issued).unwrap());
payload.set_expires_at(&util::time_to_std_time(&expire).unwrap());
let signer = josekit::jws::alg::hmac::HmacJwsAlgorithm::Hs256
.signer_from_bytes(key.as_bytes()) .signer_from_bytes(key.as_bytes())
{ .unwrap();
Ok(signer) => signer,
Err(err) => {
return Err(err);
}
};
let access_token = match josekit::jwt::encode_with_signer(&payload, &header, &signer) {
Ok(access_token) => access_token,
Err(err) => {
return Err(err);
}
};
Ok(CreateTokenResult { Ok(CreateTokenResult {
access_token, access_token: josekit::jwt::encode_with_signer(&payload, &header, &signer).unwrap(),
issued: issued.unix_timestamp(), issued: issued.unix_timestamp(),
expires_in: expire.unix_timestamp(), expires_in: expire.unix_timestamp(),
token_issued: issued, token_issued: issued,
@@ -113,69 +118,10 @@ pub fn create_token(
} }
} }
fn init_payload(
token_resource: TokenResource,
duration: time::Duration,
) -> Result<
(
josekit::jwt::JwtPayload,
time::OffsetDateTime,
time::OffsetDateTime,
),
josekit::JoseError,
> {
let mut payload = josekit::jwt::JwtPayload::new();
payload.set_subject(token_resource.message);
payload.set_issuer(token_resource.issuer);
payload.set_audience(token_resource.audiences);
if !token_resource.user_id.is_nil() {
match payload.set_claim("user_id", Some(serde_json::json!(token_resource.user_id))) {
Ok(_) => {}
Err(err) => {
return Err(err);
}
}
}
match init_payload_issued(&mut payload, duration) {
Ok((issued, expire)) => Ok((payload, issued, expire)),
Err(err) => Err(err),
}
}
fn init_payload_issued(
payload: &mut josekit::jwt::JwtPayload,
duration: time::Duration,
) -> Result<(time::OffsetDateTime, time::OffsetDateTime), josekit::JoseError> {
match get_issued() {
Ok(issued) => {
let expire = issued + duration;
let issued_at = match util::time_to_std_time(&issued) {
Ok(issued_at) => issued_at,
Err(err) => {
return Err(josekit::JoseError::InvalidClaim(err.into()));
}
};
let expires_at = match util::time_to_std_time(&expire) {
Ok(expires_at) => expires_at,
Err(err) => {
return Err(josekit::JoseError::InvalidClaim(err.into()));
}
};
payload.set_issued_at(&issued_at);
payload.set_expires_at(&expires_at);
Ok((issued, expire))
}
Err(err) => Err(josekit::JoseError::InvalidClaim(err.into())),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
const TEST_MESSAGE: &str = "Testing for schedtxt"; const TEST_MESSAGE: &str = "Testing for textsender";
const TEST_ISSUER: &str = "schedtxt-test"; const TEST_ISSUER: &str = "textsender-test";
const TEST_AUDIENCE: &str = "area-test"; const TEST_AUDIENCE: &str = "area-test";
const TEST_USER_ID: uuid::Uuid = uuid::uuid!("9ab1c75f-d184-4913-ae99-544b4dcfcb41"); const TEST_USER_ID: uuid::Uuid = uuid::uuid!("9ab1c75f-d184-4913-ae99-544b4dcfcb41");
const TEST_KEY: &str = "8342nhf7ycrt4983q7ryfc93w478ryfc3w9487ryfc342w98i7cy"; const TEST_KEY: &str = "8342nhf7ycrt4983q7ryfc93w478ryfc3w9487ryfc342w98i7cy";
@@ -192,7 +138,7 @@ mod tests {
let token_duration = time::Duration::minutes(30); let token_duration = time::Duration::minutes(30);
let key = String::from(TEST_KEY); let key = String::from(TEST_KEY);
match super::create_token(&key, token_resource, token_duration) { match super::create_token(&key, &token_resource, token_duration) {
Ok(cst) => match time::OffsetDateTime::from_unix_timestamp(cst.issued) { Ok(cst) => match time::OffsetDateTime::from_unix_timestamp(cst.issued) {
Ok(d_result) => { Ok(d_result) => {
let they_match = { let they_match = {
+4
View File
@@ -6,7 +6,9 @@ pub struct User {
pub phone_number: String, pub phone_number: String,
pub username: String, pub username: String,
pub password: String, pub password: String,
// Firstname is a pointer
pub firstname: String, pub firstname: String,
// Lastname is a pointer
pub lastname: String, pub lastname: String,
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
pub created: Option<time::OffsetDateTime>, pub created: Option<time::OffsetDateTime>,
@@ -25,6 +27,7 @@ pub struct ServiceUser {
pub passphrase: String, pub passphrase: String,
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
pub created: Option<time::OffsetDateTime>, pub created: Option<time::OffsetDateTime>,
// When serializing to json, if empty do not populate
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
pub last_login: Option<time::OffsetDateTime>, pub last_login: Option<time::OffsetDateTime>,
#[serde(skip)] #[serde(skip)]
@@ -78,6 +81,7 @@ pub struct UserProfile {
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow)] #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
pub struct Salt { pub struct Salt {
// #[serde(skip_serializing_if = "crate::init::is_uuid_nil")]
pub id: uuid::Uuid, pub id: uuid::Uuid,
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty")]
pub salt: String, pub salt: String,