diff --git a/internal/domain/access/handler.go b/internal/domain/access/handler.go new file mode 100644 index 0000000..d056901 --- /dev/null +++ b/internal/domain/access/handler.go @@ -0,0 +1,69 @@ +package access + +import ( + "antrian-operasi/internal/shared" + + "github.com/gin-gonic/gin" +) + +type AccessHandler struct { + repo IAccessRepository +} + +func NewAccessHandler(repo IAccessRepository) AccessHandler { + return AccessHandler{repo} +} + +func (h AccessHandler) SyncKeycloakRole(c *gin.Context) { + var req SyncKeycloakRoleRequest + + //bind json body + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, shared.BaseErrorResponse{ + Success: false, + Code: 400, + Message: "error bind json", + Errors: shared.ValidationError(err), + }) + return + } + + //check user role exist + users, err := h.repo.FindUserByKeycloakId(c, req.KeycloakId) + if err != nil { + errMessage := []string{err.Error()} + c.JSON(500, shared.BaseErrorResponse{ + Success: false, + Code: 500, + Message: "error finding user by keycloak id", + Errors: errMessage, + }) + return + } + + //if not exist, create user & permission + if len(users) == 0 { + err = h.repo.CreateUserPermission(c, req) + } else { //if exist, update role permission + userId := users[0].ID + err = h.repo.UpdateUserPermission(c, userId, req) + } + + if err != nil { + errMessage := []string{err.Error()} + c.JSON(500, shared.BaseErrorResponse{ + Success: false, + Code: 500, + Message: "update / insert permission error", + Errors: errMessage, + }) + return + } + + c.JSON(200, shared.BaseResponse[SyncKeycloakRoleRequest]{ + Success: true, + Code: 200, + Message: "success sync role", + Data: req, + }) +} diff --git a/internal/domain/access/model.go b/internal/domain/access/model.go new file mode 100644 index 0000000..234e01b --- /dev/null +++ b/internal/domain/access/model.go @@ -0,0 +1,19 @@ +package access + +type RolePermissionModel struct { + ID string `db:"id"` + Name string `db:"name"` + IsActive bool `db:"is_active"` +} + +type RolePageModel struct { + ID string `db:"id"` + PagePath string `db:"page_path"` +} + +type RoleUserModel struct { + ID string `db:"id"` + Name string `db:"name"` + Email string `db:"email"` + KeycloakId string `db:"keycloak_id"` +} diff --git a/internal/domain/access/repository.go b/internal/domain/access/repository.go new file mode 100644 index 0000000..f9456d9 --- /dev/null +++ b/internal/domain/access/repository.go @@ -0,0 +1,351 @@ +package access + +import ( + "antrian-operasi/internal/database" + queryUtils "antrian-operasi/internal/utils/query" + "context" + "database/sql" + "log" + "time" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +const DB_NAME = "db_role_access" +const TBL_USER = "role_users" +const TBL_PERMISSION = "role_permission" +const TBL_USER_PERMISSION = "role_user_permission" + +type IAccessRepository interface { + FindUserByKeycloakId(c context.Context, id string) ([]RoleUserModel, error) + CreateUserPermission(c context.Context, req SyncKeycloakRoleRequest) error + UpdateUserPermission(c context.Context, userId string, req SyncKeycloakRoleRequest) error +} + +type accessRepo struct { + queryBuilder *queryUtils.QueryBuilder + db database.Service +} + +func NewRepository(dbService database.Service) IAccessRepository { + queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).SetAllowedColumns([]string{ + "id", "name", "is_active", "keycloak_id", "id_user", "id_permission", "email", "created_at", + }).SetAllowedTables([]string{TBL_USER}) + + queryBuilder.SetSecurityOptions(false, 100) + + return accessRepo{ + queryBuilder: queryBuilder, + db: dbService, + } +} + +func (r accessRepo) FindUserByKeycloakId(c context.Context, id string) ([]RoleUserModel, error) { + var result []RoleUserModel + + query := queryUtils.DynamicQuery{ + From: TBL_USER, + Fields: []queryUtils.SelectField{ + {Expression: "id"}, + {Expression: "keycloak_id"}, + }, + Filters: []queryUtils.FilterGroup{ + { + Filters: []queryUtils.DynamicFilter{ + {Column: "keycloak_id", Operator: queryUtils.OpEqual, Value: id}, + }, LogicOp: "AND", + }, + }, + } + + dbconn, err := r.db.GetSQLXDB(DB_NAME) + if err != nil { + log.Printf("Unable to connect db : %s", err) + return result, err + } + + err = r.queryBuilder.ExecuteQuery(c, dbconn, query, &result) + if err != nil { + log.Printf("Unable to execute query search user : %s", err) + return result, err + } + + return result, err +} + +func (r accessRepo) CreateUserPermission(c context.Context, req SyncKeycloakRoleRequest) error { + db, err := r.db.GetSQLXDB(DB_NAME) + if err != nil { + return err + } + + // START TRANSACTION + tx, err := db.BeginTx(c, nil) + if err != nil { + return err + } + + // insert user + userId, err := r.insertUser(c, tx, req) + if err != nil { + tx.Rollback() + return err + } + + err = r.getOrCreateUserPermission(c, db, tx, userId, req.Roles) + if err != nil { + tx.Rollback() + return err + } + + tx.Commit() + + return nil +} + +func (r accessRepo) UpdateUserPermission(c context.Context, userId string, req SyncKeycloakRoleRequest) error { + db, err := r.db.GetSQLXDB(DB_NAME) + if err != nil { + return err + } + + // START TRANSACTION + tx, err := db.BeginTx(c, nil) + if err != nil { + return err + } + + // delete all user permission + err = r.deletePermissionByUserId(c, tx, userId) + if err != nil { + tx.Rollback() + return err + } + + err = r.getOrCreateUserPermission(c, db, tx, userId, req.Roles) + if err != nil { + tx.Rollback() + return err + } + + tx.Commit() + + return nil +} + +// PRIVATE FUNCTIONS + +// Table user function + +func (r accessRepo) insertUser(c context.Context, tx *sql.Tx, req SyncKeycloakRoleRequest) (string, error) { + newUserId := uuid.New().String() + + insertUserQuery := queryUtils.InsertData{ + Columns: []string{ + "id", "name", "email", "keycloak_id", "created_at", + }, + Values: []interface{}{ + newUserId, req.Username, req.Email, req.KeycloakId, time.Now(), + }, + } + + returningCols := []string{ + "id", + } + + sql, args, err := r.queryBuilder.BuildInsertQuery(TBL_USER, insertUserQuery, returningCols...) + if err != nil { + log.Printf("error building query create user %s", err) + return newUserId, err + } + + _, err = tx.ExecContext(c, sql, args...) + if err != nil { + log.Printf("error executing query create user %s", err) + return newUserId, err + } + log.Printf(sql, args) + log.Printf("success insert user") + + return newUserId, nil +} + +// End Table user function + +// Table permission functions + +func (r accessRepo) findPermissionByRoleName(c context.Context, dbconn *sqlx.DB, roles []string) ([]RolePermissionModel, error) { + var result []RolePermissionModel + + query := queryUtils.DynamicQuery{ + From: TBL_PERMISSION, + Fields: []queryUtils.SelectField{ + {Expression: "id"}, + {Expression: "name"}, + }, + Filters: []queryUtils.FilterGroup{ + { + Filters: []queryUtils.DynamicFilter{ + {Column: "name", Operator: queryUtils.OpIn, Value: roles}, + }, LogicOp: "AND", + }, + }, + } + err := r.queryBuilder.ExecuteQuery(c, dbconn, query, &result) + if err != nil { + return result, err + } + + return result, nil +} + +func (r accessRepo) insertPermissionByRoleName(c context.Context, tx *sql.Tx, newRoles []string) ([]string, error) { + var idRoles []string + + if len(newRoles) > 0 { + var valuePermission [][]interface{} + + for _, role := range newRoles { + id := uuid.New().String() + itemValues := []interface{}{id, role, false} + + idRoles = append(idRoles, id) + valuePermission = append(valuePermission, itemValues) + } + + insertPermissionQuery := queryUtils.InsertBulkData{ + Columns: []string{ + "id", "name", "is_active", + }, + Values: valuePermission, + } + + returningCols := []string{ + "id", + } + sql, args, err := r.queryBuilder.BuildBulkInsertQuery(TBL_PERMISSION, insertPermissionQuery, returningCols...) + if err != nil { + log.Printf("error building query insert new permission %s", err) + + return idRoles, err + } + _, err = tx.ExecContext(c, sql, args...) + if err != nil { + log.Println(err) + return idRoles, err + } + log.Printf(sql, args) + log.Printf("success insert new permission") + } + + return idRoles, nil +} + +// END table permission functions + +// Table user permission functions + +func (r accessRepo) deletePermissionByUserId(c context.Context, tx *sql.Tx, userId string) error { + filters := []queryUtils.FilterGroup{ + { + Filters: []queryUtils.DynamicFilter{ + {Column: "id_user", Operator: queryUtils.OpEqual, Value: userId}, + }, + }, + } + sql, args, err := r.queryBuilder.BuildDeleteQuery(TBL_USER_PERMISSION, filters) + if err != nil { + log.Printf("Unable to create delete user permission query : %v", err) + return err + } + _, err = tx.ExecContext(c, sql, args...) + if err != nil { + log.Printf("Unable to executing delete user permission : %v", err) + return err + } + return nil +} + +func (r accessRepo) insertUserPermission(c context.Context, tx *sql.Tx, userId string, roleIds []string) error { + if len(roleIds) > 0 { + var valueUserPermission [][]interface{} + + for _, roleId := range roleIds { + id := uuid.New().String() + itemValues := []interface{}{id, userId, roleId} + + valueUserPermission = append(valueUserPermission, itemValues) + } + + insertUserPermissionQuery := queryUtils.InsertBulkData{ + Columns: []string{ + "id", "id_user", "id_permission", + }, + Values: valueUserPermission, + } + + returningCols := []string{ + "id", + } + sql, args, err := r.queryBuilder.BuildBulkInsertQuery(TBL_USER_PERMISSION, insertUserPermissionQuery, returningCols...) + if err != nil { + log.Printf("error building query insert user permission %s", err) + + return err + } + _, err = tx.ExecContext(c, sql, args...) + if err != nil { + log.Println(err) + return err + } + log.Printf(sql, args) + log.Printf("success insert user permission") + } + + return nil +} + +func (r accessRepo) getOrCreateUserPermission(c context.Context, db *sqlx.DB, tx *sql.Tx, userId string, roles []string) error { + var permissionIds []string + mapPermissionExist := make(map[string]string) + var newRoles []string + + // fetch permission by name, append the permission ids + fetchRoles, err := r.findPermissionByRoleName(c, db, roles) + if err != nil { + return err + } + + if len(fetchRoles) > 0 { + for _, item := range fetchRoles { + permissionIds = append(permissionIds, item.ID) + mapPermissionExist[item.Name] = item.ID + } + } + + // if permission not exist, insert non-existed permission first then append the permission ids + for _, role := range roles { + if mapPermissionExist[role] == "" { + newRoles = append(newRoles, role) + } + } + // insert new , non existed permission + if len(newRoles) > 0 { + newRoleIds, err := r.insertPermissionByRoleName(c, tx, newRoles) + if err != nil { + return err + } else { // append new role ids to permission ids + permissionIds = append(permissionIds, newRoleIds...) + } + } + // insert all permission to user + err = r.insertUserPermission(c, tx, userId, permissionIds) + if err != nil { + return err + } + + return nil +} + +// End Table user permission functions diff --git a/internal/domain/access/request.go b/internal/domain/access/request.go new file mode 100644 index 0000000..b1c3539 --- /dev/null +++ b/internal/domain/access/request.go @@ -0,0 +1,14 @@ +package access + +type SyncKeycloakRoleRequest struct { + KeycloakId string `json:"keycloak_id" binding:"required"` + Username string `json:"name" binding:"required"` + Email string `json:"email" binding:"required"` + Roles []string `json:"client_role"` +} + +type UpsertAccessPermissionRequest struct { + Name string `json:"namaHakAkses"` + Status string `json:"status"` + Pages []string `json:"pages"` +} diff --git a/internal/domain/access/routes.go b/internal/domain/access/routes.go new file mode 100644 index 0000000..db41028 --- /dev/null +++ b/internal/domain/access/routes.go @@ -0,0 +1,14 @@ +package access + +import ( + "antrian-operasi/internal/database" + + "github.com/gin-gonic/gin" +) + +func RegisterRoutes(r *gin.RouterGroup, dbService database.Service) { + accessRepo := NewRepository(dbService) + accessHandler := NewAccessHandler(accessRepo) + + r.POST("/sync-keycloak-role", accessHandler.SyncKeycloakRole) +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 921b6d4..918e40f 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -3,6 +3,7 @@ package routes import ( "antrian-operasi/internal/config" "antrian-operasi/internal/database" + "antrian-operasi/internal/domain/access" antrianoperasi "antrian-operasi/internal/domain/antrian_operasi" "antrian-operasi/internal/domain/dashboard" "antrian-operasi/internal/domain/keycloak" @@ -43,6 +44,10 @@ func RegisterRoutes(cfg *config.Config, dbService database.Service) *gin.Engine api := router.Group("/api") + acc := api.Group("/access", authKeycloak) + { + access.RegisterRoutes(acc, dbService) + } antrian := api.Group("/antrian-operasi", authKeycloak) { antrianoperasi.RegisterRoutes(antrian, dbService)