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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/api/configSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { client } from './client'

export interface ConfigBundle {
version: number
tenantId: string
permissions: Array<{ code: string; description: string | null }>
roles: Array<{ code: string; name: string; permissionCodes: string[] }>
menus: Array<{
code: string
parentCode: string | null
label: string
path: string | null
icon: string | null
requiredPermissionCode: string | null
displayOrder: number
}>
// Present only when exported with includeUsers=true. Never carries a password.
users?: Array<{ loginId: string; email: string | null; status: string; roleCodes: string[] }>
}

export interface ImportSection {
created: string[]
updated: string[]
deleted: string[]
skipped: string[]
}

export interface ImportResult {
dryRun: boolean
mode: string
permissions: ImportSection
roles: ImportSection
menus: ImportSection
users: ImportSection
}

const base = '/config'

export const configSyncApi = {
export(tenantId: string, includeUsers = false) {
return client
.get<ConfigBundle>(`${base}/export`, { params: { tenantId, includeUsers } })
.then((r) => r.data)
},
// `import` is awkward as a method name; the backend endpoint is POST /config/import.
apply(bundle: ConfigBundle, dryRun: boolean, mode = 'merge', includeUsers = false) {
return client
.post<ImportResult>(`${base}/import`, bundle, { params: { mode, dryRun, includeUsers } })
.then((r) => r.data)
},
}
65 changes: 65 additions & 0 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,71 @@ export default {
dashboard: 'Dashboard',
diagnostics: 'Diagnostics',
auditLogs: 'Audit Logs',
configSync: 'Config Sync',
},
configSync: {
title: 'Config Sync',
intro:
'Promote definitional config (permissions, roles, menus) from this environment to another. Export a code-keyed bundle here, then import it on the target. Requires devslab.kit.config-sync.enabled=true on both servers; it is refused under a production profile.',
counts: {
permissions: '{n} permissions',
roles: '{n} roles',
menus: '{n} menus',
users: '{n} users',
},
export: {
title: 'Export',
desc: "Snapshot this environment's permissions, roles and menus as a portable, code-keyed bundle.",
run: 'Export',
download: 'Download JSON',
copy: 'Copy',
includeUsers: 'Include users (no passwords)',
empty: 'No bundle yet — click Export to generate one.',
},
import: {
title: 'Import',
desc: 'Paste or upload a bundle, preview the changes (dry-run), then apply.',
placeholder: 'Paste a config bundle JSON here…',
pickFile: 'Choose file',
mode: 'Mode',
modes: {
merge: 'Merge (add & update)',
mirror: 'Mirror (delete extras)',
},
mirrorWarn:
'Mirror deletes target entities that are absent from the bundle. Always review the dry-run before applying. Requires a kit version that supports mirror.',
dryRun: 'Preview (dry-run)',
apply: 'Apply',
invalidJson: 'Invalid JSON — could not parse the bundle.',
needDryRun: 'Run a preview first; apply unlocks once the dry-run reflects the current bundle.',
includeUsers: 'Sync users',
usersHint:
'User sync creates missing users only — no passwords are carried (created users must have one set by an admin) and existing users are never overwritten.',
},
result: {
dryRunTitle: 'Preview (dry-run) — nothing was written',
appliedTitle: 'Applied',
created: 'Created',
updated: 'Updated',
deleted: 'Deleted',
skipped: 'Skipped',
none: 'None',
section: {
permissions: 'Permissions',
roles: 'Roles',
menus: 'Menus',
users: 'Users',
},
},
toasts: {
exported: 'Config exported',
exportFailed: 'Export failed',
dryRunDone: 'Preview ready',
applied: 'Config applied',
importFailed: 'Import failed',
copied: 'Copied to clipboard',
copyFailed: 'Copy failed',
},
},
login: {
tenant: 'Tenant',
Expand Down
65 changes: 65 additions & 0 deletions src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,71 @@ export default {
dashboard: '대시보드',
diagnostics: '진단',
auditLogs: '감사 로그',
configSync: '설정 동기화',
},
configSync: {
title: '설정 동기화',
intro:
'정의성 설정(권한·역할·메뉴)을 이 환경에서 다른 환경으로 배포합니다. 여기서 코드 기준 번들을 내보낸 뒤 대상 서버에서 가져오면 됩니다. 양쪽 모두 devslab.kit.config-sync.enabled=true 가 필요하며, 운영(prod) 프로파일에서는 거부됩니다.',
counts: {
permissions: '권한 {n}개',
roles: '역할 {n}개',
menus: '메뉴 {n}개',
users: '사용자 {n}명',
},
export: {
title: '내보내기',
desc: '이 환경의 권한·역할·메뉴를 코드 기준의 이식 가능한 번들로 스냅샷합니다.',
run: '내보내기',
download: 'JSON 다운로드',
copy: '복사',
includeUsers: '사용자 포함 (비밀번호 제외)',
empty: '아직 번들이 없습니다 — 내보내기를 눌러 생성하세요.',
},
import: {
title: '가져오기',
desc: '번들을 붙여넣거나 업로드하고, 변경 내용을 미리보기(dry-run)한 뒤 적용합니다.',
placeholder: '설정 번들 JSON 을 여기에 붙여넣으세요…',
pickFile: '파일 선택',
mode: '모드',
modes: {
merge: '병합 (추가·수정)',
mirror: '미러 (없는 항목 삭제)',
},
mirrorWarn:
'미러 모드는 번들에 없는 대상 항목을 삭제합니다. 적용 전 반드시 미리보기를 확인하세요. 미러를 지원하는 kit 버전이 필요합니다.',
dryRun: '미리보기 (dry-run)',
apply: '적용',
invalidJson: '잘못된 JSON — 번들을 파싱할 수 없습니다.',
needDryRun: '먼저 미리보기를 실행하세요. 미리보기가 현재 번들과 일치하면 적용이 활성화됩니다.',
includeUsers: '사용자 동기화',
usersHint:
'사용자 동기화는 없는 사용자만 생성합니다 — 비밀번호는 전송되지 않으며(생성된 사용자는 관리자가 비밀번호를 설정해야 함) 기존 사용자는 절대 덮어쓰지 않습니다.',
},
result: {
dryRunTitle: '미리보기 (dry-run) — 아무것도 기록되지 않았습니다',
appliedTitle: '적용 완료',
created: '생성',
updated: '수정',
deleted: '삭제',
skipped: '건너뜀',
none: '없음',
section: {
permissions: '권한',
roles: '역할',
menus: '메뉴',
users: '사용자',
},
},
toasts: {
exported: '설정을 내보냈습니다',
exportFailed: '내보내기 실패',
dryRunDone: '미리보기 준비됨',
applied: '설정을 적용했습니다',
importFailed: '가져오기 실패',
copied: '클립보드에 복사됨',
copyFailed: '복사 실패',
},
},
login: {
tenant: '테넌트',
Expand Down
1 change: 1 addition & 0 deletions src/layout/AppLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const navGroups = computed(() => [
{ label: t('nav.tenants'), icon: 'pi pi-building', route: 'tenants' },
{ label: t('nav.policies'), icon: 'pi pi-shield', route: 'policies' },
{ label: t('nav.settings'), icon: 'pi pi-cog', route: 'settings' },
{ label: t('nav.configSync'), icon: 'pi pi-sync', route: 'config-sync' },
],
},
{
Expand Down
5 changes: 5 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ const routes: RouteRecordRaw[] = [
name: 'settings',
component: () => import('@/views/SettingsView.vue'),
},
{
path: 'config-sync',
name: 'config-sync',
component: () => import('@/views/ConfigSyncView.vue'),
},
{
// Voluntary self-service password change, inside the layout. The same
// ChangePasswordView also serves the forced standalone route above; it
Expand Down
Loading
Loading