use serde::{Deserialize, Serialize};
use zino_auth::{AccessKeyId, UserSession};
use zino_core::{
bail,
datetime::DateTime,
error::Error,
extension::JsonObjectExt,
model::{Model, ModelHooks},
validation::Validation,
Map, Uuid,
};
use zino_derive::{DecodeRow, Entity, ModelAccessor, Schema};
use zino_orm::ModelHelper;
#[cfg(feature = "tags")]
use crate::tag::Tag;
mod jwt_auth;
mod status;
pub use jwt_auth::JwtAuthService;
pub use status::UserStatus;
#[cfg(feature = "visibility")]
mod visibility;
#[cfg(feature = "visibility")]
pub use visibility::UserVisibility;
#[derive(
Debug, Clone, Default, Serialize, Deserialize, DecodeRow, Entity, Schema, ModelAccessor,
)]
#[serde(default)]
#[schema(auto_rename)]
pub struct User {
#[schema(read_only)]
id: Uuid,
#[schema(not_null)]
name: String,
#[cfg(feature = "namespace")]
#[schema(default_value = "User::model_namespace", index_type = "hash")]
namespace: String,
#[cfg(feature = "visibility")]
#[schema(type_name = "String", default_value = "UserVisibility::default")]
visibility: UserVisibility,
#[schema(
type_name = "String",
default_value = "UserStatus::default",
index_type = "hash"
)]
status: UserStatus,
description: String,
#[schema(unique)]
union_id: String,
#[schema(not_null, unique, write_only)]
access_key_id: String,
#[schema(not_null, unique, write_only)]
account: String,
#[schema(not_null, write_only)]
password: String,
nickname: String,
#[schema(format = "uri")]
avatar: String,
#[schema(format = "uri")]
website: String,
#[schema(format = "email")]
email: String,
location: String,
locale: String,
mobile: String,
#[schema(snapshot, nonempty, unique_items, index_type = "gin")]
roles: Vec<String>,
#[cfg(feature = "tags")]
#[schema(unique_items, reference = "Tag", index_type = "gin")]
tags: Vec<Uuid>, last_login_at: DateTime,
#[schema(format = "ip")]
last_login_ip: String,
current_login_at: DateTime,
#[schema(format = "ip")]
current_login_ip: String,
login_count: u32,
failed_login_count: u8,
extra: Map,
#[cfg(feature = "owner-id")]
#[schema(reference = "User")]
owner_id: Option<Uuid>, #[cfg(feature = "maintainer-id")]
#[schema(reference = "User")]
maintainer_id: Option<Uuid>, #[schema(read_only, default_value = "now", index_type = "btree")]
created_at: DateTime,
#[schema(default_value = "now", index_type = "btree")]
updated_at: DateTime,
version: u64,
#[cfg(feature = "edition")]
edition: u32,
}
impl Model for User {
const MODEL_NAME: &'static str = "user";
#[inline]
fn new() -> Self {
Self {
id: Uuid::now_v7(),
access_key_id: AccessKeyId::new().to_string(),
..Self::default()
}
}
fn read_map(&mut self, data: &Map) -> Validation {
let mut validation = Validation::new();
if let Some(result) = data.parse_uuid("id") {
match result {
Ok(id) => self.id = id,
Err(err) => validation.record_fail("id", err),
}
}
if let Some(name) = data.parse_string("name") {
self.name = name.into_owned();
}
if let Some(union_id) = data.parse_string("union_id") {
self.union_id = union_id.into_owned();
}
if let Some(account) = data.parse_string("account") {
self.account = account.into_owned();
}
if let Some(password) = data.parse_string("password") {
match User::encrypt_password(&password) {
Ok(password) => self.password = password,
Err(err) => validation.record_fail("password", err),
}
}
if let Some(roles) = data.parse_str_array("roles") {
if let Err(err) = self.set_roles(roles) {
validation.record_fail("roles", err);
}
}
if self.roles.is_empty() && !validation.contains_key("roles") {
validation.record("roles", "should be nonempty");
}
#[cfg(feature = "tags")]
if let Some(result) = data.parse_array("tags") {
match result {
Ok(tags) => self.tags = tags,
Err(err) => validation.record_fail("tags", err),
}
}
#[cfg(feature = "owner-id")]
if let Some(result) = data.parse_uuid("owner_id") {
match result {
Ok(owner_id) => self.owner_id = Some(owner_id),
Err(err) => validation.record_fail("owner_id", err),
}
}
#[cfg(feature = "maintainer-id")]
if let Some(result) = data.parse_uuid("maintainer_id") {
match result {
Ok(maintainer_id) => self.maintainer_id = Some(maintainer_id),
Err(err) => validation.record_fail("maintainer_id", err),
}
}
validation
}
}
impl ModelHooks for User {
type Data = ();
#[cfg(feature = "maintainer-id")]
type Extension = UserSession<Uuid, String>;
#[cfg(not(feature = "maintainer-id"))]
type Extension = ();
#[cfg(feature = "maintainer-id")]
#[inline]
async fn after_extract(&mut self, session: Self::Extension) -> Result<(), Error> {
self.maintainer_id = Some(*session.user_id());
Ok(())
}
#[cfg(feature = "maintainer-id")]
#[inline]
async fn before_validation(
data: &mut Map,
extension: Option<&Self::Extension>,
) -> Result<(), Error> {
if let Some(session) = extension {
data.upsert("maintainer_id", session.user_id().to_string());
}
Ok(())
}
}
impl User {
#[inline]
pub fn set_access_key_id(&mut self, access_key_id: AccessKeyId) {
self.access_key_id = access_key_id.to_string();
}
pub fn set_roles(&mut self, roles: Vec<&str>) -> Result<(), Error> {
let num_roles = roles.len();
let special_roles = ["superuser", "user", "guest"];
for role in &roles {
if special_roles.contains(role) && num_roles != 1 {
bail!("the special role `{}` is exclusive", role);
} else if role.is_empty() {
bail!("the `roles` can not contain empty values");
}
}
self.roles = roles.into_iter().map(|s| s.to_owned()).collect();
Ok(())
}
#[inline]
pub fn union_id(&self) -> &str {
&self.union_id
}
#[inline]
pub fn access_key_id(&self) -> &str {
self.access_key_id.as_str()
}
#[inline]
pub fn roles(&self) -> &[String] {
self.roles.as_slice()
}
pub fn user_session(&self) -> UserSession<Uuid, String> {
let mut user_session = UserSession::new(self.id, None);
user_session.set_access_key_id(self.access_key_id().into());
user_session.set_roles(self.roles());
user_session
}
}
#[cfg(test)]
mod tests {
use super::User;
use zino_core::{extension::JsonObjectExt, model::Model, Map};
#[test]
fn it_checks_user_roles() {
let mut alice = User::new();
let mut data = Map::new();
data.upsert("name", "alice");
data.upsert("roles", vec!["admin:user", "auditor"]);
let validation = alice.read_map(&data);
assert!(validation.is_success());
let user_session = alice.user_session();
assert!(user_session.is_admin());
assert!(!user_session.is_worker());
assert!(user_session.is_auditor());
assert!(user_session.has_role("admin:user"));
assert!(!user_session.has_role("admin:group"));
assert!(user_session.has_role("auditor:log"));
assert!(!user_session.has_role("auditor_record"));
}
}