feat(auth): align frontend with principal identity

Send raw passwords to the token endpoint, remove the direct js-md5 dependency, and switch role assignment screens from role-user bindings to role-principal bindings.
This commit is contained in:
pnoker
2026-06-12 21:21:52 +08:00
parent d139a05d99
commit 7ffbc540d3
13 changed files with 122 additions and 69 deletions
-1
View File
@@ -67,7 +67,6 @@
"highlight.js": "^11.11.1",
"js-base64": "^3.7.8",
"js-cookie": "^3.0.7",
"js-md5": "^0.8.3",
"json-bigint": "^1.0.0",
"marked": "^18.0.4",
"mitt": "^3.0.1",
+1 -19
View File
@@ -38,9 +38,6 @@ importers:
js-cookie:
specifier: ^3.0.7
version: 3.0.7
js-md5:
specifier: ^0.8.3
version: 0.8.3
json-bigint:
specifier: ^1.0.0
version: 1.0.0
@@ -1800,9 +1797,6 @@ packages:
resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==}
engines: {node: '>=20'}
js-md5@0.8.3:
resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==}
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
@@ -2304,10 +2298,6 @@ packages:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string-width@4.2.0:
resolution: {integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==}
engines: {node: '>=8'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -3026,7 +3016,7 @@ snapshots:
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.0
string-width-cjs: string-width@4.2.3
strip-ansi: 7.2.0
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
@@ -4372,8 +4362,6 @@ snapshots:
js-cookie@3.0.7: {}
js-md5@0.8.3: {}
js-tokens@10.0.0: {}
js-tokens@9.0.1: {}
@@ -4816,12 +4804,6 @@ snapshots:
string-argv@0.3.2: {}
string-width@4.2.0:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -17,22 +17,18 @@
import { httpGet, httpPost } from '@/api/common';
import { API_AUTH_BASE } from '@/config/constant/api';
import type { PageQuery } from '@/config/types';
import type { RoleUserBindForm } from '@/config/types/auth';
import type { RolePrincipalBindForm } from '@/config/types/auth';
export const addRoleUserBind = (body: RoleUserBindForm) => httpPost(`${API_AUTH_BASE}/role_user/add`, body);
export const addRolePrincipalBind = (body: RolePrincipalBindForm) =>
httpPost(`${API_AUTH_BASE}/role_principal/add`, body);
export const deleteRoleUserBind = (id: string) =>
httpPost(`${API_AUTH_BASE}/role_user/delete`, undefined, { params: { id } });
export const deleteRolePrincipalBind = (id: string) =>
httpPost(`${API_AUTH_BASE}/role_principal/delete`, undefined, { params: { id } });
export const listRoleUserBind = (query: PageQuery) => httpPost(`${API_AUTH_BASE}/role_user/list`, query);
export const listRolePrincipalBind = (query: PageQuery) => httpPost(`${API_AUTH_BASE}/role_principal/list`, query);
export const listRoleByUserId = (userId: string, tenantId?: string) => {
const params: Record<string, string> = { user_id: userId };
if (tenantId) {
params.tenant_id = tenantId;
}
return httpGet(`${API_AUTH_BASE}/role_user/list_role_by_user`, { params });
};
export const listRoleByPrincipalId = (principalId: string) =>
httpGet(`${API_AUTH_BASE}/role_principal/list_role_by_principal`, { params: { principal_id: principalId } });
export const listUserByRoleId = (roleId: string) =>
httpGet(`${API_AUTH_BASE}/role_user/list_user_by_role`, { params: { role_id: roleId } });
httpGet(`${API_AUTH_BASE}/role_principal/list_user_by_role`, { params: { role_id: roleId } });
+2 -2
View File
@@ -29,8 +29,8 @@ export const listRoleResourceBind = (query: PageQuery) => httpPost(`${API_AUTH_B
export const listResourceByRoleId = (roleId: string) =>
httpGet(`${API_AUTH_BASE}/role_resource/list_resource_by_role`, { params: { role_id: roleId } });
export const listResourceByUserId = (userId: string) =>
httpGet(`${API_AUTH_BASE}/role_resource/list_resource_by_user`, { params: { user_id: userId } });
export const listResourceByPrincipalId = (principalId: string) =>
httpGet(`${API_AUTH_BASE}/role_resource/list_resource_by_principal`, { params: { principal_id: principalId } });
export const listRoleByResourceId = (resourceId: string) =>
httpGet(`${API_AUTH_BASE}/role_resource/list_role_by_resource`, { params: { resource_id: resourceId } });
+73 -2
View File
@@ -22,6 +22,7 @@
export interface UserForm {
id?: string;
principalId?: string;
userName?: string;
nickName?: string;
phone?: string;
@@ -137,9 +138,10 @@ export interface ApiRecord extends ApiForm {
// ─── Bind payloads ───────────────────────────────────────────────────
export interface RoleUserBindForm {
export interface RolePrincipalBindForm {
roleId?: string;
userId?: string;
principalId?: string;
principalType?: 'USER' | 'SERVICE_ACCOUNT' | 'SYSTEM' | string;
[key: string]: unknown;
}
@@ -148,3 +150,72 @@ export interface RoleResourceBindForm {
resourceId?: string;
[key: string]: unknown;
}
// ─── MCP / OAuth ────────────────────────────────────────────────────
export interface McpClientRegistrationForm {
client_name?: string;
client_type?: 'PUBLIC' | 'CONFIDENTIAL' | string;
grant_types?: string[];
redirect_uris?: string[];
scope?: string[];
tenant_id?: string;
service_account_principal_id?: string;
[key: string]: unknown;
}
export interface OAuthClientRecord {
id: string;
clientId: string;
clientName: string;
clientType: string;
ownerPrincipalId?: string;
serviceAccountPrincipalId?: string;
tenantId?: string;
authorizationGrantTypes?: string;
redirectUris?: string;
scopes?: string;
enableFlag?: string | number;
[key: string]: unknown;
}
export interface McpConnectionForm {
connectionName?: string;
clientId?: string;
principalId?: string;
principalType?: 'USER' | 'SERVICE_ACCOUNT' | string;
tenantId?: string;
grantType?: 'authorization_code' | 'client_credentials' | string;
expireTime?: string;
remark?: string;
[key: string]: unknown;
}
export interface McpConnectionRecord extends McpConnectionForm {
id: string;
enableFlag?: string | number;
revokeTime?: string;
lastUsedTime?: string;
}
export interface McpToolRecord {
id: string;
toolId: string;
toolName: string;
toolTitle?: string;
toolCategory?: string;
serviceName?: string;
apiCode?: string;
permissionCode?: string;
httpMethod?: string;
apiPath?: string;
schemaHash?: string;
riskLevel?: 'LOW' | 'MEDIUM' | 'HIGH' | string;
readOnlyHint?: number;
destructiveHint?: number;
idempotentHint?: number;
openWorldHint?: number;
enableFlag?: string | number;
remark?: string;
[key: string]: unknown;
}
+6 -1
View File
@@ -70,8 +70,13 @@ export type {
ResourceRecord,
ApiForm,
ApiRecord,
RoleUserBindForm,
RolePrincipalBindForm,
RoleResourceBindForm,
McpClientRegistrationForm,
OAuthClientRecord,
McpConnectionForm,
McpConnectionRecord,
McpToolRecord,
} from './auth';
export type {
+1 -2
View File
@@ -27,7 +27,6 @@ import type { Login } from '@/config/types';
import { getStorage, removeStorage, setStorage } from '@/utils/storageUtil';
import { failMessage } from '@/utils/notificationUtil';
import { isNull } from '@/utils/validationUtil';
import { md5 } from 'js-md5';
interface LoginForm {
tenant: string;
@@ -81,7 +80,7 @@ export const useAuthStore = defineStore('auth', () => {
tenant: form.tenant,
name: form.name,
salt: salt,
password: md5.hex(form.password),
password: form.password,
};
const tokenRes = await generateToken(loginWithPassword);
@@ -87,7 +87,7 @@
import { getRoleById } from '@/api/role';
import { listResourceByRoleId } from '@/api/roleResourceBind';
import { listUserByRoleId } from '@/api/roleUserBind';
import { listUserByRoleId } from '@/api/rolePrincipalBind';
import { timestampLabel } from '@/utils/dateUtil';
import blankCard from '@/components/card/blank/BlankCard.vue';
@@ -151,7 +151,7 @@
import type { ElTable } from 'element-plus';
import { listRole } from '@/api/role';
import { listRoleByUserId, listRoleUserBind } from '@/api/roleUserBind';
import { listRoleByPrincipalId, listRolePrincipalBind } from '@/api/rolePrincipalBind';
interface RoleRow {
id: string;
@@ -162,7 +162,7 @@
const { t } = useI18n();
const emit = defineEmits<{
(e: 'save', userId: string, addIds: string[], removeBindIds: string[], done: () => void): void;
(e: 'save', principalId: string, addIds: string[], removeBindIds: string[], done: () => void): void;
}>();
const leftTableRef = ref<InstanceType<typeof ElTable>>();
@@ -173,7 +173,7 @@
loading: false,
submitting: false,
user: {} as any,
// bindId(RoleUserBind.id) -> roleId lookup; delete endpoint wants bindId
// bindId(RolePrincipalBind.id) -> roleId lookup; delete endpoint wants bindId
bindIdByRoleId: new Map<string, string>(),
originalRoleIds: [] as string[],
available: [] as RoleRow[],
@@ -207,10 +207,11 @@
const load = async () => {
reactiveData.loading = true;
try {
const principalId = String(reactiveData.user.principalId || '');
const [allRes, ownRes, bindsRes] = await Promise.all([
listRole({ page: { size: 1000, current: 1 } }) as Promise<any>,
listRoleByUserId(reactiveData.user.id) as Promise<any>,
listRoleUserBind({ page: { size: 1000, current: 1 }, userId: reactiveData.user.id }) as Promise<any>,
listRoleByPrincipalId(principalId) as Promise<any>,
listRolePrincipalBind({ page: { size: 1000, current: 1 }, principalId }) as Promise<any>,
]);
const allRoles: RoleRow[] = ((allRes.data?.records as any[]) || []).map(toRow);
@@ -287,7 +288,7 @@
}
reactiveData.submitting = true;
emit('save', String(reactiveData.user.id), addIds, removeBindIds, () => {
emit('save', String(reactiveData.user.principalId), addIds, removeBindIds, () => {
reactiveData.submitting = false;
reactiveData.visible = false;
});
+12 -10
View File
@@ -87,8 +87,8 @@
import type { TabsPaneContext } from 'element-plus';
import { useRoute, useRouter } from 'vue-router';
import { listResourceByUserId } from '@/api/roleResourceBind';
import { listRoleByUserId } from '@/api/roleUserBind';
import { listResourceByPrincipalId } from '@/api/roleResourceBind';
import { listRoleByPrincipalId } from '@/api/rolePrincipalBind';
import { getUserById } from '@/api/user';
import { timestampLabel } from '@/utils/dateUtil';
@@ -111,9 +111,11 @@
resourcesLoading: false,
});
const load = () => {
const principalId = () => String(reactiveData.data.principalId || '');
const load = async () => {
if (!reactiveData.id) return;
getUserById(reactiveData.id)
await getUserById(reactiveData.id)
.then((res: any) => {
reactiveData.data = res.data || {};
})
@@ -123,9 +125,9 @@
};
const loadRoles = () => {
if (!reactiveData.id || reactiveData.rolesLoaded) return;
if (!principalId() || reactiveData.rolesLoaded) return;
reactiveData.rolesLoading = true;
listRoleByUserId(reactiveData.id)
listRoleByPrincipalId(principalId())
.then((res: any) => {
reactiveData.roles = (res.data as any[]) || [];
reactiveData.rolesLoaded = true;
@@ -139,9 +141,9 @@
};
const loadResources = () => {
if (!reactiveData.id || reactiveData.resourcesLoaded) return;
if (!principalId() || reactiveData.resourcesLoaded) return;
reactiveData.resourcesLoading = true;
listResourceByUserId(reactiveData.id)
listResourceByPrincipalId(principalId())
.then((res: any) => {
reactiveData.resources = (res.data as any[]) || [];
reactiveData.resourcesLoaded = true;
@@ -161,8 +163,8 @@
if (name === 'resource') loadResources();
};
onMounted(() => {
load();
onMounted(async () => {
await load();
if (reactiveData.active === 'role') loadRoles();
if (reactiveData.active === 'resource') loadResources();
});
+4 -4
View File
@@ -19,7 +19,7 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { addUser, deleteUser, listUser, updateUser } from '@/api/user';
import { addRoleUserBind, deleteRoleUserBind } from '@/api/roleUserBind';
import { addRolePrincipalBind, deleteRolePrincipalBind } from '@/api/rolePrincipalBind';
import { usePagedList } from '@/composables/usePagedList';
import { timestampColumn } from '@/utils/dateUtil';
import { successMessage } from '@/utils/notificationUtil';
@@ -95,11 +95,11 @@ export default defineComponent({
});
};
const onAssignRoles = async (userId: string, addIds: string[], removeBindIds: string[], done: () => void) => {
const onAssignRoles = async (principalId: string, addIds: string[], removeBindIds: string[], done: () => void) => {
try {
await Promise.all([
...addIds.map((roleId) => addRoleUserBind({ userId, roleId })),
...removeBindIds.map((id) => deleteRoleUserBind(id)),
...addIds.map((roleId) => addRolePrincipalBind({ principalId, principalType: 'USER', roleId })),
...removeBindIds.map((id) => deleteRolePrincipalBind(id)),
]);
successMessage(t('settings.user.assignSaved'));
done();
+3 -5
View File
@@ -71,7 +71,7 @@ describe('auth store', () => {
});
describe('login', () => {
it('hashes password with salt, persists token, and routes to home', async () => {
it('sends the raw password over HTTPS, persists token, and routes to home', async () => {
const store = useAuthStore();
await store.login({
tenant: TEST_CREDENTIALS.tenant,
@@ -79,7 +79,7 @@ describe('auth store', () => {
password: TEST_CREDENTIALS.password,
});
// Salt request first, then token request with hashed password.
// Salt request first, then token request with the raw password.
expect(tokenMocks.generateSalt).toHaveBeenCalledTimes(1);
expect(tokenMocks.generateSalt).toHaveBeenCalledWith({
tenant: TEST_CREDENTIALS.tenant,
@@ -91,9 +91,7 @@ describe('auth store', () => {
expect(tokenPayload.tenant).toBe(TEST_CREDENTIALS.tenant);
expect(tokenPayload.name).toBe(TEST_CREDENTIALS.name);
expect(tokenPayload.salt).toBe(TEST_CREDENTIALS.salt);
// password must be md5(md5(plain) + salt) — never the plaintext.
expect(tokenPayload.password).not.toBe(TEST_CREDENTIALS.password);
expect(tokenPayload.password).toMatch(/^[a-f0-9]{32}$/);
expect(tokenPayload.password).toBe(TEST_CREDENTIALS.password);
// Storage now holds the credential triple.
expect(getStorage(AUTH_HEADERS.TENANT)).toBe(TEST_CREDENTIALS.tenant);
+3 -3
View File
@@ -27,9 +27,9 @@ const userMocks = vi.hoisted(() => ({
}));
vi.mock('@/api/user', () => userMocks);
vi.mock('@/api/roleUserBind', () => ({
addRoleUserBind: vi.fn(() => Promise.resolve({ data: true })),
deleteRoleUserBind: vi.fn(() => Promise.resolve({ data: true })),
vi.mock('@/api/rolePrincipalBind', () => ({
addRolePrincipalBind: vi.fn(() => Promise.resolve({ data: true })),
deleteRolePrincipalBind: vi.fn(() => Promise.resolve({ data: true })),
}));
vi.mock('@/utils/notificationUtil', () => ({ failMessage: vi.fn(), successMessage: vi.fn() }));