const db = require('../plugin/DataBase/db') const Redis = require('../plugin/DataBase/Redis') const { DEFAULT_PERMISSION_POINTS, DEFAULT_PERMISSION_RESOURCE_RULES, DEFAULT_BASIC_USER_PERMISSION_CODES, LEGACY_ROLE_PERMISSION_MAP } = require('./PermissionCatalog') class AccessControl { constructor() { this.schemaReady = false } parseArray(value) { if (Array.isArray(value)) return value if (!value) return [] if (typeof value !== 'string') return [] try { const parsed = JSON.parse(value) return Array.isArray(parsed) ? parsed : [] } catch (_) { return [] } } normalizeCodes(codes) { if (!codes) return [] const list = Array.isArray(codes) ? codes : [codes] return [...new Set(list.map(code => String(code || '').trim()).filter(Boolean))] } async ensurePermissionSchema() { if (this.schemaReady) return await db.query(` CREATE TABLE IF NOT EXISTS permission_points ( id INT NOT NULL AUTO_INCREMENT, code VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL, category VARCHAR(40) NOT NULL DEFAULT 'action', scope_type VARCHAR(40) NOT NULL DEFAULT 'action', page_route_name VARCHAR(120) DEFAULT NULL, enabled TINYINT NOT NULL DEFAULT 1, remark VARCHAR(255) DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_permission_points_code (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) await db.query(` CREATE TABLE IF NOT EXISTS user_permission_points ( id INT NOT NULL AUTO_INCREMENT, user_uuid VARCHAR(64) NOT NULL, permission_code VARCHAR(120) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_user_permission (user_uuid, permission_code), KEY idx_user_permission_user_uuid (user_uuid), KEY idx_user_permission_code (permission_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) await db.query(` CREATE TABLE IF NOT EXISTS user_basic_permission_denials ( id INT NOT NULL AUTO_INCREMENT, user_uuid VARCHAR(64) NOT NULL, permission_code VARCHAR(120) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_user_basic_denial (user_uuid, permission_code), KEY idx_user_basic_denial_user_uuid (user_uuid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) await db.query(` CREATE TABLE IF NOT EXISTS permission_resource_rules ( id INT NOT NULL AUTO_INCREMENT, resource_type VARCHAR(40) NOT NULL, resource_key VARCHAR(180) NOT NULL, api_method VARCHAR(16) DEFAULT NULL, api_path VARCHAR(180) DEFAULT NULL, required_codes TEXT NOT NULL, enabled TINYINT NOT NULL DEFAULT 1, remark VARCHAR(255) DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_permission_resource (resource_type, resource_key), KEY idx_permission_resource_api (api_method, api_path) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `) for (const point of DEFAULT_PERMISSION_POINTS) { await db.query( `INSERT INTO permission_points (code, name, category, scope_type, page_route_name, enabled, remark) VALUES (?, ?, ?, ?, ?, 1, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), category = VALUES(category), scope_type = VALUES(scope_type), page_route_name = VALUES(page_route_name), remark = VALUES(remark)`, [point.code, point.name, point.category, point.scope_type, point.page_route_name || null, point.remark || ''] ) } for (const rule of DEFAULT_PERMISSION_RESOURCE_RULES) { const requiredCodes = JSON.stringify(this.normalizeCodes(rule.required_codes)) await db.query( `INSERT INTO permission_resource_rules (resource_type, resource_key, api_method, api_path, required_codes, enabled, remark) VALUES (?, ?, ?, ?, ?, 1, ?) ON DUPLICATE KEY UPDATE api_method = VALUES(api_method), api_path = VALUES(api_path), remark = VALUES(remark)`, [ rule.resource_type, rule.resource_key, rule.api_method || null, rule.api_path || null, requiredCodes, rule.remark || '' ] ) } this.schemaReady = true } async checkSession(uuid, session) { return (await Redis.get(`userSession:${uuid}`)) === session } async isBanned(uuid) { const sql = 'SELECT COALESCE(is_banned, 0) AS is_banned FROM users WHERE uuid = ?' const rows = await db.query(sql, [uuid]) return Number(rows[0]?.is_banned) === 1 } async invalidateSession(uuid) { await Redis.del(`userSession:${uuid}`) } async getPermission(uuid) { const sql = 'SELECT permission FROM users WHERE uuid = ?' const rows = await db.query(sql, [uuid]) return this.parseArray(rows?.[0]?.permission) } isSuperPermission(permission) { return this.parseArray(permission).includes('admin') } async isSuperAdmin(uuid) { const permission = await this.getPermission(uuid) return this.isSuperPermission(permission) } async getPermissionPoints() { await this.ensurePermissionSchema() const rows = await db.query(` SELECT id, code, name, category, scope_type, page_route_name, enabled, remark FROM permission_points ORDER BY category, id `) return rows || [] } async getUserDirectPermissionCodes(uuid) { await this.ensurePermissionSchema() const rows = await db.query( `SELECT permission_code FROM user_permission_points WHERE user_uuid = ? ORDER BY permission_code`, [uuid] ) return (rows || []).map(row => row.permission_code) } async getUserDeniedBasicPermissionCodes(uuid) { await this.ensurePermissionSchema() const rows = await db.query( `SELECT permission_code FROM user_basic_permission_denials WHERE user_uuid = ? ORDER BY permission_code`, [uuid] ) const denied = (rows || []).map(row => row.permission_code) const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES) return this.normalizeCodes(denied.filter(code => basicSet.has(code))) } getEnabledBasicPermissionCodes(deniedBasicCodes = []) { const deniedSet = new Set(this.normalizeCodes(deniedBasicCodes)) return DEFAULT_BASIC_USER_PERMISSION_CODES.filter(code => !deniedSet.has(code)) } async getUserPermissionCodes(uuid) { await this.ensurePermissionSchema() const legacyRoles = await this.getPermission(uuid) const directCodes = await this.getUserDirectPermissionCodes(uuid) const deniedBasicCodes = await this.getUserDeniedBasicPermissionCodes(uuid) const roleCodes = legacyRoles.flatMap(role => LEGACY_ROLE_PERMISSION_MAP[role] || []) return this.normalizeCodes([ ...this.getEnabledBasicPermissionCodes(deniedBasicCodes), ...legacyRoles, ...roleCodes, ...directCodes ]) } async setUserPermissionCodes(uuid, codes) { await this.ensurePermissionSchema() const permissionCodes = this.normalizeCodes(codes) const points = await this.getPermissionPoints() const validCodes = new Set(points.map(point => point.code)) const invalidCodes = permissionCodes.filter(code => !validCodes.has(code)) if (invalidCodes.length > 0) throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`) const conn = await db.connect() await conn.beginTransaction() try { await conn.execute(`DELETE FROM user_permission_points WHERE user_uuid = ?`, [uuid]) for (const code of permissionCodes) { await conn.execute( `INSERT INTO user_permission_points (user_uuid, permission_code) VALUES (?, ?)`, [uuid, code] ) } await conn.commit() } catch (error) { try { await conn.rollback() } catch (_) { } throw error } } async setUserDeniedBasicPermissionCodes(uuid, codes) { await this.ensurePermissionSchema() const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES) const deniedCodes = this.normalizeCodes(codes).filter(code => basicSet.has(code)) const invalidCodes = this.normalizeCodes(codes).filter(code => !basicSet.has(code)) if (invalidCodes.length > 0) throw new Error(`仅可关闭基础权限:${invalidCodes.join(', ')}`) const conn = await db.connect() await conn.beginTransaction() try { await conn.execute(`DELETE FROM user_basic_permission_denials WHERE user_uuid = ?`, [uuid]) for (const code of deniedCodes) { await conn.execute( `INSERT INTO user_basic_permission_denials (user_uuid, permission_code) VALUES (?, ?)`, [uuid, code] ) } await conn.commit() } catch (error) { try { await conn.rollback() } catch (_) { } throw error } } async getResourceRules() { await this.ensurePermissionSchema() const rows = await db.query(` SELECT id, resource_type, resource_key, api_method, api_path, required_codes, enabled, remark FROM permission_resource_rules ORDER BY FIELD(resource_type, 'page', 'action', 'api'), id `) return (rows || []).map(row => ({ ...row, required_codes: this.parseArray(row.required_codes) })) } async getResourceRequiredCodes({ resourceType, resourceKey, method, path }) { await this.ensurePermissionSchema() let rows = [] if (resourceType && resourceKey) { rows = await db.query( `SELECT required_codes FROM permission_resource_rules WHERE resource_type = ? AND resource_key = ? AND enabled = 1 LIMIT 1`, [resourceType, resourceKey] ) } else if (method && path) { rows = await db.query( `SELECT required_codes FROM permission_resource_rules WHERE resource_type = 'api' AND api_method = ? AND api_path = ? AND enabled = 1 LIMIT 1`, [String(method).toUpperCase(), path] ) } return this.parseArray(rows?.[0]?.required_codes) } async updateResourceRule({ id, required_codes, enabled }) { await this.ensurePermissionSchema() const requiredCodes = this.normalizeCodes(required_codes) const points = await this.getPermissionPoints() const validCodes = new Set(points.map(point => point.code)) const invalidCodes = requiredCodes.filter(code => !validCodes.has(code)) if (invalidCodes.length > 0) throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`) const rows = await db.query( `UPDATE permission_resource_rules SET required_codes = ?, enabled = ? WHERE id = ?`, [JSON.stringify(requiredCodes), Number(enabled) === 0 ? 0 : 1, id] ) return rows?.affectedRows === 1 } async canAccess(uuid, requiredCodes) { const codes = this.normalizeCodes(requiredCodes) if (codes.length === 0) return true if (await this.isSuperAdmin(uuid)) return true const userCodes = await this.getUserPermissionCodes(uuid) return codes.some(code => userCodes.includes(code)) } async checkJwAccount(uuid, username) { const sql = 'SELECT password FROM jw_account WHERE create_user = ? AND state = 1 AND username = ?' const rows = await db.query(sql, [uuid, username]); if (!rows || rows.length !== 1 || !rows[0].password) return false return rows[0]?.password } } module.exports = new AccessControl();