diff --git a/app/module/article_categories/service/article_categories.service.go b/app/module/article_categories/service/article_categories.service.go index 36bce4b..bb85924 100644 --- a/app/module/article_categories/service/article_categories.service.go +++ b/app/module/article_categories/service/article_categories.service.go @@ -72,12 +72,12 @@ func (_i *articleCategoriesService) All(authToken string, req request.ArticleCat } } - createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) - if createdBy != nil { - if createdBy.UserLevel.LevelNumber > 1 { - req.UserLevelId = &createdBy.UserLevelId - } - } + // createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + // if createdBy != nil { + // if createdBy.UserLevel.LevelNumber > 1 { + // req.UserLevelId = &createdBy.UserLevelId + // } + // } results, paging, err := _i.Repo.GetAll(clientId, req) if err != nil { diff --git a/app/module/clients/clients.module.go b/app/module/clients/clients.module.go index 3665247..c6d163d 100644 --- a/app/module/clients/clients.module.go +++ b/app/module/clients/clients.module.go @@ -1,11 +1,12 @@ package clients import ( - "github.com/gofiber/fiber/v2" - "go.uber.org/fx" "netidhub-saas-be/app/module/clients/controller" "netidhub-saas-be/app/module/clients/repository" "netidhub-saas-be/app/module/clients/service" + + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" ) // struct of ClientsRouter @@ -47,6 +48,7 @@ func (_i *ClientsRouter) RegisterClientsRoutes() { router.Get("/", clientsController.All) router.Get("/:id", clientsController.Show) router.Post("/", clientsController.Save) + router.Post("/with-user", clientsController.CreateClientWithUser) router.Put("/:id", clientsController.Update) router.Delete("/:id", clientsController.Delete) }) diff --git a/app/module/clients/controller/clients.controller.go b/app/module/clients/controller/clients.controller.go index 9209db8..d089e2b 100644 --- a/app/module/clients/controller/clients.controller.go +++ b/app/module/clients/controller/clients.controller.go @@ -32,6 +32,9 @@ type ClientsController interface { MoveClient(c *fiber.Ctx) error GetClientStats(c *fiber.Ctx) error BulkCreateSubClients(c *fiber.Ctx) error + + // Client with user creation + CreateClientWithUser(c *fiber.Ctx) error } func NewClientsController(clientsService service.ClientsService, log zerolog.Logger) ClientsController { @@ -453,3 +456,37 @@ func (_i *clientsController) BulkCreateSubClients(c *fiber.Ctx) error { Data: result, }) } + +// CreateClientWithUser creates a client and admin user in one request +// @Summary Create client with admin user +// @Description API for creating a client and its admin user in a single request +// @Tags Clients +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param payload body request.ClientWithUserCreateRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/with-user [post] +func (_i *clientsController) CreateClientWithUser(c *fiber.Ctx) error { + req := new(request.ClientWithUserCreateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + authToken := c.Get("Authorization") + result, err := _i.clientsService.CreateClientWithUser(*req, authToken) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{result.Message}, + Data: result, + }) +} diff --git a/app/module/clients/request/clients.request.go b/app/module/clients/request/clients.request.go index 1174b8a..40f0a2d 100644 --- a/app/module/clients/request/clients.request.go +++ b/app/module/clients/request/clients.request.go @@ -25,6 +25,33 @@ type ClientsCreateRequest struct { Settings *string `json:"settings"` } +// ClientWithUserCreateRequest for creating client and admin user in one request +type ClientWithUserCreateRequest struct { + // Client information + Client ClientsCreateRequest `json:"client" validate:"required"` + + // Admin user information + AdminUser AdminUserCreateRequest `json:"adminUser" validate:"required"` +} + +// AdminUserCreateRequest for creating admin user +type AdminUserCreateRequest struct { + Username string `json:"username" validate:"required,lowercase"` + Email string `json:"email" validate:"required,email"` + Fullname string `json:"fullname" validate:"required"` + Password string `json:"password" validate:"required"` + PhoneNumber *string `json:"phoneNumber"` + Address *string `json:"address"` + WorkType *string `json:"workType"` + GenderType *string `json:"genderType"` + IdentityType *string `json:"identityType"` + IdentityGroup *string `json:"identityGroup"` + IdentityGroupNumber *string `json:"identityGroupNumber"` + IdentityNumber *string `json:"identityNumber"` + DateOfBirth *string `json:"dateOfBirth"` + LastEducation *string `json:"lastEducation"` +} + // ClientsUpdateRequest for updating existing client type ClientsUpdateRequest struct { Name *string `json:"name"` @@ -67,7 +94,7 @@ type MoveClientRequest struct { // BulkCreateSubClientsRequest for creating multiple sub-clients at once type BulkCreateSubClientsRequest struct { - ParentClientId uuid.UUID `json:"parentClientId" validate:"required"` + ParentClientId uuid.UUID `json:"parentClientId" validate:"required"` SubClients []ClientsCreateSubClientDetail `json:"subClients" validate:"required,min=1,dive"` } diff --git a/app/module/clients/response/clients.response.go b/app/module/clients/response/clients.response.go index aa75262..53428ce 100644 --- a/app/module/clients/response/clients.response.go +++ b/app/module/clients/response/clients.response.go @@ -42,6 +42,38 @@ type ClientsResponse struct { UpdatedAt time.Time `json:"updatedAt"` } +// ClientWithUserResponse for creating client and admin user response +type ClientWithUserResponse struct { + Client ClientsResponse `json:"client"` + AdminUser AdminUserResponse `json:"adminUser"` + Message string `json:"message"` +} + +// AdminUserResponse for admin user info +type AdminUserResponse struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Fullname string `json:"fullname"` + UserLevelId uint `json:"userLevelId"` + UserRoleId uint `json:"userRoleId"` + PhoneNumber *string `json:"phoneNumber"` + Address *string `json:"address"` + WorkType *string `json:"workType"` + GenderType *string `json:"genderType"` + IdentityType *string `json:"identityType"` + IdentityGroup *string `json:"identityGroup"` + IdentityGroupNumber *string `json:"identityGroupNumber"` + IdentityNumber *string `json:"identityNumber"` + DateOfBirth *string `json:"dateOfBirth"` + LastEducation *string `json:"lastEducation"` + KeycloakId *string `json:"keycloakId"` + ClientId uuid.UUID `json:"clientId"` + IsActive bool `json:"isActive"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + // ParentClientInfo minimal parent info type ParentClientInfo struct { ID uuid.UUID `json:"id"` @@ -158,4 +190,4 @@ type BulkOperationResult struct { Name string `json:"name"` Success bool `json:"success"` Error *string `json:"error,omitempty"` -} \ No newline at end of file +} diff --git a/app/module/clients/service/clients.service.go b/app/module/clients/service/clients.service.go index 8e90bc4..a08895c 100644 --- a/app/module/clients/service/clients.service.go +++ b/app/module/clients/service/clients.service.go @@ -2,12 +2,15 @@ package service import ( "errors" + "fmt" "netidhub-saas-be/app/database/entity" "netidhub-saas-be/app/module/clients/mapper" "netidhub-saas-be/app/module/clients/repository" "netidhub-saas-be/app/module/clients/request" "netidhub-saas-be/app/module/clients/response" usersRepository "netidhub-saas-be/app/module/users/repository" + usersRequest "netidhub-saas-be/app/module/users/request" + usersService "netidhub-saas-be/app/module/users/service" "netidhub-saas-be/utils/paginator" "github.com/google/uuid" @@ -20,6 +23,7 @@ import ( type clientsService struct { Repo repository.ClientsRepository UsersRepo usersRepository.UsersRepository + UsersSvc usersService.UsersService Log zerolog.Logger } @@ -30,22 +34,26 @@ type ClientsService interface { Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error) Update(id uuid.UUID, req request.ClientsUpdateRequest) (err error) Delete(id uuid.UUID) error - + // New hierarchy methods CreateSubClient(parentId uuid.UUID, req request.ClientsCreateRequest) (*entity.Clients, error) MoveClient(clientId uuid.UUID, req request.MoveClientRequest) error GetHierarchy(clientId uuid.UUID) (*response.ClientHierarchyResponse, error) GetClientStats(clientId uuid.UUID) (*response.ClientStatsResponse, error) BulkCreateSubClients(req request.BulkCreateSubClientsRequest) (*response.BulkOperationResponse, error) + + // Client with user creation + CreateClientWithUser(req request.ClientWithUserCreateRequest, authToken string) (*response.ClientWithUserResponse, error) } // NewClientsService init ClientsService -func NewClientsService(repo repository.ClientsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository) ClientsService { +func NewClientsService(repo repository.ClientsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, usersSvc usersService.UsersService) ClientsService { return &clientsService{ Repo: repo, Log: log, UsersRepo: usersRepo, + UsersSvc: usersSvc, } } @@ -300,3 +308,108 @@ func (_i *clientsService) BulkCreateSubClients(req request.BulkCreateSubClientsR Results: results, }, nil } + +// CreateClientWithUser creates a client and admin user in one transaction +func (_i *clientsService) CreateClientWithUser(req request.ClientWithUserCreateRequest, authToken string) (*response.ClientWithUserResponse, error) { + _i.Log.Info().Interface("data", req).Msg("Creating client with admin user") + + // Step 1: Create the client + clientReq := req.Client + newClient := &entity.Clients{ + Name: clientReq.Name, + Description: clientReq.Description, + ClientType: clientReq.ClientType, + ParentClientId: clientReq.ParentClientId, + MaxUsers: clientReq.MaxUsers, + MaxStorage: clientReq.MaxStorage, + Settings: clientReq.Settings, + } + + // Generate new UUID for client + newClient.ID = uuid.New() + + // Set created by if auth token provided + if authToken != "" { + createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if createdBy != nil { + newClient.CreatedById = &createdBy.ID + } + } + + // Create client + createdClient, err := _i.Repo.Create(newClient) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create client") + return nil, fmt.Errorf("failed to create client: %w", err) + } + + _i.Log.Info().Interface("clientId", createdClient.ID).Msg("Client created successfully") + + // Step 2: Create admin user for the client + adminUserReq := req.AdminUser + + // Convert to UsersCreateRequest format + userCreateReq := usersRequest.UsersCreateRequest{ + Username: adminUserReq.Username, + Email: adminUserReq.Email, + Fullname: adminUserReq.Fullname, + Password: adminUserReq.Password, + PhoneNumber: adminUserReq.PhoneNumber, + Address: adminUserReq.Address, + WorkType: adminUserReq.WorkType, + GenderType: adminUserReq.GenderType, + IdentityType: adminUserReq.IdentityType, + IdentityGroup: adminUserReq.IdentityGroup, + IdentityGroupNumber: adminUserReq.IdentityGroupNumber, + IdentityNumber: adminUserReq.IdentityNumber, + DateOfBirth: adminUserReq.DateOfBirth, + LastEducation: adminUserReq.LastEducation, + // Set default admin level and role (you may need to adjust these based on your system) + UserLevelId: 1, // Assuming level 1 is admin level + UserRoleId: 1, // Assuming role 1 is admin role + } + + // Create user with the new client ID + createdUser, err := _i.UsersSvc.Save(authToken, userCreateReq) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create admin user") + // Rollback: delete the created client + _i.Repo.Delete(createdClient.ID) + return nil, fmt.Errorf("failed to create admin user: %w", err) + } + + _i.Log.Info().Interface("userId", createdUser.ID).Msg("Admin user created successfully") + + // Step 3: Prepare response + clientResponse := mapper.ClientsResponseMapper(createdClient) + + adminUserResponse := response.AdminUserResponse{ + ID: createdUser.ID, + Username: createdUser.Username, + Email: createdUser.Email, + Fullname: createdUser.Fullname, + UserLevelId: createdUser.UserLevelId, + UserRoleId: createdUser.UserRoleId, + PhoneNumber: createdUser.PhoneNumber, + Address: createdUser.Address, + WorkType: createdUser.WorkType, + GenderType: createdUser.GenderType, + IdentityType: createdUser.IdentityType, + IdentityGroup: createdUser.IdentityGroup, + IdentityGroupNumber: createdUser.IdentityGroupNumber, + IdentityNumber: createdUser.IdentityNumber, + DateOfBirth: createdUser.DateOfBirth, + LastEducation: createdUser.LastEducation, + KeycloakId: createdUser.KeycloakId, + ClientId: *createdUser.ClientId, + IsActive: *createdUser.IsActive, + CreatedAt: createdUser.CreatedAt, + UpdatedAt: createdUser.UpdatedAt, + } + + return &response.ClientWithUserResponse{ + Client: *clientResponse, + AdminUser: adminUserResponse, + Message: fmt.Sprintf("Client '%s' and admin user '%s' created successfully", createdClient.Name, createdUser.Username), + }, nil +} diff --git a/docs/API_CREATE_CLIENT_WITH_USER.md b/docs/API_CREATE_CLIENT_WITH_USER.md new file mode 100644 index 0000000..8eee5cb --- /dev/null +++ b/docs/API_CREATE_CLIENT_WITH_USER.md @@ -0,0 +1,282 @@ +# API Create Client with User + +## ๐Ÿš€ **Overview** + +API ini memungkinkan Anda untuk membuat client dan admin user dalam satu request. Sangat berguna untuk setup awal sistem atau ketika Anda perlu membuat client baru dengan admin user yang sudah siap. + +## ๐Ÿ“‹ **Endpoint** + +``` +POST /api/clients/with-user +``` + +## ๐Ÿ”ง **Request Body** + +```json +{ + "client": { + "name": "Client Name", + "description": "Client description", + "clientType": "standalone", + "parentClientId": null, + "maxUsers": 100, + "maxStorage": 1073741824, + "settings": "{\"theme\": \"dark\", \"language\": \"en\"}" + }, + "adminUser": { + "username": "admin", + "email": "admin@client.com", + "fullname": "Admin User", + "password": "SecurePassword123!", + "phoneNumber": "+6281234567890", + "address": "Jakarta, Indonesia", + "workType": "fulltime", + "genderType": "male", + "identityType": "ktp", + "identityGroup": "employee", + "identityGroupNumber": "EMP001", + "identityNumber": "1234567890123456", + "dateOfBirth": "1990-01-01", + "lastEducation": "Bachelor" + } +} +``` + +## ๐Ÿ“ **Field Descriptions** + +### **Client Fields:** +- `name` (required): Nama client +- `description` (optional): Deskripsi client +- `clientType` (required): Tipe client (`standalone`, `parent_client`, `sub_client`) +- `parentClientId` (optional): ID parent client (untuk sub_client) +- `maxUsers` (optional): Maksimal jumlah user +- `maxStorage` (optional): Maksimal storage dalam bytes +- `settings` (optional): Settings dalam format JSON string + +### **Admin User Fields:** +- `username` (required): Username untuk login +- `email` (required): Email address +- `fullname` (required): Nama lengkap +- `password` (required): Password untuk login +- `phoneNumber` (optional): Nomor telepon +- `address` (optional): Alamat +- `workType` (optional): Tipe pekerjaan +- `genderType` (optional): Jenis kelamin +- `identityType` (optional): Tipe identitas +- `identityGroup` (optional): Grup identitas +- `identityGroupNumber` (optional): Nomor grup identitas +- `identityNumber` (optional): Nomor identitas +- `dateOfBirth` (optional): Tanggal lahir (YYYY-MM-DD) +- `lastEducation` (optional): Pendidikan terakhir + +## ๐Ÿ“ค **Response** + +### **Success Response (200):** +```json +{ + "success": true, + "messages": ["Client 'Client Name' and admin user 'admin' created successfully"], + "data": { + "client": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Client Name", + "description": "Client description", + "clientType": "standalone", + "parentClientId": null, + "maxUsers": 100, + "maxStorage": 1073741824, + "currentUsers": 1, + "currentStorage": null, + "settings": "{\"theme\": \"dark\", \"language\": \"en\"}", + "createdById": 1, + "isActive": true, + "createdAt": "2025-01-01T12:00:00Z", + "updatedAt": "2025-01-01T12:00:00Z" + }, + "adminUser": { + "id": 123, + "username": "admin", + "email": "admin@client.com", + "fullname": "Admin User", + "userLevelId": 1, + "userRoleId": 1, + "phoneNumber": "+6281234567890", + "address": "Jakarta, Indonesia", + "workType": "fulltime", + "genderType": "male", + "identityType": "ktp", + "identityGroup": "employee", + "identityGroupNumber": "EMP001", + "identityNumber": "1234567890123456", + "dateOfBirth": "1990-01-01", + "lastEducation": "Bachelor", + "keycloakId": "550e8400-e29b-41d4-a716-446655440001", + "clientId": "550e8400-e29b-41d4-a716-446655440000", + "isActive": true, + "createdAt": "2025-01-01T12:00:00Z", + "updatedAt": "2025-01-01T12:00:00Z" + }, + "message": "Client 'Client Name' and admin user 'admin' created successfully" + } +} +``` + +### **Error Response (400):** +```json +{ + "success": false, + "messages": ["Validation failed: [Username is required, Email is required]"] +} +``` + +### **Error Response (500):** +```json +{ + "success": false, + "messages": ["Failed to create admin user: Keycloak user creation failed"] +} +``` + +## ๐Ÿงช **Curl Examples** + +### **1. Create Standalone Client with Admin User:** +```bash +curl -X POST "http://localhost:8080/api/clients/with-user" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "client": { + "name": "PT. Example Company", + "description": "A technology company", + "clientType": "standalone", + "maxUsers": 50, + "maxStorage": 5368709120, + "settings": "{\"theme\": \"light\", \"language\": \"id\"}" + }, + "adminUser": { + "username": "admin", + "email": "admin@example.com", + "fullname": "John Admin", + "password": "SecurePass123!", + "phoneNumber": "+6281234567890", + "address": "Jakarta, Indonesia", + "workType": "fulltime", + "genderType": "male" + } + }' +``` + +### **2. Create Sub Client with Admin User:** +```bash +curl -X POST "http://localhost:8080/api/clients/with-user" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "client": { + "name": "PT. Example Branch Office", + "description": "Branch office of main company", + "clientType": "sub_client", + "parentClientId": "550e8400-e29b-41d4-a716-446655440000", + "maxUsers": 25, + "maxStorage": 2684354560 + }, + "adminUser": { + "username": "branch_admin", + "email": "branch@example.com", + "fullname": "Jane Branch Admin", + "password": "BranchPass123!", + "phoneNumber": "+6289876543210", + "address": "Surabaya, Indonesia" + } + }' +``` + +### **3. Create Parent Client with Admin User:** +```bash +curl -X POST "http://localhost:8080/api/clients/with-user" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "client": { + "name": "PT. Parent Company", + "description": "Parent company that can have sub-clients", + "clientType": "parent_client", + "maxUsers": 200, + "maxStorage": 10737418240, + "settings": "{\"allowSubClients\": true, \"maxSubClients\": 10}" + }, + "adminUser": { + "username": "parent_admin", + "email": "parent@company.com", + "fullname": "Parent Admin User", + "password": "ParentPass123!", + "phoneNumber": "+6281111111111", + "address": "Bandung, Indonesia", + "workType": "fulltime", + "genderType": "female", + "identityType": "ktp", + "identityNumber": "1234567890123456" + } + }' +``` + +## ๐Ÿ”’ **Authentication** + +API ini memerlukan authentication token. Token harus disertakan di header: + +``` +Authorization: Bearer YOUR_JWT_TOKEN +``` + +## โš ๏ธ **Important Notes** + +1. **User Level & Role**: Admin user akan otomatis diberikan: + - `userLevelId`: 1 (Admin level) + - `userRoleId`: 1 (Admin role) + +2. **Keycloak Integration**: User akan otomatis dibuat di Keycloak dengan `keycloakId` yang di-generate. + +3. **Transaction Safety**: Jika pembuatan user gagal, client yang sudah dibuat akan di-rollback (dihapus). + +4. **Password Requirements**: Password harus memenuhi policy keamanan Keycloak. + +5. **Email Uniqueness**: Email harus unique di seluruh sistem. + +6. **Username Uniqueness**: Username harus unique di seluruh sistem. + +## ๐Ÿšจ **Error Handling** + +### **Common Errors:** + +1. **Validation Error (400)**: + - Missing required fields + - Invalid email format + - Invalid client type + +2. **Authentication Error (401)**: + - Invalid or missing token + - Expired token + +3. **Server Error (500)**: + - Keycloak connection failed + - Database error + - User creation failed + +## ๐Ÿ”„ **Workflow** + +1. **Validate Request**: Cek semua field yang required +2. **Create Client**: Buat client di database +3. **Create User in Keycloak**: Buat user di Keycloak +4. **Create User in Database**: Simpan user info di database +5. **Return Response**: Return client dan user info + +## ๐Ÿ“Š **Use Cases** + +1. **Initial Setup**: Setup client baru dengan admin user +2. **Branch Office**: Buat branch office dengan admin lokal +3. **Subsidiary**: Buat subsidiary company dengan admin sendiri +4. **Multi-tenant**: Setup tenant baru dalam sistem multi-tenant + +--- + +**API ini sangat berguna untuk setup awal sistem atau ketika Anda perlu membuat client baru dengan admin user yang sudah siap!** ๐Ÿš€