Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions migrations/1760537448__add_locale_to_admin_users.surql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add locale field to admin_users table
-- This allows each admin user to set their preferred language/locale
DEFINE FIELD locale ON TABLE admin_users TYPE string DEFAULT "en";
3 changes: 3 additions & 0 deletions proto/admin_user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ message AdminUserModel {
string created_by = 8;
string updated_by = 9;
repeated RoleModel roles = 10;
string locale = 11;
}


Expand Down Expand Up @@ -65,6 +66,7 @@ message StoreAdminUserRequest {
bool is_super_admin = 5;
bytes profile_image_content = 6;
string profile_image_file_name = 7;
string locale = 8;
}

message StoreAdminUserResponse {
Expand All @@ -91,6 +93,7 @@ message UpdateAdminUserRequest {
string profile_image_file_name = 4;
repeated string role_ids = 5;
bool is_super_admin = 6;
string locale = 7;
}

message UpdateAdminUserResponse {
Expand Down
1 change: 1 addition & 0 deletions src/api/admin_user_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl AdminUser for AdminUserApi {
let request_data = request.into_inner();
request_data.validate(&self.state).await?;


match self
.state
.admin_user_service
Expand Down
1 change: 1 addition & 0 deletions src/api/misc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ impl Misc for MiscApi {
is_super_admin: false,
profile_image_content: vec![],
profile_image_file_name: String::new(),
locale: String::from("en"),
};

let created_admin_user = self
Expand Down
6 changes: 6 additions & 0 deletions src/api/proto/admin_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct AdminUserModel {
pub updated_by: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "10")]
pub roles: ::prost::alloc::vec::Vec<RoleModel>,
#[prost(string, tag = "11")]
pub locale: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct RoleModel {
Expand Down Expand Up @@ -96,6 +98,8 @@ pub struct StoreAdminUserRequest {
pub profile_image_content: ::prost::alloc::vec::Vec<u8>,
#[prost(string, tag = "7")]
pub profile_image_file_name: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub locale: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct StoreAdminUserResponse {
Expand Down Expand Up @@ -130,6 +134,8 @@ pub struct UpdateAdminUserRequest {
pub role_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(bool, tag = "6")]
pub is_super_admin: bool,
#[prost(string, tag = "7")]
pub locale: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UpdateAdminUserResponse {
Expand Down
14 changes: 13 additions & 1 deletion src/models/admin_user_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub struct AdminUserModel {

/// The roles assigned to the admin user.
pub roles: Vec<RoleModel>,

/// The preferred locale/language for the admin user.
pub locale: String,
}

// region: impl try_from AdminUserModel
Expand Down Expand Up @@ -99,6 +102,7 @@ impl TryFrom<AdminUserModel> for GrpcAdminUserModel {
created_by: val.created_by,
updated_by: val.updated_by,
roles: grpc_roles,
locale: val.locale,
};

Ok(model)
Expand Down Expand Up @@ -148,6 +152,7 @@ impl TryFrom<Object> for AdminUserModel {
let updated_at = val.get("updated_at").get_datetime()?;
let created_by = val.get("created_by").get_string()?;
let updated_by = val.get("updated_by").get_string()?;
let locale = val.get("locale").get_string().unwrap_or_else(|_| String::from("en"));

Ok(Self {
id,
Expand All @@ -161,6 +166,7 @@ impl TryFrom<Object> for AdminUserModel {
created_by,
updated_by,
roles,
locale,
})
}
}
Expand Down Expand Up @@ -188,6 +194,9 @@ pub struct CreatableAdminUserModel {

/// The username of the user who is creating this admin user.
pub logged_in_username: String,

/// The preferred locale/language for the admin user.
pub locale: String,
// pub role_ids: Vec<String>,
}

Expand All @@ -202,7 +211,7 @@ pub struct UpdatableAdminUserModel {

/// The email address of the admin user.
pub profile_image: String,

/// is the admin user has super admin privileges.
pub is_super_admin: bool,

Expand All @@ -211,6 +220,9 @@ pub struct UpdatableAdminUserModel {

/// The roles assigned to the admin user.
pub role_ids: Vec<String>,

/// The preferred locale/language for the admin user.
pub locale: String,
}

// /// Represents a paginated response for admin users.
Expand Down
6 changes: 6 additions & 0 deletions src/repositories/admin_user_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ impl AdminUserRepository {
"is_super_admin".into(),
creatable_admin_user_model.is_super_admin.into(),
),
(
"locale".into(),
creatable_admin_user_model.locale.into(),
),
(
"created_by".into(),
creatable_admin_user_model.logged_in_username.clone().into(),
Expand Down Expand Up @@ -160,6 +164,7 @@ impl AdminUserRepository {
full_name: $full_name,
profile_image: $profile_image,
is_super_admin: $is_super_admin,
locale: $locale,
updated_by: $logged_in_user_name,
updated_at: time::now()
};";
Expand All @@ -178,6 +183,7 @@ impl AdminUserRepository {
"is_super_admin".into(),
updatable_admin_user.is_super_admin.into(),
),
("locale".into(), updatable_admin_user.locale.into()),
("id".into(), updatable_admin_user.id.into()),
("table".into(), ADMIN_USER_TABLE.into()),
]);
Expand Down
15 changes: 15 additions & 0 deletions src/requests/admin_user_request/store_admin_user_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::api::proto::admin_user::StoreAdminUserRequest;
use crate::avored_state::AvoRedState;
use crate::models::validation_error::{ErrorMessage, ErrorResponse, Validate};
use rust_i18n::t;
use std::path::Path;

impl StoreAdminUserRequest {
/// validate
Expand Down Expand Up @@ -63,6 +64,20 @@ impl StoreAdminUserRequest {
errors.push(error_message);
}

// Validate locale if provided (strict validation)
if !self.locale.is_empty() {
let locale_path = Path::new("resources/locales").join(format!("{}.json", self.locale));
if !locale_path.exists() {
let error_message = ErrorMessage {
key: String::from("locale"),
message: t!("validation_invalid", attribute = t!("locale")).to_string(),
};

valid = false;
errors.push(error_message);
}
}

if !valid {
let error_response = ErrorResponse {
status: valid,
Expand Down
15 changes: 15 additions & 0 deletions src/requests/admin_user_request/update_admin_user_request.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::api::proto::admin_user::UpdateAdminUserRequest;
use crate::models::validation_error::{ErrorMessage, ErrorResponse, Validate};
use rust_i18n::t;
use std::path::Path;

impl UpdateAdminUserRequest {
/// validate
Expand All @@ -18,6 +19,20 @@ impl UpdateAdminUserRequest {
errors.push(error_message);
}

// Validate locale if provided (strict validation)
if !self.locale.is_empty() {
let locale_path = Path::new("resources/locales").join(format!("{}.json", self.locale));
if !locale_path.exists() {
let error_message = ErrorMessage {
key: String::from("locale"),
message: t!("validation_invalid", attribute = t!("locale")).to_string(),
};

valid = false;
errors.push(error_message);
}
}

if !valid {
let error_response = ErrorResponse {
status: valid,
Expand Down
19 changes: 19 additions & 0 deletions src/services/admin_user_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,22 @@ impl AdminUserService {
let password_hash =
self.get_password_hash_from_raw_password(&req.password, password_salt)?;

// Use provided locale or default to "en" if empty
// Request layer already validated that locale exists
let locale = if req.locale.is_empty() {
String::from("en")
} else {
req.locale.clone()
};

let mut created_admin_user_model = CreatableAdminUserModel {
full_name: req.full_name,
email: req.email,
password: password_hash,
profile_image: String::new(),
is_super_admin: req.is_super_admin,
logged_in_username,
locale,
};

if !req.profile_image_file_name.is_empty() {
Expand Down Expand Up @@ -153,13 +162,23 @@ impl AdminUserService {
logged_in_username: String,
(datastore, database_session): &DB,
) -> Result<UpdateAdminUserResponse> {

// Use provided locale or default to "en" if empty
// Request layer already validated that locale exists
let locale = if req.locale.is_empty() {
String::from("en")
} else {
req.locale.clone()
};

let mut updatable_admin_user_model = UpdatableAdminUserModel {
id: req.admin_user_id,
full_name: req.full_name,
profile_image: String::new(),
is_super_admin: req.is_super_admin,
logged_in_username: logged_in_username.clone(),
role_ids: req.role_ids,
locale,
};

if !req.profile_image_file_name.is_empty() {
Expand Down
1 change: 1 addition & 0 deletions src/services/ldap_auth_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ impl LdapAuthService {
profile_image: String::new(),
is_super_admin: false, // LDAP users are not super admins by default
logged_in_username: "system".to_string(),
locale: String::from("en"),
};

self.admin_user_repository
Expand Down
1 change: 1 addition & 0 deletions ts-grpc-vite-admin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ts-grpc-vite-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"google-protobuf": "^4.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.43.0",
"vite": "^7.1.6",
Expand Down
44 changes: 44 additions & 0 deletions ts-grpc-vite-admin/src/components/SelectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import { ChevronDownIcon } from "@heroicons/react/24/solid";

type SelectFieldProps = {
name: string;
label: string;
id?: string;
required?: boolean;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>, ...args: any[]) => void;
disabled?: boolean;
register: any;
children: React.ReactNode;
}

const SelectField = (props: SelectFieldProps) => {
return (
<div>
<label htmlFor={props.id ?? props.name} className="text-sm text-gray-600">
{props.label}
</label>

<div className="mt-1 relative">
<select
disabled={props.disabled ?? false}
id={props.id ?? props.name}
name={props.name}
required={props.required}
value={props.value}
onChange={props.onChange}
className="appearance-none rounded-md ring-1 ring-gray-400 relative border-0 block w-full px-3 py-2 pr-10 text-gray-900 focus:ring-primary-500 focus:outline-none focus:z-10 disabled:bg-gray-200 disabled:opacity-70 sm:text-sm"
{...props.register}
>
{props.children}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<ChevronDownIcon className="h-5 w-5 text-gray-400" />
</div>
</div>
</div>
);
};

export default SelectField;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// Code generated by protoc-gen-grpc-web. DO NOT EDIT.
// versions:
// protoc-gen-grpc-web v1.5.0
// protoc v6.32.0
// protoc v4.25.1
// source: admin_user.proto


Expand Down
12 changes: 12 additions & 0 deletions ts-grpc-vite-admin/src/grpc-avored/admin_user_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export class AdminUserModel extends jspb.Message {
clearRolesList(): AdminUserModel;
addRoles(value?: RoleModel, index?: number): RoleModel;

getLocale(): string;
setLocale(value: string): AdminUserModel;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): AdminUserModel.AsObject;
static toObject(includeInstance: boolean, msg: AdminUserModel): AdminUserModel.AsObject;
Expand All @@ -60,6 +63,7 @@ export namespace AdminUserModel {
createdBy: string,
updatedBy: string,
rolesList: Array<RoleModel.AsObject>,
locale: string,
}
}

Expand Down Expand Up @@ -267,6 +271,9 @@ export class StoreAdminUserRequest extends jspb.Message {
getProfileImageFileName(): string;
setProfileImageFileName(value: string): StoreAdminUserRequest;

getLocale(): string;
setLocale(value: string): StoreAdminUserRequest;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): StoreAdminUserRequest.AsObject;
static toObject(includeInstance: boolean, msg: StoreAdminUserRequest): StoreAdminUserRequest.AsObject;
Expand All @@ -284,6 +291,7 @@ export namespace StoreAdminUserRequest {
isSuperAdmin: boolean,
profileImageContent: Uint8Array | string,
profileImageFileName: string,
locale: string,
}
}

Expand Down Expand Up @@ -376,6 +384,9 @@ export class UpdateAdminUserRequest extends jspb.Message {
getIsSuperAdmin(): boolean;
setIsSuperAdmin(value: boolean): UpdateAdminUserRequest;

getLocale(): string;
setLocale(value: string): UpdateAdminUserRequest;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): UpdateAdminUserRequest.AsObject;
static toObject(includeInstance: boolean, msg: UpdateAdminUserRequest): UpdateAdminUserRequest.AsObject;
Expand All @@ -392,6 +403,7 @@ export namespace UpdateAdminUserRequest {
profileImageFileName: string,
roleIdsList: Array<string>,
isSuperAdmin: boolean,
locale: string,
}
}

Expand Down
Loading