feat(iam): 实现 RBAC + 视口配置 + DataScope
- 新增 iam_role_viewports 表(4 层视口模型:L1 导航/L2 路由/L3 组件/L4 数据) - iam_users 添加 data_scope 字段(6 级:self/class/grade/school/district/all) - JWT payload 包含 dataScope,注册时自动分配 teacher 角色 - 新增 getEffectivePermissions(多角色权限去重) - 新增 getUserViewports(按权限过滤视口) - 新增 RbacController:GET /iam/viewports, /iam/permissions/effective, /iam/roles, /iam/permissions - 添加 7 个权限点 + 12 条角色权限映射 + 7 个视口种子数据
This commit is contained in:
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS `iam_users` (
|
|||||||
`password_hash` VARCHAR(255) NOT NULL,
|
`password_hash` VARCHAR(255) NOT NULL,
|
||||||
`name` VARCHAR(100) NOT NULL,
|
`name` VARCHAR(100) NOT NULL,
|
||||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active',
|
`status` VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||||
|
`data_scope` ENUM('self','class','grade','school','district','all') NOT NULL DEFAULT 'self',
|
||||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
@@ -52,7 +53,63 @@ CREATE TABLE IF NOT EXISTS `iam_refresh_tokens` (
|
|||||||
INDEX `idx_iam_refresh_tokens_user_id` (`user_id`)
|
INDEX `idx_iam_refresh_tokens_user_id` (`user_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 种子数据:默认 teacher 角色
|
-- 视口配置表(4 层模型:L1 导航 / L2 路由 / L3 组件 / L4 数据)
|
||||||
|
CREATE TABLE IF NOT EXISTS `iam_role_viewports` (
|
||||||
|
`id` CHAR(36) NOT NULL,
|
||||||
|
`role_id` CHAR(36) NOT NULL,
|
||||||
|
`viewport_key` VARCHAR(50) NOT NULL,
|
||||||
|
`label` VARCHAR(100) NOT NULL,
|
||||||
|
`route` VARCHAR(200) NOT NULL,
|
||||||
|
`icon` VARCHAR(50) NULL,
|
||||||
|
`sort_order` VARCHAR(10) NOT NULL DEFAULT '0',
|
||||||
|
`required_permission` VARCHAR(100) NULL,
|
||||||
|
`component_config` TEXT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_iam_role_viewports_role_id` (`role_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 种子数据:默认角色
|
||||||
INSERT IGNORE INTO `iam_roles` (`id`, `name`, `description`) VALUES
|
INSERT IGNORE INTO `iam_roles` (`id`, `name`, `description`) VALUES
|
||||||
('00000000-0000-0000-0000-000000000001', 'teacher', '教师角色'),
|
('00000000-0000-0000-0000-000000000001', 'teacher', '教师角色'),
|
||||||
('00000000-0000-0000-0000-000000000002', 'admin', '管理员角色');
|
('00000000-0000-0000-0000-000000000002', 'admin', '管理员角色');
|
||||||
|
|
||||||
|
-- 种子数据:权限点(固定 UUID 便于 role_permissions 引用)
|
||||||
|
INSERT IGNORE INTO `iam_permissions` (`id`, `name`, `resource`, `action`) VALUES
|
||||||
|
('00000000-0000-0000-0000-000000000101', 'classes:read', 'classes', 'read'),
|
||||||
|
('00000000-0000-0000-0000-000000000102', 'classes:create', 'classes', 'create'),
|
||||||
|
('00000000-0000-0000-0000-000000000103', 'classes:update', 'classes', 'update'),
|
||||||
|
('00000000-0000-0000-0000-000000000104', 'classes:delete', 'classes', 'delete'),
|
||||||
|
('00000000-0000-0000-0000-000000000201', 'iam:user:read', 'iam', 'user:read'),
|
||||||
|
('00000000-0000-0000-0000-000000000202', 'iam:user:manage', 'iam', 'user:manage'),
|
||||||
|
('00000000-0000-0000-0000-000000000203', 'iam:role:manage', 'iam', 'role:manage');
|
||||||
|
|
||||||
|
-- 种子数据:teacher 角色权限(classes CRUD + user:read)
|
||||||
|
INSERT IGNORE INTO `iam_role_permissions` (`role_id`, `permission_id`) VALUES
|
||||||
|
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000101'),
|
||||||
|
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000102'),
|
||||||
|
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000103'),
|
||||||
|
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000104'),
|
||||||
|
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000201');
|
||||||
|
|
||||||
|
-- 种子数据:admin 角色权限(全部权限)
|
||||||
|
INSERT IGNORE INTO `iam_role_permissions` (`role_id`, `permission_id`) VALUES
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000101'),
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000102'),
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000103'),
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000104'),
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000201'),
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000202'),
|
||||||
|
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000203');
|
||||||
|
|
||||||
|
-- 种子数据:teacher 角色视口(L1 导航)
|
||||||
|
INSERT IGNORE INTO `iam_role_viewports` (`id`, `role_id`, `viewport_key`, `label`, `route`, `icon`, `sort_order`, `required_permission`) VALUES
|
||||||
|
('00000000-0000-0000-0000-000000000a01', '00000000-0000-0000-0000-000000000001', 'dashboard', '仪表盘', '/dashboard', 'home', '0', NULL),
|
||||||
|
('00000000-0000-0000-0000-000000000a02', '00000000-0000-0000-0000-000000000001', 'classes', '班级管理', '/classes', 'users', '1', 'classes:read'),
|
||||||
|
('00000000-0000-0000-0000-000000000a03', '00000000-0000-0000-0000-000000000001', 'profile', '个人中心', '/profile', 'user', '9', NULL);
|
||||||
|
|
||||||
|
-- 种子数据:admin 角色视口(L1 导航)
|
||||||
|
INSERT IGNORE INTO `iam_role_viewports` (`id`, `role_id`, `viewport_key`, `label`, `route`, `icon`, `sort_order`, `required_permission`) VALUES
|
||||||
|
('00000000-0000-0000-0000-000000000b01', '00000000-0000-0000-0000-000000000002', 'dashboard', '仪表盘', '/dashboard', 'home', '0', NULL),
|
||||||
|
('00000000-0000-0000-0000-000000000b02', '00000000-0000-0000-0000-000000000002', 'classes', '班级管理', '/classes', 'users', '1', 'classes:read'),
|
||||||
|
('00000000-0000-0000-0000-000000000b03', '00000000-0000-0000-0000-000000000002', 'iam', '用户管理', '/iam/users', 'shield', '2', 'iam:user:read'),
|
||||||
|
('00000000-0000-0000-0000-000000000b04', '00000000-0000-0000-0000-000000000002', 'profile', '个人中心', '/profile', 'user', '9', NULL);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { IamController } from "./iam.controller.js";
|
import { IamController } from "./iam.controller.js";
|
||||||
|
import { RbacController } from "./rbac.controller.js";
|
||||||
import { IamService } from "./iam.service.js";
|
import { IamService } from "./iam.service.js";
|
||||||
import { IamRepository } from "./iam.repository.js";
|
import { IamRepository } from "./iam.repository.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [IamController],
|
controllers: [IamController, RbacController],
|
||||||
providers: [IamService, IamRepository],
|
providers: [IamService, IamRepository],
|
||||||
})
|
})
|
||||||
export class IamModule {}
|
export class IamModule {}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
permissions,
|
permissions,
|
||||||
rolePermissions,
|
rolePermissions,
|
||||||
refreshTokens,
|
refreshTokens,
|
||||||
|
roleViewports,
|
||||||
} from "./iam.schema.js";
|
} from "./iam.schema.js";
|
||||||
import type { User, Role, Permission } from "./iam.schema.js";
|
import type { User, Role, Permission, RoleViewport } from "./iam.schema.js";
|
||||||
|
|
||||||
export class IamRepository {
|
export class IamRepository {
|
||||||
async createUser(data: {
|
async createUser(data: {
|
||||||
@@ -70,6 +71,39 @@ export class IamRepository {
|
|||||||
return result.map((r) => r.iam_permissions);
|
return result.map((r) => r.iam_permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserViewports(userId: string): Promise<RoleViewport[]> {
|
||||||
|
const db = getDb();
|
||||||
|
const userRoleRows = await db
|
||||||
|
.select()
|
||||||
|
.from(userRoles)
|
||||||
|
.where(eq(userRoles.userId, userId));
|
||||||
|
const roleIds = userRoleRows.map((r) => r.roleId);
|
||||||
|
if (roleIds.length === 0) return [];
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(roleViewports)
|
||||||
|
.where(inArray(roleViewports.roleId, roleIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserDataScope(userId: string): Promise<string> {
|
||||||
|
const db = getDb();
|
||||||
|
const [user] = await db
|
||||||
|
.select({ dataScope: users.dataScope })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId));
|
||||||
|
return user?.dataScope ?? "self";
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllRoles(): Promise<Role[]> {
|
||||||
|
const db = getDb();
|
||||||
|
return db.select().from(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllPermissions(): Promise<Permission[]> {
|
||||||
|
const db = getDb();
|
||||||
|
return db.select().from(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
async assignRole(userId: string, roleId: string): Promise<void> {
|
async assignRole(userId: string, roleId: string): Promise<void> {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
await db.insert(userRoles).values({ userId, roleId });
|
await db.insert(userRoles).values({ userId, roleId });
|
||||||
|
|||||||
@@ -1,48 +1,81 @@
|
|||||||
import { mysqlTable, varchar, char, timestamp } from 'drizzle-orm/mysql-core';
|
import {
|
||||||
|
mysqlTable,
|
||||||
|
varchar,
|
||||||
|
char,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
mysqlEnum,
|
||||||
|
} from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
export const users = mysqlTable('iam_users', {
|
export const users = mysqlTable("iam_users", {
|
||||||
id: char('id', { length: 36 }).notNull().primaryKey(),
|
id: char("id", { length: 36 }).notNull().primaryKey(),
|
||||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
email: varchar("email", { length: 255 }).notNull().unique(),
|
||||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
||||||
name: varchar('name', { length: 100 }).notNull(),
|
name: varchar("name", { length: 100 }).notNull(),
|
||||||
status: varchar('status', { length: 20 }).notNull().default('active'),
|
status: varchar("status", { length: 20 }).notNull().default("active"),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
dataScope: mysqlEnum("data_scope", [
|
||||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
|
"self",
|
||||||
|
"class",
|
||||||
|
"grade",
|
||||||
|
"school",
|
||||||
|
"district",
|
||||||
|
"all",
|
||||||
|
])
|
||||||
|
.notNull()
|
||||||
|
.default("self"),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roles = mysqlTable('iam_roles', {
|
export const roles = mysqlTable("iam_roles", {
|
||||||
id: char('id', { length: 36 }).notNull().primaryKey(),
|
id: char("id", { length: 36 }).notNull().primaryKey(),
|
||||||
name: varchar('name', { length: 50 }).notNull().unique(),
|
name: varchar("name", { length: 50 }).notNull().unique(),
|
||||||
description: varchar('description', { length: 255 }),
|
description: varchar("description", { length: 255 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userRoles = mysqlTable('iam_user_roles', {
|
export const userRoles = mysqlTable("iam_user_roles", {
|
||||||
userId: char('user_id', { length: 36 }).notNull(),
|
userId: char("user_id", { length: 36 }).notNull(),
|
||||||
roleId: char('role_id', { length: 36 }).notNull(),
|
roleId: char("role_id", { length: 36 }).notNull(),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const permissions = mysqlTable('iam_permissions', {
|
export const permissions = mysqlTable("iam_permissions", {
|
||||||
id: char('id', { length: 36 }).notNull().primaryKey(),
|
id: char("id", { length: 36 }).notNull().primaryKey(),
|
||||||
name: varchar('name', { length: 100 }).notNull().unique(),
|
name: varchar("name", { length: 100 }).notNull().unique(),
|
||||||
resource: varchar('resource', { length: 50 }).notNull(),
|
resource: varchar("resource", { length: 50 }).notNull(),
|
||||||
action: varchar('action', { length: 50 }).notNull(),
|
action: varchar("action", { length: 50 }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rolePermissions = mysqlTable('iam_role_permissions', {
|
export const rolePermissions = mysqlTable("iam_role_permissions", {
|
||||||
roleId: char('role_id', { length: 36 }).notNull(),
|
roleId: char("role_id", { length: 36 }).notNull(),
|
||||||
permissionId: char('permission_id', { length: 36 }).notNull(),
|
permissionId: char("permission_id", { length: 36 }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const refreshTokens = mysqlTable('iam_refresh_tokens', {
|
export const refreshTokens = mysqlTable("iam_refresh_tokens", {
|
||||||
id: char('id', { length: 36 }).notNull().primaryKey(),
|
id: char("id", { length: 36 }).notNull().primaryKey(),
|
||||||
userId: char('user_id', { length: 36 }).notNull(),
|
userId: char("user_id", { length: 36 }).notNull(),
|
||||||
tokenHash: varchar('token_hash', { length: 255 }).notNull(),
|
tokenHash: varchar("token_hash", { length: 255 }).notNull(),
|
||||||
expiresAt: timestamp('expires_at').notNull(),
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
revokedAt: timestamp('revoked_at'),
|
revokedAt: timestamp("revoked_at"),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 视口配置表(4 层模型:L1 导航 / L2 路由 / L3 组件 / L4 数据)
|
||||||
|
// 每条记录代表一个角色可见的导航项
|
||||||
|
export const roleViewports = mysqlTable("iam_role_viewports", {
|
||||||
|
id: char("id", { length: 36 }).notNull().primaryKey(),
|
||||||
|
roleId: char("role_id", { length: 36 }).notNull(),
|
||||||
|
viewportKey: varchar("viewport_key", { length: 50 }).notNull(),
|
||||||
|
label: varchar("label", { length: 100 }).notNull(),
|
||||||
|
route: varchar("route", { length: 200 }).notNull(),
|
||||||
|
icon: varchar("icon", { length: 50 }),
|
||||||
|
sortOrder: varchar("sort_order", { length: 10 }).notNull().default("0"),
|
||||||
|
requiredPermission: varchar("required_permission", { length: 100 }),
|
||||||
|
// L3 组件级配置(JSON):控制组件内按钮/操作的显隐
|
||||||
|
componentConfig: text("component_config"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
export type Role = typeof roles.$inferSelect;
|
export type Role = typeof roles.$inferSelect;
|
||||||
export type Permission = typeof permissions.$inferSelect;
|
export type Permission = typeof permissions.$inferSelect;
|
||||||
|
export type RoleViewport = typeof roleViewports.$inferSelect;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "../shared/errors/application-error.js";
|
} from "../shared/errors/application-error.js";
|
||||||
import { env } from "../config/env.js";
|
import { env } from "../config/env.js";
|
||||||
import type { RegisterDto, LoginDto } from "./iam.dto.js";
|
import type { RegisterDto, LoginDto } from "./iam.dto.js";
|
||||||
import type { User } from "./iam.schema.js";
|
import type { User, Role, Permission } from "./iam.schema.js";
|
||||||
|
|
||||||
export interface TokenPair {
|
export interface TokenPair {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -24,8 +24,21 @@ export interface UserInfo {
|
|||||||
name: string;
|
name: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
dataScope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewportItem {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
route: string;
|
||||||
|
icon: string | null;
|
||||||
|
sortOrder: string;
|
||||||
|
requiredPermission: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// teacher 角色固定 ID(种子数据)
|
||||||
|
const TEACHER_ROLE_ID = "00000000-0000-0000-0000-000000000001";
|
||||||
|
|
||||||
export class IamService {
|
export class IamService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IamRepository) private readonly repository: IamRepository,
|
@Inject(IamRepository) private readonly repository: IamRepository,
|
||||||
@@ -48,8 +61,8 @@ export class IamService {
|
|||||||
name: dto.name,
|
name: dto.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// P2 骨架:默认分配 teacher 角色(实际应通过邀请码注册)
|
// 默认分配 teacher 角色
|
||||||
// await this.repository.assignRole(userId, defaultRoleId);
|
await this.repository.assignRole(userId, TEACHER_ROLE_ID);
|
||||||
|
|
||||||
const { tokens } = await this.issueTokens(user);
|
const { tokens } = await this.issueTokens(user);
|
||||||
const info = await this.buildUserInfo(user);
|
const info = await this.buildUserInfo(user);
|
||||||
@@ -104,12 +117,55 @@ export class IamService {
|
|||||||
return this.buildUserInfo(user);
|
return this.buildUserInfo(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 有效权限聚合:多角色权限去重
|
||||||
|
async getEffectivePermissions(userId: string): Promise<string[]> {
|
||||||
|
const perms = await this.repository.getUserPermissions(userId);
|
||||||
|
const unique = new Set(perms.map((p) => p.name));
|
||||||
|
return [...unique];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserViewports(userId: string): Promise<ViewportItem[]> {
|
||||||
|
const viewports = await this.repository.getUserViewports(userId);
|
||||||
|
const permissions = await this.getEffectivePermissions(userId);
|
||||||
|
|
||||||
|
// 过滤:如果视口需要权限且用户不具备,则不返回
|
||||||
|
return viewports
|
||||||
|
.filter((vp) => {
|
||||||
|
if (!vp.requiredPermission) return true;
|
||||||
|
return permissions.includes(vp.requiredPermission);
|
||||||
|
})
|
||||||
|
.map((vp) => ({
|
||||||
|
key: vp.viewportKey,
|
||||||
|
label: vp.label,
|
||||||
|
route: vp.route,
|
||||||
|
icon: vp.icon,
|
||||||
|
sortOrder: vp.sortOrder,
|
||||||
|
requiredPermission: vp.requiredPermission,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.sortOrder.localeCompare(b.sortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllRoles(): Promise<Role[]> {
|
||||||
|
return this.repository.getAllRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllPermissions(): Promise<Permission[]> {
|
||||||
|
return this.repository.getAllPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
private async issueTokens(user: User): Promise<{ tokens: TokenPair }> {
|
private async issueTokens(user: User): Promise<{ tokens: TokenPair }> {
|
||||||
const roles = await this.repository.getUserRoles(user.id);
|
const roles = await this.repository.getUserRoles(user.id);
|
||||||
const roleNames = roles.map((r) => r.name);
|
const roleNames = roles.map((r) => r.name);
|
||||||
|
const dataScope = user.dataScope;
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{ sub: user.id, email: user.email, roles: roleNames, type: "access" },
|
{
|
||||||
|
sub: user.id,
|
||||||
|
email: user.email,
|
||||||
|
roles: roleNames,
|
||||||
|
dataScope,
|
||||||
|
type: "access",
|
||||||
|
},
|
||||||
env.JWT_SECRET,
|
env.JWT_SECRET,
|
||||||
{ issuer: env.JWT_ISSUER, audience: env.JWT_AUDIENCE, expiresIn: "15m" },
|
{ issuer: env.JWT_ISSUER, audience: env.JWT_AUDIENCE, expiresIn: "15m" },
|
||||||
);
|
);
|
||||||
@@ -147,6 +203,7 @@ export class IamService {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
roles: roles.map((r) => r.name),
|
roles: roles.map((r) => r.name),
|
||||||
permissions: permissions.map((p) => p.name),
|
permissions: permissions.map((p) => p.name),
|
||||||
|
dataScope: user.dataScope,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
services/iam/src/iam/rbac.controller.ts
Normal file
46
services/iam/src/iam/rbac.controller.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Controller, Get, Req } from "@nestjs/common";
|
||||||
|
import type { Request } from "express";
|
||||||
|
import { IamService } from "./iam.service.js";
|
||||||
|
import { UnauthorizedError } from "../shared/errors/application-error.js";
|
||||||
|
|
||||||
|
// RBAC 管理端点:角色/权限/视口查询
|
||||||
|
@Controller("iam")
|
||||||
|
export class RbacController {
|
||||||
|
constructor(private readonly service: IamService) {}
|
||||||
|
|
||||||
|
// 获取当前用户的视口配置(L1 导航)
|
||||||
|
@Get("viewports")
|
||||||
|
async viewports(@Req() req: Request) {
|
||||||
|
const userId = req.headers["x-user-id"] as string;
|
||||||
|
if (!userId) {
|
||||||
|
throw new UnauthorizedError("Missing x-user-id header");
|
||||||
|
}
|
||||||
|
const data = await this.service.getUserViewports(userId);
|
||||||
|
return { success: true as const, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户的有效权限
|
||||||
|
@Get("permissions/effective")
|
||||||
|
async effectivePermissions(@Req() req: Request) {
|
||||||
|
const userId = req.headers["x-user-id"] as string;
|
||||||
|
if (!userId) {
|
||||||
|
throw new UnauthorizedError("Missing x-user-id header");
|
||||||
|
}
|
||||||
|
const permissions = await this.service.getEffectivePermissions(userId);
|
||||||
|
return { success: true as const, data: { permissions } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列出所有角色(管理端用)
|
||||||
|
@Get("roles")
|
||||||
|
async roles() {
|
||||||
|
const data = await this.service.getAllRoles();
|
||||||
|
return { success: true as const, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列出所有权限点(管理端用)
|
||||||
|
@Get("permissions")
|
||||||
|
async permissions() {
|
||||||
|
const data = await this.service.getAllPermissions();
|
||||||
|
return { success: true as const, data };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user