feat(keys): auto-generate entity keys and remove manual key input from admin create forms
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.keygen import generate_key
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.api_client import ApiClient
|
from app.models.api_client import ApiClient
|
||||||
@@ -81,6 +82,14 @@ def _split_module_key(payload_module: str | None) -> str:
|
|||||||
return payload_module
|
return payload_module
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_unique_key(prefix: str, exists_fn) -> str:
|
||||||
|
for salt in range(1000):
|
||||||
|
key = generate_key(prefix=prefix, salt=salt)
|
||||||
|
if not exists_fn(key):
|
||||||
|
return key
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"failed_to_generate_{prefix.lower()}_key")
|
||||||
|
|
||||||
|
|
||||||
def _sync_member_to_authentik(
|
def _sync_member_to_authentik(
|
||||||
*,
|
*,
|
||||||
authentik_sub: str,
|
authentik_sub: str,
|
||||||
@@ -124,9 +133,8 @@ def create_system(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> SystemItem:
|
) -> SystemItem:
|
||||||
repo = SystemsRepository(db)
|
repo = SystemsRepository(db)
|
||||||
if repo.get_by_key(payload.system_key):
|
system_key = _generate_unique_key("ST", repo.get_by_key)
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_key_already_exists")
|
row = repo.create(system_key=system_key, name=payload.name, status=payload.status)
|
||||||
row = repo.create(system_key=payload.system_key, name=payload.name, status=payload.status)
|
|
||||||
return SystemItem(id=row.id, system_key=row.system_key, name=row.name, status=row.status)
|
return SystemItem(id=row.id, system_key=row.system_key, name=row.name, status=row.status)
|
||||||
|
|
||||||
|
|
||||||
@@ -180,9 +188,8 @@ def create_module(
|
|||||||
system = systems_repo.get_by_key(payload.system_key)
|
system = systems_repo.get_by_key(payload.system_key)
|
||||||
if not system:
|
if not system:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||||
full_module_key = f"{payload.system_key}.{payload.module_key}"
|
leaf_module_key = _generate_unique_key("MD", lambda k: modules_repo.get_by_key(f"{payload.system_key}.{k}"))
|
||||||
if modules_repo.get_by_key(full_module_key):
|
full_module_key = f"{payload.system_key}.{leaf_module_key}"
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="module_key_already_exists")
|
|
||||||
row = modules_repo.create(
|
row = modules_repo.create(
|
||||||
module_key=full_module_key,
|
module_key=full_module_key,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
@@ -317,9 +324,8 @@ def create_company(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> CompanyItem:
|
) -> CompanyItem:
|
||||||
repo = CompaniesRepository(db)
|
repo = CompaniesRepository(db)
|
||||||
if repo.get_by_key(payload.company_key):
|
company_key = _generate_unique_key("CP", repo.get_by_key)
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="company_key_already_exists")
|
row = repo.create(company_key=company_key, name=payload.name, status=payload.status)
|
||||||
row = repo.create(company_key=payload.company_key, name=payload.name, status=payload.status)
|
|
||||||
return CompanyItem(id=row.id, company_key=row.company_key, name=row.name, status=row.status)
|
return CompanyItem(id=row.id, company_key=row.company_key, name=row.name, status=row.status)
|
||||||
|
|
||||||
|
|
||||||
@@ -414,9 +420,8 @@ def create_site(
|
|||||||
company = companies_repo.get_by_key(payload.company_key)
|
company = companies_repo.get_by_key(payload.company_key)
|
||||||
if not company:
|
if not company:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
|
||||||
if sites_repo.get_by_key(payload.site_key):
|
site_key = _generate_unique_key("ST", sites_repo.get_by_key)
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="site_key_already_exists")
|
row = sites_repo.create(site_key=site_key, company_id=company.id, name=payload.name, status=payload.status)
|
||||||
row = sites_repo.create(site_key=payload.site_key, company_id=company.id, name=payload.name, status=payload.status)
|
|
||||||
return SiteItem(id=row.id, site_key=row.site_key, company_key=payload.company_key, name=row.name, status=row.status)
|
return SiteItem(id=row.id, site_key=row.site_key, company_key=payload.company_key, name=row.name, status=row.status)
|
||||||
|
|
||||||
|
|
||||||
@@ -688,9 +693,8 @@ def create_permission_group(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> PermissionGroupItem:
|
) -> PermissionGroupItem:
|
||||||
repo = PermissionGroupsRepository(db)
|
repo = PermissionGroupsRepository(db)
|
||||||
if repo.get_by_key(payload.group_key):
|
group_key = _generate_unique_key("GP", repo.get_by_key)
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="group_key_already_exists")
|
row = repo.create(group_key=group_key, name=payload.name, status=payload.status)
|
||||||
row = repo.create(group_key=payload.group_key, name=payload.name, status=payload.status)
|
|
||||||
return PermissionGroupItem(id=row.id, group_key=row.group_key, name=row.name, status=row.status)
|
return PermissionGroupItem(id=row.id, group_key=row.group_key, name=row.name, status=row.status)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
backend/app/core/keygen.py
Normal file
9
backend/app/core/keygen.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def generate_key(prefix: str, salt: int = 0) -> str:
|
||||||
|
date_str = datetime.now().strftime("%Y%m%d")
|
||||||
|
tail = (int(time.time() * 1000) + salt) % 10000
|
||||||
|
return f"{prefix}{date_str}X{tail:04d}"
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@ from typing import Literal
|
|||||||
|
|
||||||
|
|
||||||
class SystemCreateRequest(BaseModel):
|
class SystemCreateRequest(BaseModel):
|
||||||
system_key: str
|
|
||||||
name: str
|
name: str
|
||||||
status: str = "active"
|
status: str = "active"
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ class SystemItem(BaseModel):
|
|||||||
|
|
||||||
class ModuleCreateRequest(BaseModel):
|
class ModuleCreateRequest(BaseModel):
|
||||||
system_key: str
|
system_key: str
|
||||||
module_key: str
|
|
||||||
name: str
|
name: str
|
||||||
status: str = "active"
|
status: str = "active"
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ class ModuleItem(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CompanyCreateRequest(BaseModel):
|
class CompanyCreateRequest(BaseModel):
|
||||||
company_key: str
|
|
||||||
name: str
|
name: str
|
||||||
status: str = "active"
|
status: str = "active"
|
||||||
|
|
||||||
@@ -59,7 +56,6 @@ class CompanyItem(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SiteCreateRequest(BaseModel):
|
class SiteCreateRequest(BaseModel):
|
||||||
site_key: str
|
|
||||||
company_key: str
|
company_key: str
|
||||||
name: str
|
name: str
|
||||||
status: str = "active"
|
status: str = "active"
|
||||||
@@ -114,7 +110,6 @@ class ListResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PermissionGroupCreateRequest(BaseModel):
|
class PermissionGroupCreateRequest(BaseModel):
|
||||||
group_key: str
|
|
||||||
name: str
|
name: str
|
||||||
status: str = "active"
|
status: str = "active"
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
<el-dialog v-model="showDialog" title="新增公司" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增公司" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="Company Key" prop="company_key"><el-input v-model="form.company_key" /></el-form-item>
|
|
||||||
<el-form-item label="名稱" prop="name"><el-input v-model="form.name" /></el-form-item>
|
<el-form-item label="名稱" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -79,10 +78,9 @@ const showEditDialog = ref(false)
|
|||||||
const savingEdit = ref(false)
|
const savingEdit = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
const form = ref({ company_key: '', name: '' })
|
const form = ref({ name: '' })
|
||||||
const editForm = ref({ company_key: '', name: '', status: 'active' })
|
const editForm = ref({ company_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
company_key: [{ required: true, message: '請輸入 Company Key', trigger: 'blur' }],
|
|
||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +106,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.value = { company_key: '', name: '' }
|
form.value = { name: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row) {
|
function openEdit(row) {
|
||||||
@@ -125,8 +123,8 @@ async function handleCreate() {
|
|||||||
if (!valid) return
|
if (!valid) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await createCompany(form.value)
|
const res = await createCompany(form.value)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success(`新增成功:${res.data?.company_key || ''}`)
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
resetForm()
|
resetForm()
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -38,9 +38,6 @@
|
|||||||
<el-option v-for="s in systems" :key="s.system_key" :label="`${s.name} (${s.system_key})`" :value="s.system_key" />
|
<el-option v-for="s in systems" :key="s.system_key" :label="`${s.name} (${s.system_key})`" :value="s.system_key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Module Key" prop="module_key">
|
|
||||||
<el-input v-model="form.module_key" placeholder="campaign" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="名稱" prop="name">
|
<el-form-item label="名稱" prop="name">
|
||||||
<el-input v-model="form.name" placeholder="行銷活動" />
|
<el-input v-model="form.name" placeholder="行銷活動" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -119,11 +116,10 @@ const formRef = ref()
|
|||||||
const showEditDialog = ref(false)
|
const showEditDialog = ref(false)
|
||||||
const savingEdit = ref(false)
|
const savingEdit = ref(false)
|
||||||
|
|
||||||
const form = ref({ system_key: '', module_key: '', name: '' })
|
const form = ref({ system_key: '', name: '' })
|
||||||
const editForm = ref({ module_key: '', name: '', status: 'active' })
|
const editForm = ref({ module_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
||||||
module_key: [{ required: true, message: '請輸入 Module Key', trigger: 'blur' }],
|
|
||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +148,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.value = { system_key: '', module_key: '', name: '' }
|
form.value = { system_key: '', name: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row) {
|
function openEdit(row) {
|
||||||
@@ -173,8 +169,8 @@ async function handleCreate() {
|
|||||||
if (!valid) return
|
if (!valid) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await createModule(form.value)
|
const res = await createModule(form.value)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success(`新增成功:${res.data?.module_key || ''}`)
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
resetForm()
|
resetForm()
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -93,9 +93,6 @@
|
|||||||
|
|
||||||
<el-dialog v-model="showCreateGroup" title="新增群組" @close="resetCreateForm">
|
<el-dialog v-model="showCreateGroup" title="新增群組" @close="resetCreateForm">
|
||||||
<el-form :model="createForm" label-width="120px">
|
<el-form :model="createForm" label-width="120px">
|
||||||
<el-form-item label="Group Key">
|
|
||||||
<el-input v-model="createForm.group_key" placeholder="group-001" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="群組名稱">
|
<el-form-item label="群組名稱">
|
||||||
<el-input v-model="createForm.name" placeholder="群組名稱" />
|
<el-input v-model="createForm.name" placeholder="群組名稱" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -157,7 +154,7 @@ const members = ref([])
|
|||||||
|
|
||||||
const showCreateGroup = ref(false)
|
const showCreateGroup = ref(false)
|
||||||
const creatingGroup = ref(false)
|
const creatingGroup = ref(false)
|
||||||
const createForm = reactive({ group_key: '', name: '' })
|
const createForm = reactive({ name: '' })
|
||||||
|
|
||||||
const showEditGroup = ref(false)
|
const showEditGroup = ref(false)
|
||||||
const savingGroup = ref(false)
|
const savingGroup = ref(false)
|
||||||
@@ -198,7 +195,6 @@ const filteredModuleOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function resetCreateForm() {
|
function resetCreateForm() {
|
||||||
createForm.group_key = ''
|
|
||||||
createForm.name = ''
|
createForm.name = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,14 +241,14 @@ async function loadCatalogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateGroup() {
|
async function handleCreateGroup() {
|
||||||
if (!createForm.group_key || !createForm.name) {
|
if (!createForm.name) {
|
||||||
ElMessage.warning('請填寫完整資訊')
|
ElMessage.warning('請填寫完整資訊')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
creatingGroup.value = true
|
creatingGroup.value = true
|
||||||
try {
|
try {
|
||||||
await createPermissionGroup(createForm)
|
const res = await createPermissionGroup(createForm)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success(`新增成功:${res.data?.group_key || ''}`)
|
||||||
showCreateGroup.value = false
|
showCreateGroup.value = false
|
||||||
resetCreateForm()
|
resetCreateForm()
|
||||||
await loadGroups()
|
await loadGroups()
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
<el-dialog v-model="showDialog" title="新增站台" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增站台" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<el-form-item label="Site Key" prop="site_key"><el-input v-model="form.site_key" /></el-form-item>
|
|
||||||
<el-form-item label="Company" prop="company_key">
|
<el-form-item label="Company" prop="company_key">
|
||||||
<el-select v-model="form.company_key" style="width: 100%" filterable>
|
<el-select v-model="form.company_key" style="width: 100%" filterable>
|
||||||
<el-option v-for="c in companies" :key="c.company_key" :label="`${c.name} (${c.company_key})`" :value="c.company_key" />
|
<el-option v-for="c in companies" :key="c.company_key" :label="`${c.name} (${c.company_key})`" :value="c.company_key" />
|
||||||
@@ -79,10 +78,9 @@ const showEditDialog = ref(false)
|
|||||||
const savingEdit = ref(false)
|
const savingEdit = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
const form = ref({ site_key: '', company_key: '', name: '' })
|
const form = ref({ company_key: '', name: '' })
|
||||||
const editForm = ref({ site_key: '', company_key: '', name: '', status: 'active' })
|
const editForm = ref({ site_key: '', company_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
site_key: [{ required: true, message: '請輸入 Site Key', trigger: 'blur' }],
|
|
||||||
company_key: [{ required: true, message: '請選擇公司', trigger: 'change' }],
|
company_key: [{ required: true, message: '請選擇公司', trigger: 'change' }],
|
||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
@@ -109,7 +107,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.value = { site_key: '', company_key: '', name: '' }
|
form.value = { company_key: '', name: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row) {
|
function openEdit(row) {
|
||||||
@@ -131,8 +129,8 @@ async function handleCreate() {
|
|||||||
if (!valid) return
|
if (!valid) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await createSite(form.value)
|
const res = await createSite(form.value)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success(`新增成功:${res.data?.site_key || ''}`)
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
resetForm()
|
resetForm()
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -32,9 +32,6 @@
|
|||||||
|
|
||||||
<el-dialog v-model="showDialog" title="新增系統" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增系統" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="System Key" prop="system_key">
|
|
||||||
<el-input v-model="form.system_key" placeholder="mkt" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="名稱" prop="name">
|
<el-form-item label="名稱" prop="name">
|
||||||
<el-input v-model="form.name" placeholder="行銷平台" />
|
<el-input v-model="form.name" placeholder="行銷平台" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -111,10 +108,9 @@ const formRef = ref()
|
|||||||
const showEditDialog = ref(false)
|
const showEditDialog = ref(false)
|
||||||
const savingEdit = ref(false)
|
const savingEdit = ref(false)
|
||||||
|
|
||||||
const form = ref({ system_key: '', name: '' })
|
const form = ref({ name: '' })
|
||||||
const editForm = ref({ system_key: '', name: '', status: 'active' })
|
const editForm = ref({ system_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
|
||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +138,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.value = { system_key: '', name: '' }
|
form.value = { name: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row) {
|
function openEdit(row) {
|
||||||
@@ -163,8 +159,8 @@ async function handleCreate() {
|
|||||||
if (!valid) return
|
if (!valid) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await createSystem(form.value)
|
const res = await createSystem(form.value)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success(`新增成功:${res.data?.system_key || ''}`)
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
resetForm()
|
resetForm()
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
Reference in New Issue
Block a user