mirror of
https://gitee.com/pnoker/iot-dc3.git
synced 2026-08-31 01:39:47 +08:00
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:
@@ -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",
|
||||
|
||||
Generated
+1
-19
@@ -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 } });
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -70,8 +70,13 @@ export type {
|
||||
ResourceRecord,
|
||||
ApiForm,
|
||||
ApiRecord,
|
||||
RoleUserBindForm,
|
||||
RolePrincipalBindForm,
|
||||
RoleResourceBindForm,
|
||||
McpClientRegistrationForm,
|
||||
OAuthClientRecord,
|
||||
McpConnectionForm,
|
||||
McpConnectionRecord,
|
||||
McpToolRecord,
|
||||
} from './auth';
|
||||
|
||||
export type {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user