diff --git a/api/internal/config/config.go b/api/internal/config/config.go index 38c28874..055488d2 100644 --- a/api/internal/config/config.go +++ b/api/internal/config/config.go @@ -58,8 +58,9 @@ type DatabaseConfig struct { // MCIAMConfig MC-IAM 설정 type MCIAMConfig struct { - Use bool - TicketUse bool + Use bool + TicketUse bool + UseRegistryURL bool } // Load 설정 로드 @@ -82,8 +83,9 @@ func Load() (*Config, error) { SSLMode: getEnv("MC_WEB_CONSOLE_POSTGRES_SSLMODE", "disable"), }, MCIAM: MCIAMConfig{ - Use: getEnv("MC_WEB_CONSOLE_USE_IAM", "false") == "true", - TicketUse: getEnv("MC_WEB_CONSOLE_USE_TICKET_VALID", "false") == "true", + Use: getEnv("MC_WEB_CONSOLE_USE_IAM", "false") == "true", + TicketUse: getEnv("MC_WEB_CONSOLE_USE_TICKET_VALID", "false") == "true", + UseRegistryURL: getEnv("MC_WEB_CONSOLE_USE_REGISTRY_URL", "true") == "true", }, SetupYaml: SetupYamlConfig{ McWebconsoleMenuYaml: getEnv("MC_WEB_CONSOLE_MENUYAML", ""), diff --git a/api/internal/handler/proxy.go b/api/internal/handler/proxy.go index cf9e4e1d..a06bdec9 100644 --- a/api/internal/handler/proxy.go +++ b/api/internal/handler/proxy.go @@ -86,9 +86,11 @@ func SubsystemAnyController(c echo.Context) error { // ActionSpec: 캐시 우선 → 없으면 api.yaml ActionSpec effectiveActionSpec := actionSpec if cfg.RegistryCache != nil { - if dynamicURL := cfg.RegistryCache.GetBaseURL(subsystemName, operationId); dynamicURL != "" { - log.Printf("[RegistryCache] BaseURL override for %s: %s", subsystemName, dynamicURL) - effectiveBaseURL = dynamicURL + if cfg.MCIAM.UseRegistryURL { + if dynamicURL := cfg.RegistryCache.GetBaseURL(subsystemName, operationId); dynamicURL != "" { + log.Printf("[RegistryCache] BaseURL override for %s: %s", subsystemName, dynamicURL) + effectiveBaseURL = dynamicURL + } } if cachedSpec := cfg.RegistryCache.GetActionSpec(subsystemName, operationId); cachedSpec != nil { effectiveActionSpec = cachedSpec @@ -271,18 +273,30 @@ func encryptCredentialBody(plainBody []byte, baseURL string, service *config.Ser return nil, fmt.Errorf("empty publicKey from mc-infra-manager") } - // 2. RSA 공개키 파싱 + // 2. RSA 공개키 파싱 (cb-tumblebug은 PKCS#1 형식 "BEGIN RSA PUBLIC KEY" 반환) block, _ := pem.Decode([]byte(pkData.PublicKey)) if block == nil { return nil, fmt.Errorf("failed to decode PEM publicKey") } - pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("parse RSA publicKey: %w", err) - } - rsaPub, ok := pubInterface.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("publicKey is not RSA") + var rsaPub *rsa.PublicKey + switch block.Type { + case "RSA PUBLIC KEY": + // PKCS#1 형식 + rsaPub, err = x509.ParsePKCS1PublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse PKCS1 RSA publicKey: %w", err) + } + default: + // PKIX/SPKI 형식 (BEGIN PUBLIC KEY) + pubInterface, pkixErr := x509.ParsePKIXPublicKey(block.Bytes) + if pkixErr != nil { + return nil, fmt.Errorf("parse PKIX RSA publicKey: %w", pkixErr) + } + var ok bool + rsaPub, ok = pubInterface.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("publicKey is not RSA") + } } // 3. 평문 요청 파싱 @@ -297,10 +311,10 @@ func encryptCredentialBody(plainBody []byte, baseURL string, service *config.Ser return nil, fmt.Errorf("generate AES key: %w", err) } - // 5. credentialKeyValueList[].value AES-256-GCM 암호화 + // 5. credentialKeyValueList[].value AES-256-CBC 암호화 (cb-tumblebug 호환) encryptedKVList := make([]credentialKV, len(plain.CredentialKeyValueList)) for i, kv := range plain.CredentialKeyValueList { - cipherText, err := aesGCMEncrypt(aesKey, []byte(kv.Value)) + cipherText, err := aesCBCEncrypt(aesKey, []byte(kv.Value)) if err != nil { return nil, fmt.Errorf("encrypt credential value [%s]: %w", kv.Key, err) } @@ -327,22 +341,23 @@ func encryptCredentialBody(plainBody []byte, baseURL string, service *config.Ser return json.Marshal(encrypted) } -// aesGCMEncrypt AES-256-GCM 암호화. 반환값: nonce(12B) + ciphertext -func aesGCMEncrypt(key, plaintext []byte) ([]byte, error) { +// aesCBCEncrypt AES-256-CBC 암호화 (cb-tumblebug 호환) +// 반환값: IV(16B) + ciphertext (PKCS7 패딩, 블록 배수) +func aesCBCEncrypt(key, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - nonce := make([]byte, gcm.NonceSize()) - if _, err := rand.Read(nonce); err != nil { + blockSize := block.BlockSize() + padding := blockSize - len(plaintext)%blockSize + padded := append(plaintext, bytes.Repeat([]byte{byte(padding)}, padding)...) + iv := make([]byte, blockSize) + if _, err := rand.Read(iv); err != nil { return nil, err } - ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) - return ciphertext, nil + ciphertext := make([]byte, len(padded)) + cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, padded) + return append(iv, ciphertext...), nil } // buildAuthHeader api.yaml의 auth 타입에 따라 Authorization 헤더 값 반환 diff --git a/conf/api.yaml b/conf/api.yaml index 70cb5f1f..c5713f5a 100644 --- a/conf/api.yaml +++ b/conf/api.yaml @@ -4,19 +4,19 @@ services: mc-infra-connector: version: 0.9.4 - baseurl: http://spider_url:1024/spider + baseurl: http://mciam.onecloudcon.com:1024/spider auth: type: basic username: password: mc-iam-manager: version: 0.2.11 - baseurl: http://mciam.onecloudcon.com:5006 + baseurl: http://mciam.onecloudcon.com:5000 auth: type: bearer mc-infra-manager: version: 0.12.9 - baseurl: http://52.79.163.111:1323/tumblebug + baseurl: http://mciam.onecloudcon.com:1323/tumblebug auth: type: basic username: default @@ -28,39 +28,39 @@ services: type: bearer mc-observability: version: main - baseurl: http://observability_url:18080 + baseurl: http://mciam.onecloudcon.com:18080 auth: mc-application-manager: version: main - baseurl: http://localhost:18084 + baseurl: http://mciam.onecloudcon.com:18084 auth: mc-application-manager-fe: version: main - baseurl: http://application_manager_fe_url:18084 + baseurl: http://mciam.onecloudcon.com:18084 auth: mc-workflow-manager: version: main - baseurl: http://workflow_url:18083 + baseurl: http://mciam.onecloudcon.com:18083 auth: mc-workflow-manager-fe: version: main - baseurl: http://workflow_manager_fe_url:18083 + baseurl: http://mciam.onecloudcon.com:18083 auth: mc-cost-optimizer: version: main - baseurl: http://cost_optimizer_url:18082 + baseurl: http://mciam.onecloudcon.com:18082 auth: null mc-cost-optimizer-fe: version: main - baseurl: http://cost_optimizer_fe_url:7780 + baseurl: http://mciam.onecloudcon.com:7780 auth: null mc-data-manager: version: main:20240923 - baseurl: http://localhost:3300 + baseurl: http://mciam.onecloudcon.com:3300 auth: mc-data-manager-fe: version: main - baseurl: http://data_manager_fe_url:3300 + baseurl: http://mciam.onecloudcon.com:3300 auth: serviceActions: mc-infra-connector: @@ -879,11 +879,11 @@ serviceActions: description: 해당 프레임워크 역할에 할당된 메뉴 리스트를 반환합니다. Deleteuser: method: delete - resourcePath: /api/user/id/{userid} + resourcePath: /api/users/id/{userId} description: 사용자를 삭제합니다. Updateuser: method: put - resourcePath: /api/user/id/{userid} + resourcePath: /api/users/id/{userId} description: 사용자 정보를 업데이트 합니다. Createworkspace: method: post @@ -1058,6 +1058,10 @@ serviceActions: method: post resourcePath: /api/roles/assign/platform-role description: Platform Role 할당 + removePlatformRole: + method: delete + resourcePath: /api/roles/unassign/platform-role + description: 사용자에게서 Platform Role 제거 UpdateUserStatus: method: post resourcePath: /api/users/id/{userId}/status @@ -1110,6 +1114,14 @@ serviceActions: method: delete resourcePath: /api/users/id/{userId}/organizations/{organizationId} description: 사용자 그룹 제거 + assignGroupPlatformRole: + method: post + resourcePath: /api/groups/id/{groupId}/platform-roles + description: 그룹에 Platform Role 할당 + getGroupPlatformRoles: + method: get + resourcePath: /api/groups/id/{groupId}/platform-roles + description: 그룹의 Platform Role 목록 조회 listCspAccounts: method: post resourcePath: /api/csp-accounts/list diff --git a/conf/webconsole_menu_resources.yaml b/conf/webconsole_menu_resources.yaml index 269d700e..c32bef97 100644 --- a/conf/webconsole_menu_resources.yaml +++ b/conf/webconsole_menu_resources.yaml @@ -40,6 +40,14 @@ menus: priority: 2 menunumber: 1220 + - id: groups + parentid: organizations + displayname: Groups + restype: menu + isaction: true + priority: 2 + menunumber: 1225 + - id: approvals parentid: organizations displayname: Approvals @@ -88,11 +96,11 @@ menus: priority: 2 menunumber: 1310 - - id: regions + - id: credentials parentid: cloudsps - displayname: Regions + displayname: Credentials restype: menu - isaction: false + isaction: true priority: 2 menunumber: 1320 @@ -100,25 +108,9 @@ menus: parentid: cloudsps displayname: Connections restype: menu - isaction: false - priority: 2 - menunumber: 1330 - - - id: clouddrivers - parentid: cloudsps - displayname: Cloud Drivers - restype: menu isaction: true priority: 2 - menunumber: 1340 - - - id: credentials - parentid: cloudsps - displayname: Credentials - restype: menu - isaction: false - priority: 2 - menunumber: 1350 + menunumber: 1330 - id: cspaccounts parentid: cloudsps diff --git a/front/actions/env.go b/front/actions/env.go index 3e3794b4..1200f45b 100644 --- a/front/actions/env.go +++ b/front/actions/env.go @@ -7,6 +7,7 @@ import ( var FRONT_ADDR string var FRONT_PORT string +var API_SCHEME string var API_ADDR string var API_PORT string var SESSION_SECRET string @@ -15,6 +16,7 @@ func init() { // Get environment variables with defaults FRONT_ADDR = getEnvOrDefault("MC_WEB_CONSOLE_FRONT_ADDR", "0.0.0.0") FRONT_PORT = getEnvOrDefault("MC_WEB_CONSOLE_FRONT_PORT", "3001") + API_SCHEME = getEnvOrDefault("MC_WEB_CONSOLE_API_SCHEME", "http") API_ADDR = getEnvOrDefault("MC_WEB_CONSOLE_API_ADDR", "localhost") API_PORT = getEnvOrDefault("MC_WEB_CONSOLE_API_PORT", "3000") SESSION_SECRET = getEnvOrDefault("MC_WEB_CONSOLE_SESSION_SECRET", "mc-web-console-secret-key") diff --git a/front/actions/proxy.go b/front/actions/proxy.go index 10b6b755..f8a78ad8 100644 --- a/front/actions/proxy.go +++ b/front/actions/proxy.go @@ -11,7 +11,7 @@ var proxy *httputil.ReverseProxy func init() { var err error - ApiBaseHost, err = url.Parse("http://" + API_ADDR + ":" + API_PORT) + ApiBaseHost, err = url.Parse(API_SCHEME + "://" + API_ADDR + ":" + API_PORT) if err != nil { panic(err) } diff --git a/front/assets/js/common/api/services/cloudconnection_api.js b/front/assets/js/common/api/services/cloudconnection_api.js new file mode 100644 index 00000000..cdc8d36d --- /dev/null +++ b/front/assets/js/common/api/services/cloudconnection_api.js @@ -0,0 +1,228 @@ +// Cloud Connection API 서비스 (mc-infra-manager / cb-tumblebug) +// Credential은 RSA+AES 암호화 방식으로 등록, Connection Config는 자동 생성됨 + +function unwrapResponse(response) { + if (!response) throw new Error('Invalid response from server'); + if (response.status === 204) return null; + if (!response.data) throw new Error('Invalid response from server'); + if (response.status >= 400) { + const msg = (response.data.status && response.data.status.message) + || response.data.message + || response.data.error + || 'Request failed'; + const err = new Error(msg); + err.response = response; + throw err; + } + return response.data.responseData; +} + +// ─── CloudOS ────────────────────────────────────────────────────────── + +export async function listCloudOS() { + const controller = "/api/mc-infra-manager/GetProviderList"; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, {}); + const data = unwrapResponse(response); + return (data && data.output) ? data.output : []; +} + +// ─── Credential ─────────────────────────────────────────────────────── + +/** + * Credential 등록 Step 1: RSA 공개키 발급 + * @returns {{ tokenId: string, publicKey: string }} + */ +export async function getPublicKeyForCredential() { + const controller = "/api/mc-infra-manager/GetPublicKeyForCredentialEncryption"; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, {}); + return unwrapResponse(response); +} + +/** + * Credential 등록 Step 3: 암호화된 payload 전송 + * @param {object} payload - { providerName, credentialHolder, publicKeyTokenId, encryptedClientAesKeyByPublicKey, credentialKeyValueList } + * @returns {object} CredentialInfo (자동 생성된 ConnectionConfig 목록 포함) + */ +export async function registerCredential(payload, credentialHolder) { + const controller = "/api/mc-infra-manager/RegisterCredential"; + const data = { + request: payload, + headers: { 'X-Credential-Holder': credentialHolder || 'admin' }, + }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + return unwrapResponse(response); +} + +/** + * Credential Holder 목록 조회 + * GetCredentialHolderList 우선 호출, 404(미배포) 시 connConfig 집계로 fallback + * @returns {Array} [{ credentialHolder, providers, connectionCount, verifiedConnectionCount, isDefault }] + */ +export async function listCredentialHolders() { + try { + const controller = "/api/mc-infra-manager/GetCredentialHolderList"; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, {}); + const data = unwrapResponse(response); + if (data && data.credentialHolderList) return data.credentialHolderList; + } catch (e) { + // 404 등 미배포 환경: connConfig 집계로 fallback + console.warn('[listCredentialHolders] GetCredentialHolderList 미지원, connConfig 집계로 대체:', e.message); + } + + // fallback: connConfig 목록에서 holder 집계 + const conns = await listConnConfigs({ filterVerified: false }); + if (!conns.length) return []; + const holderMap = {}; + for (const conn of conns) { + const holder = conn.credentialHolder || 'admin'; + if (!holderMap[holder]) { + holderMap[holder] = { credentialHolder: holder, providers: new Set(), connectionCount: 0, verifiedConnectionCount: 0 }; + } + holderMap[holder].providers.add((conn.providerName || '').toLowerCase()); + holderMap[holder].connectionCount++; + if (conn.verified) holderMap[holder].verifiedConnectionCount++; + } + return Object.values(holderMap).map(h => ({ + credentialHolder: h.credentialHolder, + providers: [...h.providers], + connectionCount: h.connectionCount, + verifiedConnectionCount: h.verifiedConnectionCount, + isDefault: h.credentialHolder === 'admin', + })); +} + +/** + * Credential Holder 상세 조회 + * @param {string} holderId + * @returns {object} CredentialHolderInfo + */ +export async function getCredentialHolder(holderId) { + const controller = "/api/mc-infra-manager/GetCredentialHolder"; + const data = { pathParams: { holderId } }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + return unwrapResponse(response); +} + +// ─── Connection Config ───────────────────────────────────────────────── + +/** + * Connection Config 목록 조회 + * @param {object} filters - { filterCredentialHolder, filterVerified, filterRegionRepresentative } + * @returns {Array} ConnConfig[] + */ +export async function listConnConfigs(filters = {}, credentialHolder) { + const controller = "/api/mc-infra-manager/GetConnConfigList"; + const queryParams = {}; + if (filters.filterCredentialHolder) queryParams.filterCredentialHolder = filters.filterCredentialHolder; + if (filters.filterVerified !== undefined) queryParams.filterVerified = String(filters.filterVerified); + if (filters.filterRegionRepresentative !== undefined) queryParams.filterRegionRepresentative = String(filters.filterRegionRepresentative); + const data = { + queryParams, + headers: { 'X-Credential-Holder': credentialHolder || 'admin' }, + }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + const result = unwrapResponse(response); + return (result && (result.connConfig || result.connectionconfig)) + ? (result.connConfig || result.connectionconfig) + : []; +} + +/** + * Connection Config 상세 조회 + * @param {string} connConfigName + * @returns {object} ConnConfig + */ +export async function getConnConfig(connConfigName, credentialHolder) { + const controller = "/api/mc-infra-manager/GetConnConfig"; + const data = { + pathParams: { connConfigName }, + headers: { 'X-Credential-Holder': credentialHolder || 'admin' }, + }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + return unwrapResponse(response); +} + +// ─── 암호화 유틸 ─────────────────────────────────────────────────────── + +/** + * 평문 credential 값을 RSA 공개키로 암호화하여 RegisterCredential payload 생성 + * Web Crypto API(SubtleCrypto) 사용 — 브라우저 환경에서만 동작 + * + * @param {string} providerName - CSP 이름 (소문자, e.g. "aws") + * @param {string} credentialHolder - Holder 이름 (e.g. "admin") + * @param {string} tokenId - getPublicKeyForCredential() 응답의 tokenId + * @param {string} publicKeyPem - PEM 형식 RSA 공개키 + * @param {Array} keyValueList - [{ key: "ClientId", value: "AKIA..." }, ...] + * @returns {object} RegisterCredential API body + */ +export async function buildEncryptedCredentialPayload(providerName, credentialHolder, tokenId, publicKeyPem, keyValueList) { + // 1. AES-256 키 생성 + const aesKey = await crypto.subtle.generateKey( + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); + const rawAesKey = await crypto.subtle.exportKey('raw', aesKey); + + // 2. 각 credential 값을 AES-CBC로 암호화 + const credentialKeyValueList = await Promise.all(keyValueList.map(async ({ key, value }) => { + const iv = crypto.getRandomValues(new Uint8Array(16)); + const encoded = new TextEncoder().encode(value); + const encrypted = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, aesKey, encoded); + // IV(16) + ciphertext 를 Base64 + const combined = new Uint8Array(iv.byteLength + encrypted.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(encrypted), iv.byteLength); + return { key, encryptedValue: btoa(String.fromCharCode(...combined)) }; + })); + + // 3. PEM → CryptoKey (RSA-OAEP SHA-256) + // mc-infra-manager는 PKCS#1 형식(BEGIN RSA PUBLIC KEY)을 반환함. + // Web Crypto API는 SPKI 형식만 지원하므로 PKCS#1 → SPKI 변환 필요. + const pemBody = publicKeyPem + .replace(/-----[^-]+-----/g, '') + .replace(/\s/g, ''); + const pkcs1Der = Uint8Array.from(atob(pemBody), c => c.charCodeAt(0)); + + function derLenBytes(n) { + if (n < 0x80) return [n]; + if (n < 0x100) return [0x81, n]; + return [0x82, (n >> 8) & 0xff, n & 0xff]; + } + + let spkiDer; + if (publicKeyPem.includes('BEGIN RSA PUBLIC KEY')) { + // PKCS#1 → SPKI(SubjectPublicKeyInfo) 래핑 + const algIdSeq = new Uint8Array([ + 0x30, 0x0d, + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, + 0x05, 0x00, + ]); + const bsPayload = new Uint8Array([0x00, ...pkcs1Der]); + const bitString = new Uint8Array([0x03, ...derLenBytes(bsPayload.length), ...bsPayload]); + const seqInner = new Uint8Array([...algIdSeq, ...bitString]); + spkiDer = new Uint8Array([0x30, ...derLenBytes(seqInner.length), ...seqInner]); + } else { + spkiDer = pkcs1Der; + } + + const rsaKey = await crypto.subtle.importKey( + 'spki', + spkiDer, + { name: 'RSA-OAEP', hash: 'SHA-256' }, + false, + ['encrypt'] + ); + + // 4. AES 키를 RSA-OAEP로 암호화 + const encryptedAesKey = await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, rsaKey, rawAesKey); + const encryptedClientAesKeyByPublicKey = btoa(String.fromCharCode(...new Uint8Array(encryptedAesKey))); + + return { + providerName, + credentialHolder, + publicKeyTokenId: tokenId, + encryptedClientAesKeyByPublicKey, + credentialKeyValueList, + }; +} diff --git a/front/assets/js/common/api/services/menus_api.js b/front/assets/js/common/api/services/menus_api.js index 7209de69..3b4a56e7 100644 --- a/front/assets/js/common/api/services/menus_api.js +++ b/front/assets/js/common/api/services/menus_api.js @@ -41,3 +41,45 @@ export async function listRoles() { const response = await webconsolejs["common/api/http"].commonAPIPost(controller, {}); return response.data.responseData; } + +// 역할 메뉴 권한 변경 후 사이드바를 즉시 갱신하기 위해 호출. +// GetAllAvailableMenus를 재조회하여 localStorage를 갱신하고 +// refresh-sidebar 이벤트를 dispatch하면 sidebar.js가 updatemenu()를 재호출한다. +export async function refreshAvailableMenus() { + const response = await webconsolejs["common/api/http"].commonAPIPost( + "/api/mc-iam-manager/GetAllAvailableMenus" + ); + const menuList = response?.data?.responseData; + if (!menuList) return; + + const menuMap = new Map(); + menuList.forEach(menu => menuMap.set(menu.id, { ...menu, menus: [] })); + const rootMenus = []; + menuList.forEach(menu => { + const node = menuMap.get(menu.id); + if (menu.parentId === 'home' || !menu.parentId) { + rootMenus.push(node); + } else { + const parent = menuMap.get(menu.parentId); + if (parent) { + parent.menus.push(node); + } else { + // 부모가 응답에 없는 orphaned 노드 → 루트로 처리 + rootMenus.push(node); + } + } + }); + const sortMenus = (menus) => { + menus.sort((a, b) => + a.priority !== b.priority ? a.priority - b.priority : a.menuNumber - b.menuNumber + ); + menus.forEach(m => { if (m.menus?.length > 0) sortMenus(m.menus); }); + }; + sortMenus(rootMenus); + + // 응답 자체가 빈 배열이면 기존 사이드바 유지 (실수로 localStorage 소거 방지) + if (menuList.length === 0) return; + + webconsolejs["common/storage/localstorage"].setMenuLocalStorage(rootMenus); + document.dispatchEvent(new CustomEvent('refresh-sidebar')); +} diff --git a/front/assets/js/common/api/services/users_api.js b/front/assets/js/common/api/services/users_api.js index 367c885c..99a4073d 100644 --- a/front/assets/js/common/api/services/users_api.js +++ b/front/assets/js/common/api/services/users_api.js @@ -62,8 +62,6 @@ export async function updateUserStatus(userId, status) { export async function getUserWorkspacesByUserID(userId) { const controller = "/api/mc-iam-manager/Getuserworkspacesbyuserid"; - // var controller = "/api/" + "/mc-iam-manager/" + "Getuserworkspacesbyuserid"; - const data = { pathParams: { "userId": userId.toString() @@ -71,4 +69,75 @@ export async function getUserWorkspacesByUserID(userId) { } const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); return response.data.responseData; +} + +export async function updateUser(userId, userData) { + const controller = "/api/mc-iam-manager/Updateuser"; + const data = { + pathParams: { "userId": userId.toString() }, + request: userData + }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + return response; +} + +export async function deleteUser(userId) { + const controller = "/api/mc-iam-manager/Deleteuser"; + const data = { + pathParams: { "userId": userId.toString() } + }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + return response; +} + +function resolveWorkspaceId(workspaceId) { + if (workspaceId) { + return workspaceId.toString(); + } + + const workspaceApi = webconsolejs['common/api/services/workspace_api']; + const currentWorkspace = workspaceApi?.getCurrentWorkspace?.(); + const sessionWorkspaceId = currentWorkspace?.Id ?? currentWorkspace?.id; + return sessionWorkspaceId ? sessionWorkspaceId.toString() : '1'; +} + +export async function addUserRole(userId, roleData) { + const { roleType, roleId, workspaceId } = roleData; + + if (!roleType || !roleId) { + throw new Error('Role type and role are required.'); + } + + if (roleType === 'platform') { + return webconsolejs['common/api/services/roles_api'].assignUserToRole(roleId, userId); + } + + if (roleType === 'workspace') { + return webconsolejs['common/api/services/workspace_api'].createWorkspaceUserRoleMappingByName( + resolveWorkspaceId(workspaceId), + roleId, + userId + ); + } + + if (roleType === 'csp') { + throw new Error( + 'Direct CSP role assignment to users is not supported. Configure CSP mappings on the role instead.' + ); + } + + throw new Error(`Unsupported role type: ${roleType}`); +} + +export async function removeUserRole(userId, roleId) { + const controller = "/api/mc-iam-manager/removePlatformRole"; + const data = { + request: { + userId: userId.toString(), + roleId: roleId.toString(), + roleType: "platform" + } + }; + const response = await webconsolejs["common/api/http"].commonAPIPost(controller, data); + return response; } \ No newline at end of file diff --git a/front/assets/js/pages/auth/login.js b/front/assets/js/pages/auth/login.js index ae5ec5cd..76a46132 100644 --- a/front/assets/js/pages/auth/login.js +++ b/front/assets/js/pages/auth/login.js @@ -76,15 +76,17 @@ function convertToMenuTree(menuList) { const rootMenus = []; menuList.forEach(menu => { const menuNode = menuMap.get(menu.id); - + if (menu.parentId === 'home' || !menu.parentId) { // 최상위 메뉴 rootMenus.push(menuNode); } else { - // 하위 메뉴 + // 하위 메뉴 — 부모가 응답에 없으면(orphaned) 루트로 처리 const parentMenu = menuMap.get(menu.parentId); if (parentMenu) { parentMenu.menus.push(menuNode); + } else { + rootMenus.push(menuNode); } } }); diff --git a/front/assets/js/pages/operation/workspace/roles.js b/front/assets/js/pages/operation/workspace/roles.js index a4af80b4..c81c9401 100644 --- a/front/assets/js/pages/operation/workspace/roles.js +++ b/front/assets/js/pages/operation/workspace/roles.js @@ -2819,6 +2819,9 @@ async function saveRole() { // 헤더 클릭 이벤트 리스너 재설정 (DOM 재생성 후 이벤트 연결) setupHeaderClickEvents(); + // 사이드바 메뉴 즉시 갱신 (역할 메뉴 권한 변경 반영) + await webconsolejs["common/api/services/menus_api"].refreshAvailableMenus(); + // 페이지를 맨 위로 스크롤 window.scrollTo({ top: 0, behavior: 'smooth' }); } @@ -3221,6 +3224,9 @@ async function updateRole() { // 헤더 클릭 이벤트 리스너 재설정 (DOM 재생성 후 이벤트 연결) setupHeaderClickEvents(); + // 사이드바 메뉴 즉시 갱신 (역할 메뉴 권한 변경 반영) + await webconsolejs["common/api/services/menus_api"].refreshAvailableMenus(); + // 페이지를 맨 위로 스크롤 window.scrollTo({ top: 0, behavior: 'smooth' }); } diff --git a/front/assets/js/pages/settings/accountnaccess/organizations/companyinfo.js b/front/assets/js/pages/settings/accountnaccess/organizations/companyinfo.js new file mode 100644 index 00000000..111f81be --- /dev/null +++ b/front/assets/js/pages/settings/accountnaccess/organizations/companyinfo.js @@ -0,0 +1,76 @@ +if (typeof webconsolejs === 'undefined') window.webconsolejs = {}; +if (typeof webconsolejs['pages/settings/accountnaccess/organizations/companyinfo'] === 'undefined') { + webconsolejs['pages/settings/accountnaccess/organizations/companyinfo'] = {}; +} + +const AppState = { + org: null +}; + +const groupsApi = () => webconsolejs['common/api/services/groups_api']; + +const companyInfoPage = { + async load() { + try { + const list = await groupsApi().getGroupList(); + const orgs = Array.isArray(list) ? list : []; + if (orgs.length === 0) throw new Error('No organization found'); + // 최상위 조직: level이 가장 낮은 항목 + const root = orgs.reduce((min, o) => (o.level < min.level ? o : min), orgs[0]); + AppState.org = root; + this._render(root); + } catch (e) { + console.error('Failed to load organization info:', e); + document.getElementById('companyinfo-loading').innerHTML = + '
Failed to load organization info.
'; + } + }, + + _render(org) { + document.getElementById('ci-name').textContent = org.name || '-'; + document.getElementById('ci-code').textContent = org.organizationCode || org.code || '-'; + document.getElementById('ci-level').textContent = org.level ?? '-'; + document.getElementById('ci-path').textContent = org.path || '-'; + document.getElementById('ci-description').textContent = org.description || '-'; + document.getElementById('ci-user-count').textContent = org.userCount ?? '-'; + document.getElementById('companyinfo-loading').style.display = 'none'; + document.getElementById('companyinfo-data').style.display = ''; + }, + + enterEditMode() { + if (!AppState.org) return; + document.getElementById('ci-edit-name').value = AppState.org.name || ''; + document.getElementById('ci-edit-description').value = AppState.org.description || ''; + document.getElementById('companyinfo-view-card').style.display = 'none'; + document.getElementById('companyinfo-edit-card').style.display = ''; + }, + + cancelEdit() { + document.getElementById('companyinfo-edit-card').style.display = 'none'; + document.getElementById('companyinfo-view-card').style.display = ''; + }, + + async saveEdit() { + const name = document.getElementById('ci-edit-name').value.trim(); + if (!name) { + alert('Name is required.'); + return; + } + const description = document.getElementById('ci-edit-description').value.trim(); + try { + const updated = await groupsApi().updateGroup(AppState.org.id, { name, description }); + AppState.org = { ...AppState.org, name, description, ...updated }; + this._render(AppState.org); + this.cancelEdit(); + } catch (e) { + console.error('Failed to update organization:', e); + alert('Failed to save. Please try again.'); + } + } +}; + +window.companyInfoPage = companyInfoPage; + +document.addEventListener('DOMContentLoaded', () => { + companyInfoPage.load(); +}); diff --git a/front/assets/js/pages/settings/accountnaccess/organizations/menus.js b/front/assets/js/pages/settings/accountnaccess/organizations/menus.js index e67e29ea..1f930b68 100644 --- a/front/assets/js/pages/settings/accountnaccess/organizations/menus.js +++ b/front/assets/js/pages/settings/accountnaccess/organizations/menus.js @@ -71,6 +71,7 @@ const MenuManager = { AppState.menus.selectedMenu = null; UIManager.hideDetailPanel(); await this.loadTree(); + await webconsolejs["common/api/services/menus_api"].refreshAvailableMenus(); } catch (error) { console.error("Error creating menu:", error); alert("Failed to create menu: " + (error.message || error)); @@ -86,6 +87,7 @@ const MenuManager = { AppState.menus.selectedMenu = updated; UIManager.showDetailPanel(updated); } + await webconsolejs["common/api/services/menus_api"].refreshAvailableMenus(); } catch (error) { console.error("Error updating menu:", error); alert("Failed to update menu: " + (error.message || error)); @@ -98,6 +100,7 @@ const MenuManager = { AppState.menus.selectedMenu = null; UIManager.hideDetailPanel(); await this.loadTree(); + await webconsolejs["common/api/services/menus_api"].refreshAvailableMenus(); } catch (error) { console.error("Error deleting menu:", error); alert("Failed to delete menu: " + (error.message || error)); diff --git a/front/assets/js/pages/settings/accountnaccess/organizations/users.js b/front/assets/js/pages/settings/accountnaccess/organizations/users.js index 44008b35..a3ae77ae 100644 --- a/front/assets/js/pages/settings/accountnaccess/organizations/users.js +++ b/front/assets/js/pages/settings/accountnaccess/organizations/users.js @@ -120,24 +120,41 @@ const UserManager = { // 폼 데이터 수집 collectFormData() { - const email = document.getElementById('create-user-email').value; - return { - username: email, // email을 username으로 사용 - email: email, + const username = document.getElementById('create-user-username').value.trim(); + const email = document.getElementById('create-user-email').value.trim(); + // const password = document.getElementById('create-user-password').value; + const data = { + username, + email, firstName: document.getElementById('create-user-firstname').value, lastName: document.getElementById('create-user-lastname').value, enabled: document.getElementById('create-user-enabled').checked, emailVerified: false // 고정값 }; + // if (password && password.trim() !== '') { + // data.password = password.trim(); + // } + return data; }, // 유효성 검증 validateUserData(userData) { const errors = []; - - if (!userData.email || userData.email.trim() === '') { + + const username = (userData.username || '').trim(); + const email = (userData.email || '').trim(); + + if (!username) { + errors.push('User ID is required'); + } else if (username.includes('@')) { + errors.push('User ID must not be an email address'); + } else if (email && username === email) { + errors.push('User ID must differ from email address'); + } + + if (!email) { errors.push('Email is required'); - } else if (!this.isValidEmail(userData.email)) { + } else if (!this.isValidEmail(email)) { errors.push('Invalid email format'); } @@ -148,7 +165,11 @@ const UserManager = { if (!userData.lastName || userData.lastName.trim() === '') { errors.push('Last name is required'); } - + + // if (userData.password && userData.password.length < 8) { + // errors.push('Password must be at least 8 characters'); + // } + return errors; }, @@ -353,7 +374,7 @@ const UIManager = { descriptionsElement.innerHTML = '-'; actionsElement.innerHTML = `
- +
+ +
+
+
+
+ +
+ + + + + + + + + + +{{ partial("partials/layout/pageloader.html") | raw }} + +{{ javascriptTag("common/api/services/groups_api.js") | raw }} +{{ javascriptTag("pages/settings/accountnaccess/organizations/companyinfo.js") | raw }} diff --git a/front/templates/pages/settings/accountnaccess/organizations/users.html b/front/templates/pages/settings/accountnaccess/organizations/users.html index 1abead52..b8395a51 100644 --- a/front/templates/pages/settings/accountnaccess/organizations/users.html +++ b/front/templates/pages/settings/accountnaccess/organizations/users.html @@ -482,7 +482,7 @@

Platform Roles

+
+ + + 로그인 ID — email 과 별도 (예: abc / abc@abc.com) +
Create User required />
+
+ @@ -910,4 +925,8 @@ -{{ javascriptTag("common/api/services/users_api.js") | raw }} {{ javascriptTag("common/api/services/groups_api.js") | raw }} {{ javascriptTag("pages/settings/accountnaccess/organizations/users.js") | raw }} +{{ javascriptTag("common/api/services/users_api.js") | raw }} +{{ javascriptTag("common/api/services/groups_api.js") | raw }} +{{ javascriptTag("common/api/services/roles_api.js") | raw }} +{{ javascriptTag("common/api/services/workspace_api.js") | raw }} +{{ javascriptTag("pages/settings/accountnaccess/organizations/users.js") | raw }} diff --git a/front/templates/pages/settings/environment/cloudsps/clouddrivers.html b/front/templates/pages/settings/environment/cloudsps/clouddrivers.html index 83b9acf5..f2804120 100644 --- a/front/templates/pages/settings/environment/cloudsps/clouddrivers.html +++ b/front/templates/pages/settings/environment/cloudsps/clouddrivers.html @@ -106,7 +106,7 @@

Add Cloud Driver

- + diff --git a/front/templates/pages/settings/environment/cloudsps/connections.html b/front/templates/pages/settings/environment/cloudsps/connections.html index 31e0690a..f7611bdb 100644 --- a/front/templates/pages/settings/environment/cloudsps/connections.html +++ b/front/templates/pages/settings/environment/cloudsps/connections.html @@ -29,6 +29,14 @@

Connections

-
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
diff --git a/go.work.sum b/go.work.sum index 5e504d46..3f3bfd57 100644 --- a/go.work.sum +++ b/go.work.sum @@ -50,6 +50,7 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5il github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/go-co-op/gocron v1.28.1 h1:z2+Y094RkGriilTVdGq6IeNbTkZbGMTQSxK4zITogo0= @@ -110,6 +111,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= @@ -162,8 +164,10 @@ github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -178,6 +182,7 @@ github.com/markbates/refresh v1.12.0/go.mod h1:Vpwi1+q+2U1VxE7C0Ilj6r2/+TigRzQcL github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -192,10 +197,12 @@ github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDm github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= @@ -210,6 +217,7 @@ github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43Z github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= @@ -226,7 +234,9 @@ github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= @@ -277,6 +287,7 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -307,9 +318,11 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -321,6 +334,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= @@ -339,11 +353,13 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/scripts/start-remote.sh b/scripts/start-remote.sh new file mode 100755 index 00000000..30dd15d4 --- /dev/null +++ b/scripts/start-remote.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +LOGS_DIR="$SCRIPT_DIR/logs" +mkdir -p "$LOGS_DIR" + +API_PORT="${MC_WEB_CONSOLE_API_PORT:-3007}" +FRONT_PORT="${MC_WEB_CONSOLE_FRONT_PORT:-3017}" + +echo "[remote] API server : http://localhost:$API_PORT" +echo "[remote] Front server: http://0.0.0.0:$FRONT_PORT" +echo "[remote] Framework : mciam.onecloudcon.com" + +# webpack 빌드 (SKIP_BUILD=true 로 건너뛸 수 있음) +if [ "${SKIP_BUILD:-false}" != "true" ]; then + echo "[remote] Building frontend..." + cd "$ROOT_DIR/front" && npm run build +fi + +# API 서버 기동 (백그라운드) +cd "$ROOT_DIR/api" +MC_WEB_CONSOLE_API_ADDR=0.0.0.0 \ +MC_WEB_CONSOLE_API_PORT=$API_PORT \ +go run cmd/main.go > "$LOGS_DIR/api.log" 2>&1 & +echo $! > "$LOGS_DIR/api.pid" +echo "[remote] API server started (PID: $(cat $LOGS_DIR/api.pid)), log: $LOGS_DIR/api.log" +sleep 2 + +# Front 서버 기동 (포그라운드 — Ctrl+C 로 종료) +cd "$ROOT_DIR/front" +MC_WEB_CONSOLE_FRONT_PORT=$FRONT_PORT \ +MC_WEB_CONSOLE_FRONT_ADDR=0.0.0.0 \ +MC_WEB_CONSOLE_API_SCHEME=http \ +MC_WEB_CONSOLE_API_ADDR=localhost \ +MC_WEB_CONSOLE_API_PORT=$API_PORT \ +go run cmd/app/main.go