Browse Source

feat: add created_by/updated_by tracking to backend entities

- Add created_by and updated_by fields to User, Workspace, Group,
  Collection, and Membership entities
- Add actor_id parameter to Create and Update request structs
- Update service implementations to store and retrieve the new fields
- Add ResolveUserName helper function in HTTP server
- Update API responses to include created_by_name and updated_by_name
Fszontagh 6 tháng trước cách đây
mục cha
commit
2b1299be6a

+ 3 - 0
webserver/include/smartbotic/webserver/collection_service.hpp

@@ -32,6 +32,8 @@ struct CollectionInfo {
     int64_t size_bytes = 0;
     std::string created_at;
     std::string updated_at;
+    std::string created_by;       // User ID who created this collection
+    std::string updated_by;       // User ID who last updated this collection
     CollectionSettings settings;
 };
 
@@ -40,6 +42,7 @@ struct CreateCollectionRequest {
     std::string workspace_id;
     std::string name;             // Display name
     CollectionSettings settings;
+    std::string actor_id;         // User ID performing the action
 };
 
 /// Result of a collection operation

+ 4 - 0
webserver/include/smartbotic/webserver/group_service.hpp

@@ -19,6 +19,8 @@ struct Group {
     std::string created_at;
     std::string updated_at;
     std::string deleted_at;  // Empty if not deleted (soft delete)
+    std::string created_by;  // User ID who created this group
+    std::string updated_by;  // User ID who last updated this group
 
     /// Check if group is deleted (soft delete)
     [[nodiscard]] auto IsDeleted() const -> bool { return !deleted_at.empty(); }
@@ -40,6 +42,7 @@ struct CreateGroupRequest {
     std::vector<std::string> permissions;
     bool is_system = false;         // Only superadmin can create system groups
     std::string parent_group_id;    // Optional parent for permission inheritance
+    std::string actor_id;  // User ID performing the action
 };
 
 /// Request to update a group
@@ -47,6 +50,7 @@ struct UpdateGroupRequest {
     std::optional<std::string> name;
     std::optional<std::vector<std::string>> permissions;
     std::optional<std::string> parent_group_id;
+    std::string actor_id;  // User ID performing the action
     // Note: is_system cannot be changed after creation
 };
 

+ 3 - 0
webserver/include/smartbotic/webserver/http_server.hpp

@@ -282,6 +282,9 @@ private:
     // Check if user has superadmin privileges (uses authorization service)
     [[nodiscard]] auto IsSuperadmin(const AuthUser& user) -> bool;
 
+    // Resolve user ID to user name for display
+    [[nodiscard]] auto ResolveUserName(const std::string& user_id) -> std::string;
+
     // Static file serving
     [[nodiscard]] static auto GetMimeType(const std::string& path) -> std::string;
 

+ 4 - 0
webserver/include/smartbotic/webserver/membership_service.hpp

@@ -17,6 +17,8 @@ struct WorkspaceMember {
     std::string created_at;
     std::string updated_at;
     std::string deleted_at;  // Empty if not deleted (soft delete)
+    std::string created_by;  // User ID who created this membership
+    std::string updated_by;  // User ID who last updated this membership
 
     /// Check if membership is deleted (soft delete)
     [[nodiscard]] auto IsDeleted() const -> bool { return !deleted_at.empty(); }
@@ -27,11 +29,13 @@ struct AddMemberRequest {
     std::string workspace_id;
     std::string user_id;
     std::vector<std::string> group_ids;
+    std::string actor_id;  // User ID performing the action
 };
 
 /// Request to update a membership
 struct UpdateMemberRequest {
     std::optional<std::vector<std::string>> group_ids;
+    std::string actor_id;  // User ID performing the action
 };
 
 /// Result of a membership operation

+ 4 - 0
webserver/include/smartbotic/webserver/user_service.hpp

@@ -17,6 +17,8 @@ struct User {
     std::string created_at;
     std::string updated_at;
     std::string deleted_at;  // Empty if not deleted (soft delete)
+    std::string created_by;  // User ID who created this user
+    std::string updated_by;  // User ID who last updated this user
 
     /// Check if user is deleted (soft delete)
     [[nodiscard]] auto IsDeleted() const -> bool { return !deleted_at.empty(); }
@@ -27,6 +29,7 @@ struct CreateUserRequest {
     std::string email;
     std::string password;  // Plain text, will be hashed
     std::string name;
+    std::string actor_id;  // User ID performing the action (empty for self-registration)
 };
 
 /// Request to update a user
@@ -34,6 +37,7 @@ struct UpdateUserRequest {
     std::optional<std::string> email;
     std::optional<std::string> password;  // Plain text, will be hashed
     std::optional<std::string> name;
+    std::string actor_id;  // User ID performing the action
 };
 
 /// Result of a user operation

+ 4 - 0
webserver/include/smartbotic/webserver/workspace_service.hpp

@@ -18,6 +18,8 @@ struct Workspace {
     std::string created_at;
     std::string updated_at;
     std::string deleted_at;  // Empty if not deleted (soft delete)
+    std::string created_by;  // User ID who created this workspace
+    std::string updated_by;  // User ID who last updated this workspace
 
     /// Check if workspace is deleted (soft delete)
     [[nodiscard]] auto IsDeleted() const -> bool { return !deleted_at.empty(); }
@@ -27,12 +29,14 @@ struct Workspace {
 struct CreateWorkspaceRequest {
     std::string name;
     nlohmann::json settings = nlohmann::json::object();  // Optional settings
+    std::string actor_id;  // User ID performing the action
 };
 
 /// Request to update a workspace
 struct UpdateWorkspaceRequest {
     std::optional<std::string> name;
     std::optional<nlohmann::json> settings;
+    std::string actor_id;  // User ID performing the action
 };
 
 /// Result of a workspace operation

+ 9 - 0
webserver/src/group_service.cpp

@@ -165,6 +165,8 @@ auto GroupService::CreateSystemGroup(const std::string& name, const std::vector<
     SetStringValue(data, "created_at", GetCurrentTimestamp());
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
     SetStringValue(data, "deleted_at", "");
+    SetStringValue(data, "created_by", "");  // System-created
+    SetStringValue(data, "updated_by", "");
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document created_doc;
@@ -229,6 +231,8 @@ auto GroupService::CreateGroup(const CreateGroupRequest& request) -> GroupResult
     SetStringValue(data, "created_at", GetCurrentTimestamp());
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
     SetStringValue(data, "deleted_at", "");
+    SetStringValue(data, "created_by", request.actor_id);
+    SetStringValue(data, "updated_by", request.actor_id);
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document created_doc;
@@ -473,6 +477,9 @@ auto GroupService::UpdateGroup(const std::string& id, const UpdateGroupRequest&
     }
 
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
+    if (!request.actor_id.empty()) {
+        SetStringValue(data, "updated_by", request.actor_id);
+    }
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document updated_doc;
@@ -549,6 +556,8 @@ auto GroupService::DocumentToGroup(const ::smartbotic::database::Document& doc)
     group.created_at = GetStringValue(doc.data(), "created_at");
     group.updated_at = GetStringValue(doc.data(), "updated_at");
     group.deleted_at = GetStringValue(doc.data(), "deleted_at");
+    group.created_by = GetStringValue(doc.data(), "created_by");
+    group.updated_by = GetStringValue(doc.data(), "updated_by");
     return group;
 }
 

+ 42 - 6
webserver/src/http_server.cpp

@@ -1140,6 +1140,17 @@ auto HttpServer::IsSuperadmin(const AuthUser& user) -> bool {
     return std::find(user.groups.begin(), user.groups.end(), "superadmin") != user.groups.end();
 }
 
+auto HttpServer::ResolveUserName(const std::string& user_id) -> std::string {
+    if (user_id.empty() || !userService_) {
+        return "";
+    }
+    auto result = userService_->GetUser(user_id, true);
+    if (result.success && result.user) {
+        return result.user->name;
+    }
+    return "";
+}
+
 void HttpServer::HandleCreateUser(const httplib::Request& req, httplib::Response& res) {
     // Authenticate the request
     auto auth_user = AuthenticateRequest(req);
@@ -1265,14 +1276,19 @@ void HttpServer::HandleListUsers(const httplib::Request& req, httplib::Response&
         // Build response
         nlohmann::json users_array = nlohmann::json::array();
         for (const auto& user : result.users) {
-            users_array.push_back({
+            nlohmann::json user_obj = {
                 {"id", user.id},
                 {"email", user.email},
                 {"name", user.name},
                 {"created_at", user.created_at},
                 {"updated_at", user.updated_at},
-                {"deleted_at", user.deleted_at}
-            });
+                {"deleted_at", user.deleted_at},
+                {"created_by", user.created_by},
+                {"updated_by", user.updated_by},
+                {"created_by_name", ResolveUserName(user.created_by)},
+                {"updated_by_name", ResolveUserName(user.updated_by)}
+            };
+            users_array.push_back(user_obj);
         }
 
         nlohmann::json response = {
@@ -1621,7 +1637,11 @@ void HttpServer::HandleListWorkspaces(const httplib::Request& req, httplib::Resp
                 {"name", workspace.name},
                 {"settings", workspace.settings},
                 {"created_at", workspace.created_at},
-                {"updated_at", workspace.updated_at}
+                {"updated_at", workspace.updated_at},
+                {"created_by", workspace.created_by},
+                {"updated_by", workspace.updated_by},
+                {"created_by_name", ResolveUserName(workspace.created_by)},
+                {"updated_by_name", ResolveUserName(workspace.updated_by)}
             };
             if (IsSuperadmin(*auth_user)) {
                 ws_json["deleted_at"] = workspace.deleted_at;
@@ -2006,7 +2026,11 @@ void HttpServer::HandleListGroups(const httplib::Request& req, httplib::Response
                         {"parent_group_id", group.parent_group_id},
                         {"created_at", group.created_at},
                         {"updated_at", group.updated_at},
-                        {"deleted_at", group.deleted_at}
+                        {"deleted_at", group.deleted_at},
+                        {"created_by", group.created_by},
+                        {"updated_by", group.updated_by},
+                        {"created_by_name", ResolveUserName(group.created_by)},
+                        {"updated_by_name", ResolveUserName(group.updated_by)}
                     };
                     groups_array.push_back(grp_json);
                 }
@@ -2022,7 +2046,11 @@ void HttpServer::HandleListGroups(const httplib::Request& req, httplib::Response
                 {"is_system", group.is_system},
                 {"parent_group_id", group.parent_group_id},
                 {"created_at", group.created_at},
-                {"updated_at", group.updated_at}
+                {"updated_at", group.updated_at},
+                {"created_by", group.created_by},
+                {"updated_by", group.updated_by},
+                {"created_by_name", ResolveUserName(group.created_by)},
+                {"updated_by_name", ResolveUserName(group.updated_by)}
             };
             if (IsSuperadmin(*auth_user)) {
                 grp_json["deleted_at"] = group.deleted_at;
@@ -3116,6 +3144,10 @@ void HttpServer::HandleListCollections(const httplib::Request& req, httplib::Res
                 {"created_at", coll.created_at},
                 {"updated_at", coll.updated_at},
                 {"is_system", is_system},
+                {"created_by", coll.created_by},
+                {"updated_by", coll.updated_by},
+                {"created_by_name", ResolveUserName(coll.created_by)},
+                {"updated_by_name", ResolveUserName(coll.updated_by)},
                 {"settings", {
                     {"schema", coll.settings.schema},
                     {"encrypted_fields", coll.settings.encrypted_fields},
@@ -3138,6 +3170,10 @@ void HttpServer::HandleListCollections(const httplib::Request& req, httplib::Res
                         {"created_at", coll.created_at},
                         {"updated_at", coll.updated_at},
                         {"is_system", true},
+                        {"created_by", coll.created_by},
+                        {"updated_by", coll.updated_by},
+                        {"created_by_name", ResolveUserName(coll.created_by)},
+                        {"updated_by_name", ResolveUserName(coll.updated_by)},
                         {"settings", nlohmann::json::object()}
                     };
                     collections_array.push_back(coll_json);

+ 8 - 0
webserver/src/membership_service.cpp

@@ -83,6 +83,8 @@ auto MembershipService::DocumentToMember(const ::smartbotic::database::Document&
         member.created_at = GetStringValue(data, "created_at");
         member.updated_at = GetStringValue(data, "updated_at");
         member.deleted_at = GetStringValue(data, "deleted_at");
+        member.created_by = GetStringValue(data, "created_by");
+        member.updated_by = GetStringValue(data, "updated_by");
     }
 
     return member;
@@ -181,6 +183,8 @@ auto MembershipService::AddMember(const AddMemberRequest& request) -> Membership
     SetStringValue(data, "created_at", GetCurrentTimestamp());
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
     SetStringValue(data, "deleted_at", "");
+    SetStringValue(data, "created_by", request.actor_id);
+    SetStringValue(data, "updated_by", request.actor_id);
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document created_doc;
@@ -441,6 +445,10 @@ auto MembershipService::UpdateMembership(const std::string& workspace_id, const
 
     SetStringValue(data, "updated_at", timestamp);
     member.updated_at = timestamp;
+    if (!request.actor_id.empty()) {
+        SetStringValue(data, "updated_by", request.actor_id);
+        member.updated_by = request.actor_id;
+    }
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document updated_doc;

+ 7 - 0
webserver/src/user_service.cpp

@@ -136,6 +136,8 @@ auto UserService::CreateUser(const CreateUserRequest& request) -> UserResult {
     SetStringValue(data, "created_at", GetCurrentTimestamp());
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
     SetStringValue(data, "deleted_at", "");
+    SetStringValue(data, "created_by", request.actor_id);
+    SetStringValue(data, "updated_by", request.actor_id);
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document created_doc;
@@ -318,6 +320,9 @@ auto UserService::UpdateUser(const std::string& id, const UpdateUserRequest& req
     }
 
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
+    if (!request.actor_id.empty()) {
+        SetStringValue(data, "updated_by", request.actor_id);
+    }
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document updated_doc;
@@ -396,6 +401,8 @@ auto UserService::DocumentToUser(const ::smartbotic::database::Document& doc) ->
     user.created_at = GetStringValue(doc.data(), "created_at");
     user.updated_at = GetStringValue(doc.data(), "updated_at");
     user.deleted_at = GetStringValue(doc.data(), "deleted_at");
+    user.created_by = GetStringValue(doc.data(), "created_by");
+    user.updated_by = GetStringValue(doc.data(), "updated_by");
     return user;
 }
 

+ 7 - 0
webserver/src/workspace_service.cpp

@@ -119,6 +119,8 @@ auto WorkspaceService::CreateWorkspace(const CreateWorkspaceRequest& request) ->
     SetStringValue(data, "created_at", GetCurrentTimestamp());
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
     SetStringValue(data, "deleted_at", "");
+    SetStringValue(data, "created_by", request.actor_id);
+    SetStringValue(data, "updated_by", request.actor_id);
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document created_doc;
@@ -316,6 +318,9 @@ auto WorkspaceService::UpdateWorkspace(const std::string& id, const UpdateWorksp
     }
 
     SetStringValue(data, "updated_at", GetCurrentTimestamp());
+    if (!request.actor_id.empty()) {
+        SetStringValue(data, "updated_by", request.actor_id);
+    }
 
     grpc::ClientContext ctx;
     ::smartbotic::database::Document updated_doc;
@@ -379,6 +384,8 @@ auto WorkspaceService::DocumentToWorkspace(const ::smartbotic::database::Documen
     workspace.created_at = GetStringValue(doc.data(), "created_at");
     workspace.updated_at = GetStringValue(doc.data(), "updated_at");
     workspace.deleted_at = GetStringValue(doc.data(), "deleted_at");
+    workspace.created_by = GetStringValue(doc.data(), "created_by");
+    workspace.updated_by = GetStringValue(doc.data(), "updated_by");
 
     // Parse settings JSON
     std::string settings_str = GetStringValue(doc.data(), "settings");