diff --git a/src/App.vue b/src/App.vue
index 04b74c2..4a038d1 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -2,22 +2,16 @@
-
-
member.ose.tw
-
-
-
@@ -52,20 +45,18 @@ const showNav = computed(() => {
const userTabs = [
{ to: '/me', label: '我的資料' },
- { to: '/me/permissions', label: '我的權限' }
+ { to: '/me/permissions', label: '我的角色' }
]
const adminTabs = [
- { to: '/admin/systems', label: '系統' },
- { to: '/admin/modules', label: '模組' },
{ to: '/admin/companies', label: '公司' },
{ to: '/admin/sites', label: '站台' },
+ { to: '/admin/systems', label: '系統' },
+ { to: '/admin/roles', label: '角色' },
{ to: '/admin/members', label: '會員' },
- { to: '/admin/permission-groups', label: '群組' },
{ to: '/admin/api-clients', label: 'API Clients' }
]
-// 行內 NavTab 元件:避免另開檔案
const NavTab = defineComponent({
props: { to: String },
setup(props, { slots }) {
diff --git a/src/api/companies.js b/src/api/companies.js
index a5d7268..5db9bd3 100644
--- a/src/api/companies.js
+++ b/src/api/companies.js
@@ -1,6 +1,6 @@
import { adminHttp } from './http'
-export const getCompanies = () => adminHttp.get('/admin/companies')
+export const getCompanies = (params) => adminHttp.get('/admin/companies', { params })
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
export const deleteCompany = (companyKey) => adminHttp.delete(`/admin/companies/${companyKey}`)
diff --git a/src/api/members.js b/src/api/members.js
index 06e426d..615853d 100644
--- a/src/api/members.js
+++ b/src/api/members.js
@@ -1,10 +1,10 @@
import { adminHttp } from './http'
-export const getMembers = () => adminHttp.get('/admin/members')
-export const upsertMember = (data) => adminHttp.post('/admin/members/upsert', data)
+export const getMembers = (params) => adminHttp.get('/admin/members', { params })
+export const createMember = (data) => adminHttp.post('/admin/members', data)
export const updateMember = (userSub, data) => adminHttp.patch(`/admin/members/${userSub}`, data)
-export const deleteMember = (userSub) => adminHttp.delete(`/admin/members/${userSub}`)
+export const deleteMember = (userSub, syncToIdp = true) => adminHttp.delete(`/admin/members/${userSub}`, { params: { sync_to_idp: syncToIdp } })
export const resetMemberPassword = (userSub) => adminHttp.post(`/admin/members/${userSub}/password/reset`)
-export const getMemberPermissionGroups = (userSub) => adminHttp.get(`/admin/members/${userSub}/permission-groups`)
-export const setMemberPermissionGroups = (userSub, groupKeys) =>
- adminHttp.put(`/admin/members/${userSub}/permission-groups`, { group_keys: groupKeys })
+export const getMemberSites = (userSub) => adminHttp.get(`/admin/members/${userSub}/sites`)
+export const setMemberSites = (userSub, siteKeys) => adminHttp.put(`/admin/members/${userSub}/sites`, { site_keys: siteKeys })
+export const getMemberRoles = (userSub) => adminHttp.get(`/admin/members/${userSub}/roles`)
diff --git a/src/api/modules.js b/src/api/modules.js
deleted file mode 100644
index 44ee34d..0000000
--- a/src/api/modules.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { adminHttp } from './http'
-
-export const getModules = () => adminHttp.get('/admin/modules')
-export const createModule = (data) => adminHttp.post('/admin/modules', data)
-export const updateModule = (moduleKey, data) => adminHttp.patch(`/admin/modules/${moduleKey}`, data)
-export const deleteModule = (moduleKey) => adminHttp.delete(`/admin/modules/${moduleKey}`)
-export const getModuleGroups = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/groups`)
-export const getModuleMembers = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/members`)
diff --git a/src/api/permission-admin.js b/src/api/permission-admin.js
deleted file mode 100644
index cc4d23f..0000000
--- a/src/api/permission-admin.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { adminHttp } from './http'
-
-export const grantPermission = (data) => adminHttp.post('/admin/permissions/grant', data)
-export const revokePermission = (data) => adminHttp.post('/admin/permissions/revoke', data)
-export const listDirectPermissions = (params) => adminHttp.get('/admin/permissions/direct', { params })
-export const revokeDirectPermissionById = (permissionId) => adminHttp.delete(`/admin/permissions/direct/${permissionId}`)
diff --git a/src/api/permission-groups.js b/src/api/permission-groups.js
deleted file mode 100644
index ecb075e..0000000
--- a/src/api/permission-groups.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { adminHttp } from './http'
-
-export const getPermissionGroups = () => adminHttp.get('/admin/permission-groups')
-export const createPermissionGroup = (data) => adminHttp.post('/admin/permission-groups', data)
-export const updatePermissionGroup = (groupKey, data) => adminHttp.patch(`/admin/permission-groups/${groupKey}`, data)
-export const deletePermissionGroup = (groupKey) => adminHttp.delete(`/admin/permission-groups/${groupKey}`)
-export const getPermissionGroupPermissions = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/permissions`)
-export const getPermissionGroupBindings = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/bindings`)
-export const updatePermissionGroupBindings = (groupKey, data) =>
- adminHttp.put(`/admin/permission-groups/${groupKey}/bindings`, data)
-
-export const addMemberToGroup = (groupKey, idpSub) =>
- adminHttp.post(`/admin/permission-groups/${groupKey}/members/${idpSub}`)
-
-export const removeMemberFromGroup = (groupKey, idpSub) =>
- adminHttp.delete(`/admin/permission-groups/${groupKey}/members/${idpSub}`)
-
-export const groupGrant = (groupKey, data) =>
- adminHttp.post(`/admin/permission-groups/${groupKey}/permissions/grant`, data)
-
-export const groupRevoke = (groupKey, data) =>
- adminHttp.post(`/admin/permission-groups/${groupKey}/permissions/revoke`, data)
diff --git a/src/api/roles.js b/src/api/roles.js
new file mode 100644
index 0000000..a4c713c
--- /dev/null
+++ b/src/api/roles.js
@@ -0,0 +1,7 @@
+import { adminHttp } from './http'
+
+export const getRoles = (params) => adminHttp.get('/admin/roles', { params })
+export const createRole = (data) => adminHttp.post('/admin/roles', data)
+export const updateRole = (roleKey, data) => adminHttp.patch(`/admin/roles/${roleKey}`, data)
+export const deleteRole = (roleKey) => adminHttp.delete(`/admin/roles/${roleKey}`)
+export const getRoleSites = (roleKey) => adminHttp.get(`/admin/roles/${roleKey}/sites`)
diff --git a/src/api/sites.js b/src/api/sites.js
index 9556165..cda7309 100644
--- a/src/api/sites.js
+++ b/src/api/sites.js
@@ -1,6 +1,9 @@
import { adminHttp } from './http'
-export const getSites = () => adminHttp.get('/admin/sites')
+export const getSites = (params) => adminHttp.get('/admin/sites', { params })
export const createSite = (data) => adminHttp.post('/admin/sites', data)
export const updateSite = (siteKey, data) => adminHttp.patch(`/admin/sites/${siteKey}`, data)
export const deleteSite = (siteKey) => adminHttp.delete(`/admin/sites/${siteKey}`)
+export const getSiteRoles = (siteKey) => adminHttp.get(`/admin/sites/${siteKey}/roles`)
+export const setSiteRoles = (siteKey, roleKeys) => adminHttp.put(`/admin/sites/${siteKey}/roles`, { role_keys: roleKeys })
+export const getSiteMembers = (siteKey) => adminHttp.get(`/admin/sites/${siteKey}/members`)
diff --git a/src/api/systems.js b/src/api/systems.js
index 486a96f..653a561 100644
--- a/src/api/systems.js
+++ b/src/api/systems.js
@@ -1,8 +1,7 @@
import { adminHttp } from './http'
-export const getSystems = () => adminHttp.get('/admin/systems')
+export const getSystems = (params) => adminHttp.get('/admin/systems', { params })
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
export const deleteSystem = (systemKey) => adminHttp.delete(`/admin/systems/${systemKey}`)
-export const getSystemGroups = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/groups`)
-export const getSystemMembers = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/members`)
+export const getSystemRoles = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/roles`)
diff --git a/src/pages/AuthCallbackPage.vue b/src/pages/AuthCallbackPage.vue
index ca07e7c..7a10c8b 100644
--- a/src/pages/AuthCallbackPage.vue
+++ b/src/pages/AuthCallbackPage.vue
@@ -78,9 +78,9 @@ onMounted(async () => {
// 導向原頁面或預設的 /me
sessionStorage.removeItem('oidc_expected_state')
sessionStorage.removeItem('oidc_pkce_verifier')
- const redirect = '/login'
+ const redirect = sessionStorage.getItem('post_login_redirect') || '/me'
sessionStorage.removeItem('post_login_redirect')
- router.push(redirect)
+ router.replace(redirect)
} catch (err) {
sessionStorage.removeItem('oidc_expected_state')
sessionStorage.removeItem('oidc_pkce_verifier')
diff --git a/src/pages/admin/CompaniesPage.vue b/src/pages/admin/CompaniesPage.vue
index faa91b4..0b8661f 100644
--- a/src/pages/admin/CompaniesPage.vue
+++ b/src/pages/admin/CompaniesPage.vue
@@ -2,7 +2,7 @@
公司管理
- 新增公司
+ 新增公司
@@ -11,31 +11,40 @@
-
-
+
+
+
編輯
- 站台
+ 查看站台
刪除
-
-
-
+
+
+
+
+
+
+
+
+
+
- 取消
- 確認
+ 取消
+ 建立
-
-
+
+
-
+
+
@@ -45,16 +54,17 @@
取消
- 儲存
+ 儲存
-
+
-
-
-
+
+
+
+
關閉
@@ -73,21 +83,22 @@ const companies = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
-const showDialog = ref(false)
-const submitting = ref(false)
-const showEditDialog = ref(false)
-const savingEdit = ref(false)
-const formRef = ref()
-const form = ref({ name: '' })
-const editForm = ref({ company_key: '', name: '', status: 'active' })
+const showCreateDialog = ref(false)
+const showEditDialog = ref(false)
+const creating = ref(false)
+const saving = ref(false)
+const createFormRef = ref()
+
+const createForm = ref({ display_name: '', legal_name: '', status: 'active' })
+const editForm = ref({ company_key: '', display_name: '', legal_name: '', status: 'active' })
const rules = {
- name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
+ display_name: [{ required: true, message: '請輸入顯示名稱', trigger: 'blur' }]
}
const showSitesDialog = ref(false)
const sitesLoading = ref(false)
-const selectedCompanyKey = ref('')
+const selectedCompanyDisplayName = ref('')
const companySites = ref([])
async function load() {
@@ -98,67 +109,75 @@ async function load() {
companies.value = res.data?.items || []
} catch (err) {
error.value = true
- errorMsg.value = err.response?.status === 422
- ? '缺少管理員 API 認證,請檢查前端 .env.development'
- : '載入失敗,請稍後再試'
+ errorMsg.value = err.response?.data?.detail || '載入公司失敗'
} finally {
loading.value = false
}
}
-function resetForm() {
- form.value = { name: '' }
+function resetCreateForm() {
+ createForm.value = { display_name: '', legal_name: '', status: 'active' }
}
function openEdit(row) {
- editForm.value = { company_key: row.company_key, name: row.name, status: row.status || 'active' }
+ editForm.value = {
+ company_key: row.company_key,
+ display_name: row.display_name,
+ legal_name: row.legal_name || '',
+ status: row.status || 'active'
+ }
showEditDialog.value = true
}
function resetEditForm() {
- editForm.value = { company_key: '', name: '', status: 'active' }
+ editForm.value = { company_key: '', display_name: '', legal_name: '', status: 'active' }
}
async function handleCreate() {
- const valid = await formRef.value.validate().catch(() => false)
+ const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
- submitting.value = true
+ creating.value = true
try {
- const res = await createCompany(form.value)
+ const res = await createCompany(createForm.value)
ElMessage.success(`新增成功:${res.data?.company_key || ''}`)
- showDialog.value = false
- resetForm()
+ showCreateDialog.value = false
+ resetCreateForm()
await load()
} catch (err) {
- ElMessage.error('新增失敗')
+ ElMessage.error(err.response?.data?.detail || '新增公司失敗')
} finally {
- submitting.value = false
+ creating.value = false
}
}
async function handleEdit() {
- savingEdit.value = true
+ saving.value = true
try {
- await updateCompany(editForm.value.company_key, { name: editForm.value.name, status: editForm.value.status })
+ await updateCompany(editForm.value.company_key, {
+ display_name: editForm.value.display_name,
+ legal_name: editForm.value.legal_name || null,
+ status: editForm.value.status
+ })
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
- ElMessage.error('更新失敗')
+ ElMessage.error(err.response?.data?.detail || '更新公司失敗')
} finally {
- savingEdit.value = false
+ saving.value = false
}
}
async function openSites(row) {
- selectedCompanyKey.value = row.company_key
showSitesDialog.value = true
+ selectedCompanyDisplayName.value = `${row.display_name} (${row.company_key})`
sitesLoading.value = true
try {
const res = await getCompanySites(row.company_key)
- companySites.value = res.data?.items || []
- } catch (err) {
- ElMessage.error('載入公司站台失敗')
+ companySites.value = res.data?.sites || []
+ } catch (_err) {
+ ElMessage.error('載入站台列表失敗')
+ companySites.value = []
} finally {
sitesLoading.value = false
}
@@ -167,7 +186,7 @@ async function openSites(row) {
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
- `確認刪除公司 ${row.name}(${row.company_key})?此操作會一併刪除旗下站台與相關授權。`,
+ `確認刪除公司 ${row.display_name}(${row.company_key})?`,
'刪除確認',
{ type: 'warning' }
)
@@ -176,7 +195,7 @@ async function handleDelete(row) {
await load()
} catch (err) {
if (err === 'cancel') return
- ElMessage.error(err.response?.data?.detail || '刪除失敗')
+ ElMessage.error(err.response?.data?.detail || '刪除公司失敗')
}
}
diff --git a/src/pages/admin/MembersPage.vue b/src/pages/admin/MembersPage.vue
index 9defad7..dc66a37 100644
--- a/src/pages/admin/MembersPage.vue
+++ b/src/pages/admin/MembersPage.vue
@@ -14,29 +14,35 @@
-
+
-
-
+
+
{{ row.is_active ? '是' : '否' }}
-
+
編輯
+ 角色
重設密碼
刪除
-
-
+
+
-
-
-
-
+
+
+
+
@@ -48,15 +54,20 @@
-
-
+
+
-
-
-
+
+
+
@@ -67,6 +78,20 @@
儲存
+
+
+
+
+
+
+
+
+
+
+
+ 關閉
+
+
@@ -74,31 +99,32 @@
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
+import { getSites } from '@/api/sites'
import {
getMembers,
- upsertMember,
+ createMember,
updateMember,
deleteMember,
resetMemberPassword,
- getMemberPermissionGroups,
- setMemberPermissionGroups
+ getMemberSites,
+ setMemberSites,
+ getMemberRoles
} from '@/api/members'
-import { getPermissionGroups } from '@/api/permission-groups'
const members = ref([])
-const groups = ref([])
+const siteOptions = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
const showCreateDialog = ref(false)
-const createFormRef = ref()
const creating = ref(false)
+const createFormRef = ref()
const createForm = ref({
username: '',
email: '',
display_name: '',
- group_keys: [],
+ site_keys: [],
is_active: true,
sync_to_idp: true
})
@@ -114,21 +140,30 @@ const editForm = ref({
username: '',
email: '',
display_name: '',
- group_keys: [],
+ site_keys: [],
is_active: true,
sync_to_idp: true
})
+const showRolesDialog = ref(false)
+const selectedUserLabel = ref('')
+const effectiveRoles = ref([])
+const rolesLoading = ref(false)
+
+async function loadCatalogs() {
+ const res = await getSites({ limit: 500, offset: 0 })
+ siteOptions.value = res.data?.items || []
+}
+
async function load() {
loading.value = true
error.value = false
try {
- const [membersRes, groupsRes] = await Promise.all([getMembers(), getPermissionGroups()])
+ const [membersRes] = await Promise.all([getMembers(), loadCatalogs()])
members.value = membersRes.data?.items || []
- groups.value = groupsRes.data?.items || []
} catch (err) {
error.value = true
- errorMsg.value = err.response?.data?.detail || '載入失敗,請稍後再試'
+ errorMsg.value = err.response?.data?.detail || '載入會員失敗'
} finally {
loading.value = false
}
@@ -139,38 +174,7 @@ function resetCreateForm() {
username: '',
email: '',
display_name: '',
- group_keys: [],
- is_active: true,
- sync_to_idp: true
- }
-}
-
-async function openEdit(row) {
- editForm.value = {
- user_sub: row.user_sub,
- username: row.username || '',
- email: row.email || '',
- display_name: row.display_name || '',
- group_keys: [],
- is_active: !!row.is_active,
- sync_to_idp: true
- }
- try {
- const res = await getMemberPermissionGroups(row.user_sub)
- editForm.value.group_keys = res.data?.group_keys || []
- } catch (err) {
- ElMessage.warning('載入會員群組失敗,仍可先編輯基本資料')
- }
- showEditDialog.value = true
-}
-
-function resetEditForm() {
- editForm.value = {
- user_sub: '',
- username: '',
- email: '',
- display_name: '',
- group_keys: [],
+ site_keys: [],
is_active: true,
sync_to_idp: true
}
@@ -181,23 +185,60 @@ async function handleCreate() {
if (!valid) return
creating.value = true
try {
- const created = await upsertMember({ ...createForm.value })
- const createdSub = created.data?.user_sub
- if (createdSub && createForm.value.group_keys.length > 0) {
- await setMemberPermissionGroups(createdSub, createForm.value.group_keys)
+ const payload = {
+ username: createForm.value.username || null,
+ email: createForm.value.email || null,
+ display_name: createForm.value.display_name || null,
+ is_active: createForm.value.is_active,
+ sync_to_idp: createForm.value.sync_to_idp
+ }
+ const res = await createMember(payload)
+ const userSub = res.data?.user_sub
+ if (userSub) {
+ await setMemberSites(userSub, createForm.value.site_keys || [])
}
ElMessage.success('新增會員成功')
showCreateDialog.value = false
resetCreateForm()
await load()
} catch (err) {
- const detail = err.response?.data?.detail
- ElMessage.error(detail || '新增會員失敗')
+ ElMessage.error(err.response?.data?.detail || '新增會員失敗')
} finally {
creating.value = false
}
}
+async function openEdit(row) {
+ editForm.value = {
+ user_sub: row.user_sub,
+ username: row.username || '',
+ email: row.email || '',
+ display_name: row.display_name || '',
+ site_keys: [],
+ is_active: !!row.is_active,
+ sync_to_idp: true
+ }
+ try {
+ const res = await getMemberSites(row.user_sub)
+ editForm.value.site_keys = (res.data?.sites || []).map((site) => site.site_key)
+ } catch (_err) {
+ ElMessage.warning('讀取會員站台失敗,仍可編輯基本資料')
+ }
+ showEditDialog.value = true
+}
+
+function resetEditForm() {
+ editForm.value = {
+ user_sub: '',
+ username: '',
+ email: '',
+ display_name: '',
+ site_keys: [],
+ is_active: true,
+ sync_to_idp: true
+ }
+}
+
async function handleEdit() {
saving.value = true
try {
@@ -208,13 +249,12 @@ async function handleEdit() {
is_active: editForm.value.is_active,
sync_to_idp: editForm.value.sync_to_idp
})
- await setMemberPermissionGroups(editForm.value.user_sub, editForm.value.group_keys || [])
+ await setMemberSites(editForm.value.user_sub, editForm.value.site_keys || [])
ElMessage.success('更新會員成功')
showEditDialog.value = false
await load()
} catch (err) {
- const detail = err.response?.data?.detail
- ElMessage.error(detail || '更新會員失敗')
+ ElMessage.error(err.response?.data?.detail || '更新會員失敗')
} finally {
saving.value = false
}
@@ -223,16 +263,15 @@ async function handleEdit() {
async function handleResetPassword(row) {
try {
const res = await resetMemberPassword(row.user_sub)
- const pwd = res.data?.temporary_password || ''
- if (!pwd) {
- ElMessage.success('密碼已重設')
+ const tempPassword = res.data?.temporary_password
+ if (tempPassword) {
+ await navigator.clipboard.writeText(tempPassword)
+ ElMessage.success('已重設密碼,臨時密碼已複製')
return
}
- await navigator.clipboard.writeText(pwd)
- ElMessage.success(`已重設密碼,臨時密碼已複製:${pwd}`)
+ ElMessage.success('已重設密碼')
} catch (err) {
- const detail = err.response?.data?.detail
- ElMessage.error(detail || '重設密碼失敗')
+ ElMessage.error(err.response?.data?.detail || '重設密碼失敗')
}
}
@@ -243,12 +282,27 @@ async function handleDelete(row) {
'刪除確認',
{ type: 'warning' }
)
- await deleteMember(row.user_sub)
+ await deleteMember(row.user_sub, true)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
- ElMessage.error(err.response?.data?.detail || '刪除失敗')
+ ElMessage.error(err.response?.data?.detail || '刪除會員失敗')
+ }
+}
+
+async function openRoles(row) {
+ selectedUserLabel.value = `${row.display_name || row.username || row.user_sub}`
+ showRolesDialog.value = true
+ rolesLoading.value = true
+ try {
+ const res = await getMemberRoles(row.user_sub)
+ effectiveRoles.value = res.data?.roles || []
+ } catch (_err) {
+ ElMessage.error('載入會員角色失敗')
+ effectiveRoles.value = []
+ } finally {
+ rolesLoading.value = false
}
}
diff --git a/src/pages/admin/ModulesPage.vue b/src/pages/admin/ModulesPage.vue
deleted file mode 100644
index 05426f9..0000000
--- a/src/pages/admin/ModulesPage.vue
+++ /dev/null
@@ -1,241 +0,0 @@
-
-
-
-
模組管理
- 新增模組
-
-
-
-
-
-
-
-
-
- {{ getSystemName(row.system_key) }}
-
-
-
-
-
-
- 編輯
- 群組
- 會員
- 刪除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 確認
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 儲存
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ row.is_active ? '是' : '否' }}
-
-
-
-
-
- 關閉
-
-
-
-
-
-
diff --git a/src/pages/admin/PermissionGroupsPage.vue b/src/pages/admin/PermissionGroupsPage.vue
deleted file mode 100644
index f8525db..0000000
--- a/src/pages/admin/PermissionGroupsPage.vue
+++ /dev/null
@@ -1,371 +0,0 @@
-
-
-
群組與權限管理
-
-
-
-
- 群組列表
- 新增群組
-
-
-
-
-
-
-
-
-
-
-
-
- 編輯
- 設定關聯
- 刪除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 儲存關聯
-
-
-
-
-
-
-
-
-
-
- 取消
- 確認
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 取消
- 儲存
-
-
-
-
-
-
diff --git a/src/pages/admin/RolesPage.vue b/src/pages/admin/RolesPage.vue
new file mode 100644
index 0000000..4cfe50b
--- /dev/null
+++ b/src/pages/admin/RolesPage.vue
@@ -0,0 +1,269 @@
+
+
+
+
角色管理
+ 新增角色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 編輯
+ 站台
+ 刪除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 建立
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 儲存
+
+
+
+
+
+
+
+
+
+
+
+ 關閉
+
+
+
+
+
+
diff --git a/src/pages/admin/SitesPage.vue b/src/pages/admin/SitesPage.vue
index 1eb39ae..87fa5f1 100644
--- a/src/pages/admin/SitesPage.vue
+++ b/src/pages/admin/SitesPage.vue
@@ -2,7 +2,7 @@
站台管理
- 新增站台
+ 新增站台
@@ -11,43 +11,62 @@
-
- {{ getCompanyName(row.company_key) }}
-
-
-
-
+
+
+
+
+
編輯
+ 角色
+ 會員
刪除
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- 取消
- 確認
+ 取消
+ 建立
-
+
-
-
-
+
+
+
-
+
+
@@ -57,7 +76,53 @@
取消
- 儲存
+ 儲存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 儲存角色
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.is_active ? '是' : '否' }}
+
+
+
+ 關閉
@@ -67,110 +132,123 @@
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
-import { getSites, createSite, updateSite, deleteSite } from '@/api/sites'
+import { getSites, createSite, updateSite, deleteSite, getSiteRoles, setSiteRoles, getSiteMembers } from '@/api/sites'
import { getCompanies } from '@/api/companies'
+import { getRoles } from '@/api/roles'
const sites = ref([])
const companies = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
-const showDialog = ref(false)
-const submitting = ref(false)
-const showEditDialog = ref(false)
-const savingEdit = ref(false)
-const formRef = ref()
-const form = ref({ company_key: '', name: '' })
-const editForm = ref({ site_key: '', company_key: '', name: '', status: 'active' })
+const showCreateDialog = ref(false)
+const showEditDialog = ref(false)
+const creating = ref(false)
+const saving = ref(false)
+const createFormRef = ref()
+
+const createForm = ref({ company_key: '', display_name: '', domain: '', status: 'active' })
+const editForm = ref({ site_key: '', company_key: '', display_name: '', domain: '', status: 'active' })
const rules = {
company_key: [{ required: true, message: '請選擇公司', trigger: 'change' }],
- name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
+ display_name: [{ required: true, message: '請輸入站台名稱', trigger: 'blur' }]
}
-async function loadCompanies() {
- const res = await getCompanies()
- companies.value = res.data?.items || []
-}
+const showRolesDialog = ref(false)
+const rolesLoading = ref(false)
+const rolesSaving = ref(false)
+const selectedSiteKey = ref('')
+const selectedSiteLabel = ref('')
+const siteRoles = ref([])
+const roleOptions = ref([])
+const selectedRoleKeys = ref([])
+
+const showMembersDialog = ref(false)
+const membersLoading = ref(false)
+const siteMembers = ref([])
async function load() {
loading.value = true
error.value = false
try {
- const [sitesRes] = await Promise.all([getSites(), loadCompanies()])
+ const [sitesRes, companiesRes] = await Promise.all([getSites(), getCompanies()])
sites.value = sitesRes.data?.items || []
+ companies.value = companiesRes.data?.items || []
} catch (err) {
error.value = true
- errorMsg.value = err.response?.status === 422
- ? '缺少管理員 API 認證,請檢查前端 .env.development'
- : '載入失敗,請稍後再試'
+ errorMsg.value = err.response?.data?.detail || '載入站台失敗'
} finally {
loading.value = false
}
}
-function resetForm() {
- form.value = { company_key: '', name: '' }
+function resetCreateForm() {
+ createForm.value = { company_key: '', display_name: '', domain: '', status: 'active' }
}
function openEdit(row) {
editForm.value = {
site_key: row.site_key,
company_key: row.company_key,
- name: row.name,
+ display_name: row.display_name,
+ domain: row.domain || '',
status: row.status || 'active'
}
showEditDialog.value = true
}
-function getCompanyName(companyKey) {
- const company = companies.value.find(c => c.company_key === companyKey)
- return company ? company.name : companyKey
-}
-
function resetEditForm() {
- editForm.value = { site_key: '', company_key: '', name: '', status: 'active' }
+ editForm.value = { site_key: '', company_key: '', display_name: '', domain: '', status: 'active' }
}
async function handleCreate() {
- const valid = await formRef.value.validate().catch(() => false)
+ const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
- submitting.value = true
+ creating.value = true
try {
- const res = await createSite(form.value)
- ElMessage.success(`新增成功:${res.data?.site_key || ''}`)
- showDialog.value = false
- resetForm()
+ const payload = {
+ company_key: createForm.value.company_key,
+ display_name: createForm.value.display_name,
+ domain: createForm.value.domain || null,
+ status: createForm.value.status
+ }
+ await createSite(payload)
+ ElMessage.success('新增站台成功')
+ showCreateDialog.value = false
+ resetCreateForm()
await load()
} catch (err) {
- ElMessage.error('新增失敗')
+ ElMessage.error(err.response?.data?.detail || '新增站台失敗')
} finally {
- submitting.value = false
+ creating.value = false
}
}
async function handleEdit() {
- savingEdit.value = true
+ saving.value = true
try {
- await updateSite(editForm.value.site_key, {
+ const payload = {
company_key: editForm.value.company_key,
- name: editForm.value.name,
+ display_name: editForm.value.display_name,
+ domain: editForm.value.domain || null,
status: editForm.value.status
- })
+ }
+ await updateSite(editForm.value.site_key, payload)
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
- ElMessage.error('更新失敗')
+ ElMessage.error(err.response?.data?.detail || '更新站台失敗')
} finally {
- savingEdit.value = false
+ saving.value = false
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
- `確認刪除站台 ${row.name}(${row.site_key})?此操作會清理相關授權。`,
+ `確認刪除站台 ${row.display_name}(${row.site_key})?`,
'刪除確認',
{ type: 'warning' }
)
@@ -179,7 +257,61 @@ async function handleDelete(row) {
await load()
} catch (err) {
if (err === 'cancel') return
- ElMessage.error(err.response?.data?.detail || '刪除失敗')
+ ElMessage.error(err.response?.data?.detail || '刪除站台失敗')
+ }
+}
+
+async function openRoles(row) {
+ selectedSiteKey.value = row.site_key
+ selectedSiteLabel.value = `${row.company_display_name} / ${row.display_name}`
+ showRolesDialog.value = true
+ rolesLoading.value = true
+ try {
+ const [siteRolesRes, allRolesRes] = await Promise.all([
+ getSiteRoles(row.site_key),
+ getRoles({ limit: 500, offset: 0 })
+ ])
+ siteRoles.value = siteRolesRes.data?.roles || []
+ roleOptions.value = allRolesRes.data?.items || []
+ selectedRoleKeys.value = siteRoles.value.map((role) => role.role_key)
+ } catch (_err) {
+ ElMessage.error('載入站台角色失敗')
+ siteRoles.value = []
+ roleOptions.value = []
+ selectedRoleKeys.value = []
+ } finally {
+ rolesLoading.value = false
+ }
+}
+
+async function handleSaveSiteRoles() {
+ if (!selectedSiteKey.value) return
+ rolesSaving.value = true
+ try {
+ const deduped = [...new Set(selectedRoleKeys.value)]
+ const res = await setSiteRoles(selectedSiteKey.value, deduped)
+ siteRoles.value = res.data?.roles || []
+ selectedRoleKeys.value = siteRoles.value.map((role) => role.role_key)
+ ElMessage.success('站台角色已更新')
+ } catch (err) {
+ ElMessage.error(err.response?.data?.detail || '儲存站台角色失敗')
+ } finally {
+ rolesSaving.value = false
+ }
+}
+
+async function openMembers(row) {
+ selectedSiteLabel.value = `${row.company_display_name} / ${row.display_name}`
+ showMembersDialog.value = true
+ membersLoading.value = true
+ try {
+ const res = await getSiteMembers(row.site_key)
+ siteMembers.value = res.data?.members || []
+ } catch (_err) {
+ ElMessage.error('載入站台會員失敗')
+ siteMembers.value = []
+ } finally {
+ membersLoading.value = false
}
}
diff --git a/src/pages/admin/SystemsPage.vue b/src/pages/admin/SystemsPage.vue
index f5d7fd9..ad7c8eb 100644
--- a/src/pages/admin/SystemsPage.vue
+++ b/src/pages/admin/SystemsPage.vue
@@ -2,55 +2,49 @@
系統管理
- 新增系統
+ 新增系統
-
-
+
-
-
-
+
+
+
+
編輯
- 群組
- 會員
+ 角色
刪除
-
-
-
-
+
+
+
+
+
+
+
+
+
- 取消
- 確認
+ 取消
+ 建立
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -60,34 +54,20 @@
取消
- 儲存
+ 儲存
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ row.is_active ? '是' : '否' }}
-
-
-
-
+
+
+
+
+
+
+
+
- 關閉
+ 關閉
@@ -97,30 +77,30 @@
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
-import { getSystems, createSystem, updateSystem, deleteSystem, getSystemGroups, getSystemMembers } from '@/api/systems'
+import { getSystems, createSystem, updateSystem, deleteSystem, getSystemRoles } from '@/api/systems'
const systems = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
-const showDialog = ref(false)
-const submitting = ref(false)
-const formRef = ref()
-const showEditDialog = ref(false)
-const savingEdit = ref(false)
-const form = ref({ name: '' })
-const editForm = ref({ system_key: '', name: '', status: 'active' })
+const showCreateDialog = ref(false)
+const showEditDialog = ref(false)
+const creating = ref(false)
+const saving = ref(false)
+const createFormRef = ref()
+
+const createForm = ref({ name: '', idp_client_id: '', status: 'active' })
+const editForm = ref({ system_key: '', name: '', idp_client_id: '', status: 'active' })
const rules = {
- name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
+ name: [{ required: true, message: '請輸入系統名稱', trigger: 'blur' }],
+ idp_client_id: [{ required: true, message: '請輸入 Keycloak Client ID', trigger: 'blur' }]
}
-const showRelationDialog = ref(false)
-const relationLoading = ref(false)
-const relationSystemKey = ref('')
-const relationTab = ref('groups')
-const relationGroups = ref([])
-const relationMembers = ref([])
+const showRolesDialog = ref(false)
+const selectedSystemLabel = ref('')
+const systemRoles = ref([])
+const rolesLoading = ref(false)
async function load() {
loading.value = true
@@ -130,93 +110,93 @@ async function load() {
systems.value = res.data?.items || []
} catch (err) {
error.value = true
- errorMsg.value = err.response?.status === 422
- ? '缺少管理員 API 認證,請檢查前端 .env.development'
- : '載入失敗,請稍後再試'
+ errorMsg.value = err.response?.data?.detail || '載入系統失敗'
} finally {
loading.value = false
}
}
-function resetForm() {
- form.value = { name: '' }
+function resetCreateForm() {
+ createForm.value = { name: '', idp_client_id: '', status: 'active' }
}
function openEdit(row) {
editForm.value = {
system_key: row.system_key,
name: row.name,
+ idp_client_id: row.idp_client_id,
status: row.status || 'active'
}
showEditDialog.value = true
}
function resetEditForm() {
- editForm.value = { system_key: '', name: '', status: 'active' }
+ editForm.value = { system_key: '', name: '', idp_client_id: '', status: 'active' }
}
async function handleCreate() {
- const valid = await formRef.value.validate().catch(() => false)
+ const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
- submitting.value = true
+ creating.value = true
try {
- const res = await createSystem(form.value)
- ElMessage.success(`新增成功:${res.data?.system_key || ''}`)
- showDialog.value = false
- resetForm()
+ await createSystem(createForm.value)
+ ElMessage.success('新增系統成功')
+ showCreateDialog.value = false
+ resetCreateForm()
await load()
} catch (err) {
- ElMessage.error('新增失敗,請稍後再試')
+ ElMessage.error(err.response?.data?.detail || '新增系統失敗')
} finally {
- submitting.value = false
+ creating.value = false
}
}
async function handleEdit() {
- savingEdit.value = true
+ saving.value = true
try {
await updateSystem(editForm.value.system_key, {
name: editForm.value.name,
+ idp_client_id: editForm.value.idp_client_id,
status: editForm.value.status
})
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
- ElMessage.error('更新失敗')
+ ElMessage.error(err.response?.data?.detail || '更新系統失敗')
} finally {
- savingEdit.value = false
+ saving.value = false
}
}
async function handleDelete(row) {
try {
- await ElMessageBox.confirm(`確認刪除系統 ${row.name}(${row.system_key})?`, '刪除確認', { type: 'warning' })
+ await ElMessageBox.confirm(
+ `確認刪除系統 ${row.name}(${row.system_key})?`,
+ '刪除確認',
+ { type: 'warning' }
+ )
await deleteSystem(row.system_key)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
- ElMessage.error(err.response?.data?.detail || '刪除失敗')
+ ElMessage.error(err.response?.data?.detail || '刪除系統失敗')
}
}
-async function openRelations(row, tab) {
- relationSystemKey.value = row.system_key
- relationTab.value = tab
- showRelationDialog.value = true
- relationLoading.value = true
+async function openRoles(row) {
+ selectedSystemLabel.value = `${row.name} (${row.system_key})`
+ showRolesDialog.value = true
+ rolesLoading.value = true
try {
- const [groupsRes, membersRes] = await Promise.all([
- getSystemGroups(row.system_key),
- getSystemMembers(row.system_key)
- ])
- relationGroups.value = groupsRes.data?.items || []
- relationMembers.value = membersRes.data?.items || []
- } catch (err) {
- ElMessage.error('載入系統關聯資料失敗')
+ const res = await getSystemRoles(row.system_key)
+ systemRoles.value = res.data?.roles || []
+ } catch (_err) {
+ ElMessage.error('載入系統角色失敗')
+ systemRoles.value = []
} finally {
- relationLoading.value = false
+ rolesLoading.value = false
}
}
diff --git a/src/pages/permissions/PermissionAdminPage.vue b/src/pages/permissions/PermissionAdminPage.vue
deleted file mode 100644
index b2f32e8..0000000
--- a/src/pages/permissions/PermissionAdminPage.vue
+++ /dev/null
@@ -1,439 +0,0 @@
-
-
-
權限管理
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Grant 授權
-
- 清除
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Revoke 撤銷
-
- 清除
-
-
-
-
-
-
-
-
-
已授權列表(直接授權)
-
-
-
-
-
- 查詢
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 撤銷
-
-
-
-
-
-
-
-
diff --git a/src/pages/permissions/PermissionSnapshotPage.vue b/src/pages/permissions/PermissionSnapshotPage.vue
index 69b543a..5a3b1af 100644
--- a/src/pages/permissions/PermissionSnapshotPage.vue
+++ b/src/pages/permissions/PermissionSnapshotPage.vue
@@ -21,23 +21,20 @@
Sub:
{{ snapshot.user_sub }}
-
+
-
-
-
-
-
+
+
+
+
+
diff --git a/src/router/index.js b/src/router/index.js
index b776b21..621d25e 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -25,24 +25,6 @@ const routes = [
component: () => import('@/pages/permissions/PermissionSnapshotPage.vue'),
meta: { requiresAuth: true }
},
- {
- path: '/admin/permissions',
- name: 'admin-permissions',
- component: () => import('@/pages/permissions/PermissionAdminPage.vue'),
- meta: { requiresAuth: true }
- },
- {
- path: '/admin/systems',
- name: 'admin-systems',
- component: () => import('@/pages/admin/SystemsPage.vue'),
- meta: { requiresAuth: true }
- },
- {
- path: '/admin/modules',
- name: 'admin-modules',
- component: () => import('@/pages/admin/ModulesPage.vue'),
- meta: { requiresAuth: true }
- },
{
path: '/admin/companies',
name: 'admin-companies',
@@ -56,15 +38,21 @@ const routes = [
meta: { requiresAuth: true }
},
{
- path: '/admin/members',
- name: 'admin-members',
- component: () => import('@/pages/admin/MembersPage.vue'),
+ path: '/admin/systems',
+ name: 'admin-systems',
+ component: () => import('@/pages/admin/SystemsPage.vue'),
meta: { requiresAuth: true }
},
{
- path: '/admin/permission-groups',
- name: 'admin-permission-groups',
- component: () => import('@/pages/admin/PermissionGroupsPage.vue'),
+ path: '/admin/roles',
+ name: 'admin-roles',
+ component: () => import('@/pages/admin/RolesPage.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/admin/members',
+ name: 'admin-members',
+ component: () => import('@/pages/admin/MembersPage.vue'),
meta: { requiresAuth: true }
},
{
diff --git a/src/stores/admin.js b/src/stores/admin.js
deleted file mode 100644
index f802f47..0000000
--- a/src/stores/admin.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
-import { getSystems } from '@/api/systems'
-import { getModules } from '@/api/modules'
-import { getCompanies } from '@/api/companies'
-import { getSites } from '@/api/sites'
-
-export const useAdminStore = defineStore('admin', () => {
- const systems = ref([])
- const modules = ref([])
- const companies = ref([])
- const sites = ref([])
-
- async function loadAllData() {
- try {
- const [sysRes, modRes, comRes, siteRes] = await Promise.all([
- getSystems(),
- getModules(),
- getCompanies(),
- getSites()
- ])
- systems.value = sysRes.data || []
- modules.value = modRes.data || []
- companies.value = comRes.data || []
- sites.value = siteRes.data || []
- } catch (err) {
- console.error('Error loading admin data:', err)
- throw err
- }
- }
-
- return {
- systems,
- modules,
- companies,
- sites,
- loadAllData
- }
-})
diff --git a/src/stores/permission.js b/src/stores/permission.js
index b529e25..b07c6f5 100644
--- a/src/stores/permission.js
+++ b/src/stores/permission.js
@@ -1,7 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getMyPermissionSnapshot } from '@/api/me'
-import { grantPermission, revokePermission } from '@/api/permission-admin'
export const usePermissionStore = defineStore('permission', () => {
const snapshot = ref(null)
@@ -12,20 +11,8 @@ export const usePermissionStore = defineStore('permission', () => {
return res.data
}
- async function grant(data) {
- const res = await grantPermission(data)
- return res.data
- }
-
- async function revoke(data) {
- const res = await revokePermission(data)
- return res.data
- }
-
return {
snapshot,
- fetchMySnapshot,
- grant,
- revoke
+ fetchMySnapshot
}
})