update
This commit is contained in:
@@ -6,6 +6,7 @@ import userRouter from "../../modules/user/routes/user.router";
|
|||||||
import reportRouter from "../../modules/report/routes/report.router";
|
import reportRouter from "../../modules/report/routes/report.router";
|
||||||
import authMiddleware from "../middleware/auth.middleware";
|
import authMiddleware from "../middleware/auth.middleware";
|
||||||
import RoleRouter from "../../modules/roles/role.routes";
|
import RoleRouter from "../../modules/roles/role.routes";
|
||||||
|
import PermissionRouter from "../../modules/permission/permission.routes";
|
||||||
|
|
||||||
const mainRouter = router.Router();
|
const mainRouter = router.Router();
|
||||||
|
|
||||||
@@ -15,5 +16,6 @@ mainRouter.use("/ticket", authMiddleware, ticketRouter);
|
|||||||
mainRouter.use("/user", authMiddleware, userRouter);
|
mainRouter.use("/user", authMiddleware, userRouter);
|
||||||
mainRouter.use("/report", authMiddleware, reportRouter);
|
mainRouter.use("/report", authMiddleware, reportRouter);
|
||||||
mainRouter.use("/role", authMiddleware, RoleRouter);
|
mainRouter.use("/role", authMiddleware, RoleRouter);
|
||||||
|
mainRouter.use("/permission", authMiddleware, PermissionRouter);
|
||||||
|
|
||||||
export default mainRouter;
|
export default mainRouter;
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
import {
|
import {
|
||||||
Model,
|
Model,
|
||||||
DataTypes,
|
DataTypes,
|
||||||
Optional,
|
Optional,
|
||||||
Sequelize,
|
Sequelize,
|
||||||
BelongsToManySetAssociationsMixin,
|
BelongsToManySetAssociationsMixin,
|
||||||
BelongsToManyAddAssociationsMixin,
|
BelongsToManyAddAssociationsMixin,
|
||||||
BelongsToManyRemoveAssociationsMixin,
|
BelongsToManyRemoveAssociationsMixin,
|
||||||
BelongsToManyGetAssociationsMixin,
|
BelongsToManyGetAssociationsMixin,
|
||||||
HasManyGetAssociationsMixin
|
HasManyGetAssociationsMixin
|
||||||
} from "sequelize";
|
} from "sequelize";
|
||||||
import User from "./User";
|
import User from "./User";
|
||||||
import Permission from "./Permission";
|
import Permission from "./Permission";
|
||||||
|
|
||||||
|
|
||||||
interface RoleAttributes {
|
interface RoleAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
}
|
|
||||||
|
|
||||||
interface RoleCreationAttributes
|
|
||||||
extends Optional<RoleAttributes, "id" | "description"> {}
|
|
||||||
|
|
||||||
class Role
|
|
||||||
extends Model<RoleAttributes, RoleCreationAttributes>
|
|
||||||
implements RoleAttributes
|
|
||||||
{
|
|
||||||
public id!: string;
|
|
||||||
public name!: string;
|
|
||||||
public description?: string;
|
|
||||||
|
|
||||||
public readonly createdAt!: Date;
|
|
||||||
public readonly updatedAt!: Date;
|
|
||||||
|
|
||||||
// Users relation
|
|
||||||
public getUsers!: HasManyGetAssociationsMixin<User>;
|
|
||||||
|
|
||||||
// Permissions relation
|
|
||||||
public getPermissions!: BelongsToManyGetAssociationsMixin<Permission>;
|
|
||||||
public setPermissions!: BelongsToManySetAssociationsMixin<Permission, string>;
|
|
||||||
public addPermissions!: BelongsToManyAddAssociationsMixin<Permission, string>;
|
|
||||||
public removePermissions!: BelongsToManyRemoveAssociationsMixin<Permission, string>;
|
|
||||||
|
|
||||||
static initModel(sequelize: Sequelize): typeof Role {
|
|
||||||
Role.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
},
|
|
||||||
|
|
||||||
name: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
description: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: "Role",
|
|
||||||
tableName: "roles",
|
|
||||||
timestamps: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return Role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static associate(models: any) {
|
interface RoleCreationAttributes
|
||||||
Role.hasMany(models.User, {
|
extends Optional<RoleAttributes, "id" | "description"> {}
|
||||||
foreignKey: "roleId",
|
|
||||||
as: "users",
|
|
||||||
});
|
|
||||||
|
|
||||||
Role.belongsToMany(models.Permission, {
|
class Role
|
||||||
through: models.RolePermission,
|
extends Model<RoleAttributes, RoleCreationAttributes>
|
||||||
foreignKey: "roleId",
|
implements RoleAttributes
|
||||||
otherKey: "permissionId",
|
{
|
||||||
as: "permissions",
|
public id!: string;
|
||||||
});
|
public name!: string;
|
||||||
|
public description?: string;
|
||||||
|
|
||||||
|
public readonly createdAt!: Date;
|
||||||
|
public readonly updatedAt!: Date;
|
||||||
|
|
||||||
|
// Users relation
|
||||||
|
public getUsers!: HasManyGetAssociationsMixin<User>;
|
||||||
|
|
||||||
|
// Permissions relation
|
||||||
|
public getPermissions!: BelongsToManyGetAssociationsMixin<Permission>;
|
||||||
|
public setPermissions!: BelongsToManySetAssociationsMixin<Permission, string>;
|
||||||
|
public addPermissions!: BelongsToManyAddAssociationsMixin<Permission, string>;
|
||||||
|
public removePermissions!: BelongsToManyRemoveAssociationsMixin<Permission, string>;
|
||||||
|
|
||||||
|
static initModel(sequelize: Sequelize): typeof Role {
|
||||||
|
Role.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
},
|
||||||
|
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
unique: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
description: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "Role",
|
||||||
|
tableName: "roles",
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate(models: any) {
|
||||||
|
Role.hasMany(models.User, {
|
||||||
|
foreignKey: "roleId",
|
||||||
|
as: "users",
|
||||||
|
});
|
||||||
|
|
||||||
|
Role.belongsToMany(models.Permission, {
|
||||||
|
through: models.RolePermission,
|
||||||
|
foreignKey: "roleId",
|
||||||
|
otherKey: "permissionId",
|
||||||
|
as: "permissions",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default Role;
|
export default Role;
|
||||||
|
|||||||
75
src/modules/permission/permission.controller.ts
Normal file
75
src/modules/permission/permission.controller.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { Controller } from "../../core/controller/main.controller";
|
||||||
|
import PermissionService from "./permission.service";
|
||||||
|
import { ServerResponse } from "../../core/types";
|
||||||
|
|
||||||
|
class PermissionControllerClass extends Controller {
|
||||||
|
#service;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#service = PermissionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ایجاد دسترسی جدید
|
||||||
|
create = async (req:any, res: ServerResponse, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const result = await this.#service.create(req.body);
|
||||||
|
return res.status(201).json({status:201,data:result});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// دریافت لیست دسترسیها (با فیلتر و صفحهبندی)
|
||||||
|
getAll = async (req:any, res: ServerResponse, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const result = await this.#service.getAll(req.query);
|
||||||
|
return res.status(200).json({status:200,data:result});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// دریافت یک دسترسی خاص
|
||||||
|
getById = async (req:any, res: ServerResponse, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await this.#service.getById(id);
|
||||||
|
return res.status(200).json({status:200,data:result});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ویرایش دسترسی
|
||||||
|
update = async (req:any, res: ServerResponse, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await this.#service.update(id, req.body);
|
||||||
|
return res.status(200).json({status:200,data:result});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getList = async(req:any,res:ServerResponse,next:NextFunction) => {
|
||||||
|
try {
|
||||||
|
const result = await this.#service.getList();
|
||||||
|
return res.status(200).json({status:200,data:result});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// حذف دسترسی
|
||||||
|
delete = async (req:any, res: ServerResponse, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await this.#service.delete(id);
|
||||||
|
return res.status(200).json({status:200,data:result});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionController = new PermissionControllerClass();
|
||||||
|
export default PermissionController;
|
||||||
19
src/modules/permission/permission.routes.ts
Normal file
19
src/modules/permission/permission.routes.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import PermissionController from "./permission.controller";
|
||||||
|
|
||||||
|
const PermissionRouter = Router();
|
||||||
|
|
||||||
|
PermissionRouter.post("/create", PermissionController.create);
|
||||||
|
|
||||||
|
|
||||||
|
PermissionRouter.get("/all", PermissionController.getAll);
|
||||||
|
PermissionRouter.get("/list", PermissionController.getList);
|
||||||
|
|
||||||
|
PermissionRouter.get("/get/:id", PermissionController.getById);
|
||||||
|
|
||||||
|
|
||||||
|
PermissionRouter.put("/update/:id", PermissionController.update);
|
||||||
|
|
||||||
|
PermissionRouter.delete("/remove/:id", PermissionController.delete);
|
||||||
|
|
||||||
|
export default PermissionRouter;
|
||||||
81
src/modules/permission/permission.service.ts
Normal file
81
src/modules/permission/permission.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Op } from "sequelize";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import Permission from "../../models/Permission";
|
||||||
|
|
||||||
|
class PermissionServiceClass {
|
||||||
|
async create(data: { name: string }) {
|
||||||
|
const existing = await Permission.findOne({ where: { name: data.name } });
|
||||||
|
if (existing) {
|
||||||
|
throw createHttpError.BadRequest("این دسترسی قبلاً ثبت شده است.");
|
||||||
|
}
|
||||||
|
return await Permission.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(query: any) {
|
||||||
|
const { page = 1, limit = 10, search, startDate, endDate } = query;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const whereClause: any = {};
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
whereClause.name = { [Op.iLike]: `%${search}%` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate || endDate) {
|
||||||
|
whereClause.createdAt = {};
|
||||||
|
if (startDate) whereClause.createdAt[Op.gte] = new Date(startDate);
|
||||||
|
if (endDate) {
|
||||||
|
const end = new Date(endDate);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
whereClause.createdAt[Op.lte] = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { count, rows } = await Permission.findAndCountAll({
|
||||||
|
where: whereClause,
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
limit: parseInt(limit),
|
||||||
|
offset: parseInt(offset.toString()),
|
||||||
|
distinct: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: rows,
|
||||||
|
totalItems: count,
|
||||||
|
totalPages: Math.ceil(count / limit),
|
||||||
|
currentPage: parseInt(page),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async getList() {
|
||||||
|
const data = await Permission.findAll({});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
async getById(id: string) {
|
||||||
|
const permission = await Permission.findByPk(id);
|
||||||
|
if (!permission) {
|
||||||
|
throw createHttpError.NotFound("دسترسی مورد نظر یافت نشد.");
|
||||||
|
}
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: { name: string }) {
|
||||||
|
const permission = await this.getById(id);
|
||||||
|
|
||||||
|
if (data.name && data.name !== permission.name) {
|
||||||
|
const existing = await Permission.findOne({ where: { name: data.name } });
|
||||||
|
if (existing) throw createHttpError.BadRequest("نام دسترسی تکراری است.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await permission.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
const permission = await this.getById(id);
|
||||||
|
await permission.destroy();
|
||||||
|
return { message: "دسترسی با موفقیت حذف شد." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionService = new PermissionServiceClass();
|
||||||
|
export default PermissionService;
|
||||||
@@ -23,9 +23,9 @@ class RoleControllerClass extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(_req: any, res: ServerResponse, next: NextFunction) {
|
async findAll(req: any, res: ServerResponse, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const roles = await this.#serivce.findAll();
|
const roles = await this.#serivce.findAll(req);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -36,6 +36,18 @@ class RoleControllerClass extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findList(req: any, res: ServerResponse, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const roles = await this.#serivce.findList();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
status: 200,
|
||||||
|
data: roles,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
findById = async (req: any, res: ServerResponse, next: NextFunction) => {
|
findById = async (req: any, res: ServerResponse, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const role = await this.#serivce.findById(req.params.id);
|
const role = await this.#serivce.findById(req.params.id);
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import RoleController from "./role.controller";
|
|||||||
|
|
||||||
const RoleRouter = Router();
|
const RoleRouter = Router();
|
||||||
RoleRouter.post("/create", RoleController.create);
|
RoleRouter.post("/create", RoleController.create);
|
||||||
RoleRouter.get("/list", RoleController.findAll);
|
RoleRouter.get("/list", RoleController.findList);
|
||||||
|
RoleRouter.get("/all", RoleController.findAll);
|
||||||
RoleRouter.get("/:id", RoleController.findById);
|
RoleRouter.get("/:id", RoleController.findById);
|
||||||
RoleRouter.put("/update/:id", RoleController.update);
|
RoleRouter.put("/update/:id", RoleController.update);
|
||||||
RoleRouter.delete("/remove/:id", RoleController.delete);
|
RoleRouter.delete("/remove/:id", RoleController.delete);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import createHttpError from "http-errors";
|
||||||
import Permission from "../../models/Permission";
|
import Permission from "../../models/Permission";
|
||||||
import Role from "../../models/Role";
|
import Role from "../../models/Role";
|
||||||
import User from "../../models/User";
|
import User from "../../models/User";
|
||||||
|
import { GlobalMessages } from "../../core/messages";
|
||||||
|
import { Op } from "sequelize";
|
||||||
|
|
||||||
class RoleServiceClass {
|
class RoleServiceClass {
|
||||||
async create(data: { name: string; description?: string }) {
|
async create(data: { name: string; description?: string,permissions:string[] }) {
|
||||||
const existingRole = await Role.findOne({
|
const existingRole = await Role.findOne({
|
||||||
where: { name: data.name },
|
where: { name: data.name },
|
||||||
});
|
});
|
||||||
@@ -12,15 +15,28 @@ class RoleServiceClass {
|
|||||||
throw new Error("Role already exists");
|
throw new Error("Role already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1️⃣ ساخت Role
|
||||||
const role = await Role.create({
|
const role = await Role.create({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2️⃣ اگر permission ارسال شده بود
|
||||||
|
if (data.permissions && data.permissions.length) {
|
||||||
|
const permissions = await Permission.findAll({
|
||||||
|
where: {
|
||||||
|
id: data.permissions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3️⃣ اتصال permissions به role
|
||||||
|
await role.setPermissions(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
return role;
|
return role;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll() {
|
async findList() {
|
||||||
return await Role.findAll({
|
return await Role.findAll({
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@@ -37,6 +53,77 @@ class RoleServiceClass {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAll(req: any) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
name, // فیلتر نام نقش (اختیاری)
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
// ۱. محاسبه offset برای صفحهبندی
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// ۲. ساخت آبجکت فیلترها به صورت داینامیک
|
||||||
|
const whereClause: any = {};
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
// استفاده از Op.iLike برای جستجوی متنی (Case-Insensitive) در دیتابیسهای PostgreSQL
|
||||||
|
// اگر از MySQL استفاده میکنید، از Op.like استفاده کنید
|
||||||
|
whereClause.name = { [Op.iLike]: `%${name}%` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate || endDate) {
|
||||||
|
whereClause.createdAt = {};
|
||||||
|
if (startDate) {
|
||||||
|
whereClause.createdAt[Op.gte] = new Date(startDate); // تاریخ شروع (بزرگتر مساوی)
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
// تنظیم ساعت روی پایان روز برای شامل شدن کلِ روز آخر
|
||||||
|
const end = new Date(endDate);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
whereClause.createdAt[Op.lte] = end; // تاریخ پایان (کوچکتر مساوی)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ۳. استفاده از findAndCountAll برای دریافت همزمان دیتا و تعداد کل رکوردها
|
||||||
|
const { count, rows } = await Role.findAndCountAll({
|
||||||
|
where: whereClause,
|
||||||
|
distinct: true, // اضافه کردن این خط مشکل شمارش تکراری را حل میکند
|
||||||
|
col: "id", // به صورت اختیاری مشخص کنید روی کدام ستون distinct انجام شود
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Permission,
|
||||||
|
as: "permissions",
|
||||||
|
through: { attributes: [] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
order: [["createdAt", "DESC"]],
|
||||||
|
limit: parseInt(limit),
|
||||||
|
offset: parseInt(offset.toString()),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ۴. خروجی استاندارد صفحهبندی شده
|
||||||
|
return {
|
||||||
|
data: rows,
|
||||||
|
totalItems: count,
|
||||||
|
totalPages: Math.ceil(count / limit),
|
||||||
|
currentPage: parseInt(page),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// مدیریت خطا مشابه متد Ticket
|
||||||
|
throw new createHttpError.InternalServerError(
|
||||||
|
GlobalMessages.errors.server,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async findById(id: string) {
|
async findById(id: string) {
|
||||||
const role = await Role.findByPk(id, {
|
const role = await Role.findByPk(id, {
|
||||||
include: [
|
include: [
|
||||||
|
|||||||
Reference in New Issue
Block a user