use std::{collections::HashMap, sync::OnceLock};
use anyhow::anyhow;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumDiscriminants, EnumString};
use typeshare::typeshare;
use crate::entities::{I64, MongoId};
use super::{
JsonValue, ResourceTargetVariant,
permission::PermissionLevelAndSpecifics,
};
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(
feature = "mongo",
derive(mongo_indexed::derive::MongoIndexed)
)]
#[cfg_attr(feature = "mongo", doc_index({ "config.type": 1 }))]
#[cfg_attr(feature = "mongo", sparse_doc_index({ "config.data.google_id": 1 }))]
#[cfg_attr(feature = "mongo", sparse_doc_index({ "config.data.github_id": 1 }))]
pub struct User {
#[serde(
default,
rename = "_id",
skip_serializing_if = "String::is_empty",
with = "bson::serde_helpers::hex_string_as_object_id"
)]
pub id: MongoId,
#[cfg_attr(feature = "mongo", unique_index)]
pub username: String,
#[cfg_attr(feature = "mongo", index)]
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub super_admin: bool,
#[serde(default)]
pub admin: bool,
#[serde(default)]
pub create_server_permissions: bool,
#[serde(default)]
pub create_build_permissions: bool,
pub config: UserConfig,
#[serde(default)]
pub linked_logins: LinkedLoginsMap,
#[serde(default)]
pub totp: UserTotpConfig,
#[serde(default)]
pub passkey: UserPasskeyConfig,
#[serde(default = "default_external_skip_2fa")]
pub external_skip_2fa: bool,
#[serde(default)]
pub last_update_view: I64,
#[serde(default)]
pub recents: HashMap<ResourceTargetVariant, Vec<String>>,
#[serde(default)]
#[cfg_attr(feature = "utoipa", schema(value_type = HashMap<ResourceTargetVariant, PermissionLevelAndSpecifics>))]
pub all:
IndexMap<ResourceTargetVariant, PermissionLevelAndSpecifics>,
#[serde(default)]
pub updated_at: I64,
}
fn default_external_skip_2fa() -> bool {
true
}
pub struct NewUserParams {
pub username: String,
pub enabled: bool,
pub admin: bool,
pub super_admin: bool,
pub config: UserConfig,
pub updated_at: i64,
}
impl User {
pub fn new(
NewUserParams {
username,
enabled,
admin,
super_admin,
config,
updated_at,
}: NewUserParams,
) -> User {
User {
id: Default::default(),
username,
enabled,
admin,
super_admin,
create_server_permissions: admin,
create_build_permissions: admin,
updated_at,
last_update_view: 0,
config,
recents: Default::default(),
all: Default::default(),
linked_logins: Default::default(),
totp: Default::default(),
passkey: Default::default(),
external_skip_2fa: Default::default(),
}
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, EnumDiscriminants)]
#[strum_discriminants(name(UserConfigVariant))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(
not(feature = "utoipa"),
strum_discriminants(derive(
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
AsRefStr
))
)]
#[cfg_attr(
feature = "utoipa",
strum_discriminants(derive(
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
AsRefStr,
utoipa::ToSchema
))
)]
#[serde(tag = "type", content = "data")]
pub enum UserConfig {
Local { password: String },
Google { google_id: String, avatar: String },
Github { github_id: String, avatar: String },
Oidc { provider: String, user_id: String },
Service { description: String },
}
impl Default for UserConfig {
fn default() -> Self {
Self::Local {
password: String::new(),
}
}
}
impl UserConfig {
pub fn sanitize(&mut self) {
if let UserConfig::Local { password } = self
&& !password.is_empty()
{
*password = "#".repeat(8);
}
}
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct LinkedLoginsMap(HashMap<UserConfigVariant, UserConfig>);
impl LinkedLoginsMap {
pub fn get(
&self,
variant: UserConfigVariant,
) -> Option<&UserConfig> {
self.0.get(&variant)
}
pub fn update(&mut self, login: UserConfig) -> anyhow::Result<()> {
if let UserConfig::Service { .. } = &login {
return Err(anyhow!(
"Cannot insert Service type configuration as additional login method."
));
}
self.0.insert((&login).into(), login);
Ok(())
}
pub fn remove(&mut self, variant: UserConfigVariant) {
self.0.remove(&variant);
}
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct UserTotpConfig {
pub secret: String,
pub confirmed_at: I64,
pub recovery_codes: Vec<String>,
}
impl UserTotpConfig {
pub fn sanitize(&mut self) {
self.secret.clear();
self.recovery_codes.clear();
}
pub fn enrolled(&self) -> bool {
!self.secret.is_empty()
}
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct UserPasskeyConfig {
pub passkey: Option<JsonValue>,
pub created_at: I64,
}
impl UserPasskeyConfig {
pub fn sanitize(&mut self) {
self.passkey = None;
}
}
impl User {
pub fn sanitize(&mut self) {
self.config.sanitize();
self
.linked_logins
.0
.values_mut()
.for_each(UserConfig::sanitize);
self.totp.sanitize();
self.passkey.sanitize();
}
pub fn is_service_user(user_id: &str) -> bool {
matches!(
user_id,
"System"
| "000000000000000000000000"
| "Procedure"
| "000000000000000000000001"
| "Action"
| "000000000000000000000002"
| "Git Webhook"
| "000000000000000000000003"
| "Auto Redeploy"
| "000000000000000000000004"
| "Resource Sync"
| "000000000000000000000005"
| "Stack Wizard"
| "000000000000000000000006"
| "Build Manager"
| "000000000000000000000007"
| "Repo Manager"
| "000000000000000000000008"
)
}
}
pub fn admin_service_user(user_id: &str) -> Option<User> {
match user_id {
"000000000000000000000000" | "System" => {
system_user().to_owned().into()
}
"000000000000000000000001" | "Procedure" => {
procedure_user().to_owned().into()
}
"000000000000000000000002" | "Action" => {
action_user().to_owned().into()
}
"000000000000000000000003" | "Git Webhook" => {
git_webhook_user().to_owned().into()
}
"000000000000000000000004" | "Auto Redeploy" => {
auto_redeploy_user().to_owned().into()
}
"000000000000000000000005" | "Resource Sync" => {
sync_user().to_owned().into()
}
"000000000000000000000006" | "Stack Wizard" => {
stack_user().to_owned().into()
}
"000000000000000000000007" | "Build Manager" => {
build_user().to_owned().into()
}
"000000000000000000000008" | "Repo Manager" => {
repo_user().to_owned().into()
}
_ => None,
}
}
pub fn system_user() -> &'static User {
static SYSTEM_USER: OnceLock<User> = OnceLock::new();
SYSTEM_USER.get_or_init(|| {
let id_name = String::from("System");
User {
id: "000000000000000000000000".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn procedure_user() -> &'static User {
static PROCEDURE_USER: OnceLock<User> = OnceLock::new();
PROCEDURE_USER.get_or_init(|| {
let id_name = String::from("Procedure");
User {
id: "000000000000000000000001".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn action_user() -> &'static User {
static ACTION_USER: OnceLock<User> = OnceLock::new();
ACTION_USER.get_or_init(|| {
let id_name = String::from("Action");
User {
id: "000000000000000000000002".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn git_webhook_user() -> &'static User {
static GIT_WEBHOOK_USER: OnceLock<User> = OnceLock::new();
GIT_WEBHOOK_USER.get_or_init(|| {
let id_name = String::from("Git Webhook");
User {
id: "000000000000000000000003".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn auto_redeploy_user() -> &'static User {
static AUTO_REDEPLOY_USER: OnceLock<User> = OnceLock::new();
AUTO_REDEPLOY_USER.get_or_init(|| {
let id_name = String::from("Auto Redeploy");
User {
id: "000000000000000000000004".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn sync_user() -> &'static User {
static SYNC_USER: OnceLock<User> = OnceLock::new();
SYNC_USER.get_or_init(|| {
let id_name = String::from("Resource Sync");
User {
id: "000000000000000000000005".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn stack_user() -> &'static User {
static STACK_USER: OnceLock<User> = OnceLock::new();
STACK_USER.get_or_init(|| {
let id_name = String::from("Stack Wizard");
User {
id: "000000000000000000000006".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn build_user() -> &'static User {
static BUILD_USER: OnceLock<User> = OnceLock::new();
BUILD_USER.get_or_init(|| {
let id_name = String::from("Build Manager");
User {
id: "000000000000000000000007".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn repo_user() -> &'static User {
static REPO_USER: OnceLock<User> = OnceLock::new();
REPO_USER.get_or_init(|| {
let id_name = String::from("Repo Manager");
User {
id: "000000000000000000000008".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}