From 4f4eb500654b589a6cb4cbc8a4ba90b99f964e16 Mon Sep 17 00:00:00 2001
From: div-seungha
Date: Tue, 17 Dec 2024 14:42:18 +0900
Subject: [PATCH] Add `theme` field in `Account` to represent user's screen
color theme
---
CHANGELOG.md | 5 +
Cargo.toml | 2 +-
src/account.rs | 7 +-
src/migration.rs | 280 ++++++++++++++++++++++++++++++++++++++---
src/tables/accounts.rs | 11 ++
5 files changed, 283 insertions(+), 22 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8383618..1198c4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ Versioning](https://semver.org/spec/v2.0.0.html).
## [0.33.0] - 2024-12-12
+### Added
+
+- Added `Account::theme` field to represent user's selected screen color theme
+ on the user interface.
+
### Changed
- Modified `Table::get_patterns` to take a reference to a slice of tuples
diff --git a/Cargo.toml b/Cargo.toml
index 17afa9f..708fe90 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "review-database"
-version = "0.33.0"
+version = "0.33.1-alpha"
edition = "2021"
[dependencies]
diff --git a/src/account.rs b/src/account.rs
index f356fac..f43e3dc 100644
--- a/src/account.rs
+++ b/src/account.rs
@@ -36,6 +36,7 @@ pub struct Account {
pub name: String,
pub department: String,
pub language: Option
,
+ pub theme: Option,
pub(crate) creation_time: DateTime,
pub(crate) last_signin_time: Option>,
pub allow_access_from: Option>,
@@ -60,6 +61,7 @@ impl Account {
name: String,
department: String,
language: Option,
+ theme: Option,
allow_access_from: Option>,
max_parallel_sessions: Option,
) -> Result {
@@ -72,6 +74,7 @@ impl Account {
role,
name,
department,
+ theme,
language,
creation_time: now,
last_signin_time: None,
@@ -170,7 +173,7 @@ impl SaltedPassword {
/// # Errors
///
/// Returns an error if the salt cannot be generated.
- fn new_with_hash_algorithm(
+ pub(crate) fn new_with_hash_algorithm(
password: &str,
hash_algorithm: &PasswordHashAlgorithm,
) -> Result {
@@ -292,6 +295,7 @@ mod tests {
None,
None,
None,
+ None,
);
assert!(account.is_ok());
@@ -319,6 +323,7 @@ mod tests {
department: String::new(),
name: String::new(),
language: None,
+ theme: None,
creation_time: Utc::now(),
last_signin_time: None,
allow_access_from: None,
diff --git a/src/migration.rs b/src/migration.rs
index d0ad939..e38e76c 100644
--- a/src/migration.rs
+++ b/src/migration.rs
@@ -136,6 +136,11 @@ pub fn migrate_data_dir>(data_dir: P, backup_dir: P) -> Result<()
Version::parse("0.30.0")?,
migrate_0_29_to_0_30_0,
),
+ (
+ VersionReq::parse(">=0.33.0,<0.34.0")?,
+ Version::parse("0.34.0")?,
+ migrate_0_33_to_0_34_0,
+ ),
];
let mut store = super::Store::new(data_dir, backup_dir)?;
@@ -211,6 +216,67 @@ fn read_version_file(path: &Path) -> Result {
Version::parse(&ver).context("cannot parse VERSION")
}
+fn migrate_0_33_to_0_34_0(store: &super::Store) -> Result<()> {
+ migrate_0_34_account(store)
+}
+
+fn migrate_0_34_account(store: &super::Store) -> Result<()> {
+ use bincode::Options;
+ use chrono::{DateTime, Utc};
+
+ use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword};
+ use crate::types::Account;
+
+ #[derive(Deserialize, Serialize)]
+ pub struct OldAccount {
+ pub username: String,
+ password: SaltedPassword,
+ pub role: Role,
+ pub name: String,
+ pub department: String,
+ pub language: Option,
+ creation_time: DateTime,
+ last_signin_time: Option>,
+ pub allow_access_from: Option>,
+ pub max_parallel_sessions: Option,
+ password_hash_algorithm: PasswordHashAlgorithm,
+ password_last_modified_at: DateTime,
+ }
+
+ impl From for Account {
+ fn from(input: OldAccount) -> Self {
+ Self {
+ username: input.username,
+ password: input.password,
+ role: input.role,
+ name: input.name,
+ department: input.department,
+ language: input.language,
+ theme: None,
+ creation_time: input.creation_time,
+ last_signin_time: input.last_signin_time,
+ allow_access_from: input.allow_access_from,
+ max_parallel_sessions: input.max_parallel_sessions,
+ password_hash_algorithm: input.password_hash_algorithm,
+ password_last_modified_at: input.password_last_modified_at,
+ }
+ }
+ }
+
+ let map = store.account_map();
+ let raw = map.raw();
+ let mut accounts = vec![];
+ for (key, old_value) in raw.iter_forward()? {
+ let old = bincode::DefaultOptions::new().deserialize::(&old_value)?;
+ raw.delete(&key)?;
+ accounts.push(old.into());
+ }
+ for account in accounts {
+ map.insert(&account)?;
+ }
+ Ok(())
+}
+
fn migrate_0_29_to_0_30_0(store: &super::Store) -> Result<()> {
migrate_0_30_tidb(store)?;
migrate_0_30_event_struct(store)
@@ -1016,7 +1082,6 @@ fn migrate_0_29_account(store: &super::Store) -> Result<()> {
use chrono::{DateTime, Utc};
use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword};
- use crate::types::Account;
#[derive(Deserialize, Serialize)]
pub struct OldAccount {
@@ -1032,7 +1097,23 @@ fn migrate_0_29_account(store: &super::Store) -> Result<()> {
password_hash_algorithm: PasswordHashAlgorithm,
}
- impl From for Account {
+ #[derive(Deserialize, Serialize)]
+ pub struct AccountV29 {
+ pub username: String,
+ password: SaltedPassword,
+ pub role: Role,
+ pub name: String,
+ pub department: String,
+ language: Option,
+ creation_time: DateTime,
+ last_signin_time: Option>,
+ pub allow_access_from: Option>,
+ pub max_parallel_sessions: Option,
+ password_hash_algorithm: PasswordHashAlgorithm,
+ password_last_modified_at: DateTime,
+ }
+
+ impl From for AccountV29 {
fn from(input: OldAccount) -> Self {
Self {
username: input.username,
@@ -1053,14 +1134,11 @@ fn migrate_0_29_account(store: &super::Store) -> Result<()> {
let map = store.account_map();
let raw = map.raw();
- let mut accounts = vec![];
for (key, old_value) in raw.iter_forward()? {
let old = bincode::DefaultOptions::new().deserialize::(&old_value)?;
- raw.delete(&key)?;
- accounts.push(old.into());
- }
- for account in accounts {
- map.insert(&account)?;
+ let new: AccountV29 = old.into();
+ let new_value = bincode::DefaultOptions::new().serialize::(&new)?;
+ raw.update((&key, &old_value), (&key, &new_value))?;
}
Ok(())
}
@@ -3441,12 +3519,12 @@ mod tests {
fn migrate_0_26_to_0_29_account() {
use std::net::IpAddr;
+ use anyhow::Result;
use bincode::Options;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword};
- use crate::types::Account;
#[derive(Deserialize, Serialize)]
pub struct OldAccount {
@@ -3462,7 +3540,58 @@ mod tests {
password_hash_algorithm: PasswordHashAlgorithm,
}
- impl From for Account {
+ #[derive(Clone, Deserialize, Serialize, PartialEq, Debug)]
+ pub struct AccountV29 {
+ pub username: String,
+ password: SaltedPassword,
+ pub role: Role,
+ pub name: String,
+ pub department: String,
+ language: Option,
+ creation_time: DateTime,
+ last_signin_time: Option>,
+ pub allow_access_from: Option>,
+ pub max_parallel_sessions: Option,
+ password_hash_algorithm: PasswordHashAlgorithm,
+ password_last_modified_at: DateTime,
+ }
+
+ impl AccountV29 {
+ const DEFAULT_HASH_ALGORITHM: PasswordHashAlgorithm = PasswordHashAlgorithm::Argon2id;
+
+ fn new(
+ username: &str,
+ password: &str,
+ role: Role,
+ name: String,
+ department: String,
+ language: Option,
+ allow_access_from: Option>,
+ max_parallel_sessions: Option,
+ ) -> Result {
+ let password = SaltedPassword::new_with_hash_algorithm(
+ password,
+ &Self::DEFAULT_HASH_ALGORITHM,
+ )?;
+ let now = Utc::now();
+ Ok(Self {
+ username: username.to_string(),
+ password,
+ role,
+ name,
+ department,
+ language,
+ creation_time: now,
+ last_signin_time: None,
+ allow_access_from,
+ max_parallel_sessions,
+ password_hash_algorithm: Self::DEFAULT_HASH_ALGORITHM,
+ password_last_modified_at: now,
+ })
+ }
+ }
+
+ impl From for AccountV29 {
fn from(input: OldAccount) -> Self {
Self {
username: input.username,
@@ -3481,8 +3610,8 @@ mod tests {
}
}
- impl From for OldAccount {
- fn from(input: Account) -> Self {
+ impl From for OldAccount {
+ fn from(input: AccountV29) -> Self {
Self {
username: input.username,
password: input.password,
@@ -3502,7 +3631,7 @@ mod tests {
let map = settings.store.account_map();
let raw = map.raw();
- let mut test = Account::new(
+ let mut test = AccountV29::new(
"test",
"password",
Role::SecurityAdministrator,
@@ -3526,13 +3655,21 @@ mod tests {
assert!(super::migrate_0_29_account(&settings.store).is_ok());
let map = settings.store.account_map();
- let res = map.get(&test.username);
- assert!(res.is_ok());
- let account = res.unwrap();
- if let Some(a) = &account {
- test.password_last_modified_at = a.password_last_modified_at;
- }
- assert_eq!(account, Some(test));
+ let raw = map.raw();
+ let raw_value = raw
+ .get(test.username.as_bytes())
+ .expect("Failed to get raw value from database");
+ assert!(raw_value.is_some());
+
+ let raw_owned = raw_value.unwrap();
+ let raw_bytes = raw_owned.as_ref();
+
+ let account: AccountV29 = bincode::DefaultOptions::new()
+ .deserialize(raw_bytes)
+ .expect("Failed to deserialize into AccountV29");
+
+ test.password_last_modified_at = account.password_last_modified_at;
+ assert_eq!(account, test);
}
#[test]
@@ -3614,4 +3751,107 @@ mod tests {
assert_eq!(rule.category, EventCategory::Reconnaissance);
});
}
+
+ #[test]
+ fn migrate_0_34_account() {
+ use std::net::IpAddr;
+
+ use bincode::Options;
+ use chrono::{DateTime, Utc};
+ use serde::{Deserialize, Serialize};
+
+ use crate::account::{PasswordHashAlgorithm, Role, SaltedPassword};
+ use crate::types::Account;
+
+ #[derive(Deserialize, Serialize)]
+ pub struct OldAccount {
+ pub username: String,
+ password: SaltedPassword,
+ pub role: Role,
+ pub name: String,
+ pub department: String,
+ language: Option,
+ creation_time: DateTime,
+ last_signin_time: Option>,
+ pub allow_access_from: Option>,
+ pub max_parallel_sessions: Option,
+ password_hash_algorithm: PasswordHashAlgorithm,
+ password_last_modified_at: DateTime,
+ }
+
+ impl From for Account {
+ fn from(input: OldAccount) -> Self {
+ Self {
+ username: input.username,
+ password: input.password,
+ role: input.role,
+ name: input.name,
+ department: input.department,
+ language: input.language,
+ theme: None,
+ creation_time: input.creation_time,
+ last_signin_time: input.last_signin_time,
+ allow_access_from: input.allow_access_from,
+ max_parallel_sessions: input.max_parallel_sessions,
+ password_hash_algorithm: input.password_hash_algorithm,
+ password_last_modified_at: input.password_last_modified_at,
+ }
+ }
+ }
+
+ impl From for OldAccount {
+ fn from(input: Account) -> Self {
+ Self {
+ username: input.username,
+ password: input.password,
+ role: input.role,
+ name: input.name,
+ department: input.department,
+ language: input.language,
+ creation_time: input.creation_time,
+ last_signin_time: input.last_signin_time,
+ allow_access_from: input.allow_access_from,
+ max_parallel_sessions: input.max_parallel_sessions,
+ password_hash_algorithm: input.password_hash_algorithm,
+ password_last_modified_at: input.password_last_modified_at,
+ }
+ }
+ }
+
+ let settings = TestSchema::new();
+ let map = settings.store.account_map();
+ let raw = map.raw();
+
+ let test = Account::new(
+ "test",
+ "password",
+ Role::SecurityAdministrator,
+ "name".to_string(),
+ "department".to_string(),
+ None,
+ None,
+ None,
+ None,
+ )
+ .unwrap();
+
+ let old: OldAccount = test.clone().into();
+ let value = bincode::DefaultOptions::new()
+ .serialize(&old)
+ .expect("serializable");
+
+ assert!(raw.put(old.username.as_bytes(), &value).is_ok());
+
+ let (db_dir, backup_dir) = settings.close();
+ let settings = TestSchema::new_with_dir(db_dir, backup_dir);
+
+ assert!(super::migrate_0_34_account(&settings.store).is_ok());
+
+ let map = settings.store.account_map();
+ let res = map.get(&test.username);
+ assert!(res.is_ok());
+ let account = res.unwrap();
+
+ assert_eq!(account, Some(test));
+ }
}
diff --git a/src/tables/accounts.rs b/src/tables/accounts.rs
index da4f893..eec4dc6 100644
--- a/src/tables/accounts.rs
+++ b/src/tables/accounts.rs
@@ -77,6 +77,7 @@ impl<'d> Table<'d, Account> {
name: &Option<(String, String)>,
department: &Option<(String, String)>,
language: &Option<(Option, Option)>,
+ theme: &Option<(Option, Option)>,
allow_access_from: &Option<(Option>, Option>)>,
max_parallel_sessions: &Option<(Option, Option)>,
) -> Result<(), anyhow::Error> {
@@ -117,6 +118,12 @@ impl<'d> Table<'d, Account> {
}
account.language.clone_from(new);
}
+ if let Some((old, new)) = theme {
+ if account.theme != *old {
+ bail!("old value mismatch");
+ }
+ account.theme.clone_from(new);
+ }
if let Some((old, new)) = &allow_access_from {
if account.allow_access_from != *old {
bail!("old value mismatch");
@@ -177,6 +184,7 @@ mod tests {
None,
None,
None,
+ None,
)
.unwrap();
table.put(&acc1).unwrap();
@@ -191,6 +199,7 @@ mod tests {
None,
None,
None,
+ None,
)
.unwrap();
table.put(&acc2).unwrap();
@@ -221,6 +230,7 @@ mod tests {
None,
None,
None,
+ None,
)
.unwrap();
table.put(&acc1).unwrap();
@@ -239,6 +249,7 @@ mod tests {
None,
None,
None,
+ None,
)
.unwrap();
table.put(&acc2).unwrap();