Rechtemanagement erweitert
This commit is contained in:
parent
85f07108c1
commit
673e33aaf7
27
app.js
27
app.js
|
@ -45,9 +45,30 @@ const memberRoutes = require("./routes/member");
|
||||||
const trainingRoutes = require("./routes/training");
|
const trainingRoutes = require("./routes/training");
|
||||||
const anwesendRoutes = require("./routes/anwesend");
|
const anwesendRoutes = require("./routes/anwesend");
|
||||||
|
|
||||||
app.use("/user", userRoutes);
|
app.use(
|
||||||
app.use("/spiele", spieleRoutes);
|
"/:abteilung?/user",
|
||||||
app.use("/feature", featureRoutes);
|
(req, res, next) => {
|
||||||
|
req.abteilung = req.params.abteilung; // `abteilung` in req speichern
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
userRoutes
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
"/:abteilung?/spiele",
|
||||||
|
(req, res, next) => {
|
||||||
|
req.abteilung = req.params.abteilung; // `abteilung` in req speichern
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
spieleRoutes
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
"/:abteilung?/feature",
|
||||||
|
(req, res, next) => {
|
||||||
|
req.abteilung = req.params.abteilung; // `abteilung` in req speichern
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
featureRoutes
|
||||||
|
);
|
||||||
app.use(
|
app.use(
|
||||||
"/:abteilung?/member",
|
"/:abteilung?/member",
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
|
|
|
@ -20,6 +20,36 @@ const requireAuth = (req, res, next) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function requireRole(minRole) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
const user = req.user; // Aus decoded JWT
|
||||||
|
const abteilung = req.abteilung;
|
||||||
|
|
||||||
|
if (!user || !user.roles || !abteilung) {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Keine Berechtigung (keine Abteilung gesetzt)" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = user.roles[abteilung];
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Keine Rechte für diese Abteilung" });
|
||||||
|
}
|
||||||
|
console.log(role);
|
||||||
|
console.log(minRole);
|
||||||
|
console.log(role <= minRole);
|
||||||
|
if (role > minRole) {
|
||||||
|
// z.B. 1: Admin, 2: ÜL, etc.
|
||||||
|
return res.status(403).json({ error: "Nicht genügend Rechte" });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const requireAdmin = async (req, res, next) => {
|
const requireAdmin = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query("SELECT role FROM users WHERE id = $1", [
|
const result = await pool.query("SELECT role FROM users WHERE id = $1", [
|
||||||
|
@ -37,4 +67,4 @@ const requireAdmin = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { requireAuth, requireAdmin };
|
module.exports = { requireAuth, requireRole, requireAdmin };
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
|
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
|
||||||
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Auth-Middleware
|
const {
|
||||||
|
requireAuth,
|
||||||
|
requireRole,
|
||||||
|
requireAdmin,
|
||||||
|
} = require("../middleware/auth"); // Auth-Middleware
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// **1. Alle Anwesenheiten eines Trainings abrufen**
|
// **1. Alle Anwesenheiten eines Trainings abrufen**
|
||||||
router.get("/:id", requireAuth, async (req, res) => {
|
router.get("/:id", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params; // Training-ID
|
const { id } = req.params; // Training-ID
|
||||||
const abteilung = req.abteilung; // Abteilung aus Middleware
|
const abteilung = req.abteilung; // Abteilung aus Middleware
|
||||||
|
|
||||||
|
@ -34,7 +38,7 @@ router.get("/:id", requireAuth, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// **2. Anwesenheit für ein Training setzen**
|
// **2. Anwesenheit für ein Training setzen**
|
||||||
router.post("/:id", requireAuth, async (req, res) => {
|
router.post("/:id", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params; // Training-ID
|
const { id } = req.params; // Training-ID
|
||||||
const { inriege, anw } = req.body; // Arrays mit IDs
|
const { inriege, anw } = req.body; // Arrays mit IDs
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
|
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
|
||||||
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Auth-Middleware
|
const {
|
||||||
|
requireAuth,
|
||||||
|
requireRole,
|
||||||
|
requireAdmin,
|
||||||
|
} = require("../middleware/auth"); // Auth-Middleware
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get("/:id?", requireAuth, async (req, res) => {
|
router.get("/:id?", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params; // Mitglieds-ID (optional)
|
const { id } = req.params; // Mitglieds-ID (optional)
|
||||||
const abteilung = req.abteilung; // Abteilung aus Middleware
|
const abteilung = req.abteilung; // Abteilung aus Middleware
|
||||||
|
|
||||||
|
console.log(abteilung);
|
||||||
try {
|
try {
|
||||||
let query;
|
let query;
|
||||||
let values = [];
|
let values = [];
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
if (abteilung) {
|
if (abteilung) {
|
||||||
// **Einzelnes Mitglied mit aktueller Riege abrufen**
|
console.log("**Einzelnes Mitglied mit aktueller Riege abrufen**");
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
m.*,
|
m.*,
|
||||||
|
@ -32,12 +37,12 @@ router.get("/:id?", requireAuth, async (req, res) => {
|
||||||
`;
|
`;
|
||||||
values = [id, abteilung];
|
values = [id, abteilung];
|
||||||
} else {
|
} else {
|
||||||
// **Einzelnes Mitglied ohne Riege abrufen**
|
console.log(" **Einzelnes Mitglied ohne Riege abrufen**");
|
||||||
query = `SELECT * FROM members WHERE id = $1`;
|
query = `SELECT * FROM members WHERE id = $1`;
|
||||||
values = [id];
|
values = [id];
|
||||||
}
|
}
|
||||||
} else if (abteilung) {
|
} else if (abteilung) {
|
||||||
// **Alle Mitglieder einer Abteilung abrufen**
|
console.log("**Alle Mitglieder einer Abteilung abrufen**");
|
||||||
query = `
|
query = `
|
||||||
SELECT
|
SELECT
|
||||||
m.*,
|
m.*,
|
||||||
|
@ -51,11 +56,11 @@ router.get("/:id?", requireAuth, async (req, res) => {
|
||||||
AND riegenzuordnung.bis IS NULL
|
AND riegenzuordnung.bis IS NULL
|
||||||
LEFT JOIN riegen r
|
LEFT JOIN riegen r
|
||||||
ON riegenzuordnung.fid_riege = r.id
|
ON riegenzuordnung.fid_riege = r.id
|
||||||
WHERE r.fid_abteilung = $1 OR r.fid_abteilung IS NULL
|
WHERE r.fid_abteilung = $1
|
||||||
`;
|
`;
|
||||||
values = [abteilung];
|
values = [abteilung];
|
||||||
} else {
|
} else {
|
||||||
// **Alle Mitglieder abrufen**
|
console.log("**Alle Mitglieder abrufen**");
|
||||||
query = `SELECT * FROM mitglieder`;
|
query = `SELECT * FROM mitglieder`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ router.get("/:id?", requireAuth, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// **2. Mitglied anlegen oder aktualisieren (Nur für Admins)**
|
// **2. Mitglied anlegen oder aktualisieren (Nur für Admins)**
|
||||||
router.put("/:id?", requireAuth, requireAdmin, async (req, res) => {
|
router.put("/:id?", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const {
|
const {
|
||||||
vorname,
|
vorname,
|
||||||
|
@ -174,7 +179,7 @@ router.put("/:id?", requireAuth, requireAdmin, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// **3. Mitglied löschen (Nur für Admins)**
|
// **3. Mitglied löschen (Nur für Admins)**
|
||||||
router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
|
router.delete("/:id", requireAuth, requireRole(2), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -197,7 +202,7 @@ router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// **1. notfallnummern abrufen (Falls ID gegeben: Nur die Nummern eines Mitglieds)**
|
// **1. notfallnummern abrufen (Falls ID gegeben: Nur die Nummern eines Mitglieds)**
|
||||||
router.get("/phone/:id", requireAuth, async (req, res) => {
|
router.get("/phone/:id", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -220,7 +225,7 @@ router.get("/phone/:id", requireAuth, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// **2. Telefonnummer anlegen oder aktualisieren (Nur für Admins)**
|
// **2. Telefonnummer anlegen oder aktualisieren (Nur für Admins)**
|
||||||
router.put("/phone/:id?", requireAuth, requireAdmin, async (req, res) => {
|
router.put("/phone/:id?", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { fid_teilnehmer, name, nummer, verbindung, stand } = req.body;
|
const { fid_teilnehmer, name, nummer, verbindung, stand } = req.body;
|
||||||
|
|
||||||
|
@ -285,7 +290,7 @@ router.put("/phone/:id?", requireAuth, requireAdmin, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// **3. Telefonnummer löschen (Nur für Admins)**
|
// **3. Telefonnummer löschen (Nur für Admins)**
|
||||||
router.delete("/phone/:id", requireAuth, requireAdmin, async (req, res) => {
|
router.delete("/phone/:id", requireAuth, requireRole(2), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -307,7 +312,7 @@ router.delete("/phone/:id", requireAuth, requireAdmin, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/riege/:id", requireAuth, requireAdmin, async (req, res) => {
|
router.post("/riege/:id", requireAuth, requireRole(2), async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const abteilung = req.abteilung;
|
const abteilung = req.abteilung;
|
||||||
const { neue_riege } = req.body;
|
const { neue_riege } = req.body;
|
||||||
|
|
|
@ -61,7 +61,7 @@ router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put("/:id?", requireAuth, requireAdmin, async (req, res) => {
|
router.put("/:id?", requireAuth, async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, material, regeln, variationen, dauer, type } = req.body;
|
const { name, material, regeln, variationen, dauer, type } = req.body;
|
||||||
|
|
||||||
|
@ -115,5 +115,4 @@ router.put("/:id?", requireAuth, requireAdmin, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
|
const pool = require("../db"); // PostgreSQL-Datenbankverbindung
|
||||||
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Auth-Middleware
|
const { requireAuth, requireRole } = require("../middleware/auth"); // Auth-Middleware
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ router.get("/:year?/:kw?", requireAuth, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/leiten/:id", requireAuth, async (req, res) => {
|
router.post("/leiten/:id", requireAuth, requireRole(3), async (req, res) => {
|
||||||
const { id } = req.params; // Trainings-ID
|
const { id } = req.params; // Trainings-ID
|
||||||
const abteilung = req.abteilung; // Abteilung aus Middleware
|
const abteilung = req.abteilung; // Abteilung aus Middleware
|
||||||
const { fid_helfer, fid_spiel, typ, action } = req.body; // Daten aus POST-Body
|
const { fid_helfer, fid_spiel, typ, action } = req.body; // Daten aus POST-Body
|
||||||
|
@ -122,7 +122,7 @@ router.post("/leiten/:id", requireAuth, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/new/:jahr/:kw", requireAuth, requireAdmin, async (req, res) => {
|
router.post("/new/:jahr/:kw", requireAuth, requireRole(2), async (req, res) => {
|
||||||
const { jahr, kw } = req.params;
|
const { jahr, kw } = req.params;
|
||||||
const abteilung = req.abteilung; // Abteilung aus Middleware
|
const abteilung = req.abteilung; // Abteilung aus Middleware
|
||||||
|
|
||||||
|
|
109
routes/user.js
109
routes/user.js
|
@ -4,7 +4,11 @@ const bcrypt = require("bcryptjs");
|
||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const { requireAuth, requireAdmin } = require("../middleware/auth"); // Falls Middleware in einer extra Datei liegt
|
const {
|
||||||
|
requireAuth,
|
||||||
|
requireRole,
|
||||||
|
requireAdmin,
|
||||||
|
} = require("../middleware/auth"); // Falls Middleware in einer extra Datei liegt
|
||||||
const { transporter, sendActivationEmail } = require("../middleware/mail");
|
const { transporter, sendActivationEmail } = require("../middleware/mail");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
@ -47,7 +51,7 @@ router.post("/login", async (req, res) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userResult = await pool.query(
|
const userResult = await pool.query(
|
||||||
"SELECT *, CASE WHEN admin_temp IS NOT NULL AND (now() - admin_temp) > interval '22 hours' THEN 'expired' ELSE 'valid' END AS admin_status FROM users WHERE username = $1",
|
"SELECT * FROM users WHERE username = $1",
|
||||||
[username]
|
[username]
|
||||||
);
|
);
|
||||||
if (userResult.rows.length > 0) {
|
if (userResult.rows.length > 0) {
|
||||||
|
@ -56,20 +60,24 @@ router.post("/login", async (req, res) => {
|
||||||
const match = await bcrypt.compare(password, user.password);
|
const match = await bcrypt.compare(password, user.password);
|
||||||
if (match) {
|
if (match) {
|
||||||
if (user.is_active) {
|
if (user.is_active) {
|
||||||
|
const rightsResult = await pool.query(
|
||||||
|
`SELECT fid_abteilung, fid_role FROM userrights WHERE fid_user = $1`,
|
||||||
|
[user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rollen-Map erstellen:
|
||||||
|
const roles = {};
|
||||||
|
rightsResult.rows.forEach((row) => {
|
||||||
|
roles[row.fid_abteilung] = row.fid_role;
|
||||||
|
});
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ id: user.id, username: user.username, role: user.role },
|
{ id: user.id, username: user.username, roles },
|
||||||
process.env.JWT_SECRET,
|
process.env.JWT_SECRET,
|
||||||
{
|
{
|
||||||
expiresIn: "24h",
|
expiresIn: "24d",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (user.admin_status === "expired") {
|
res.json({ token, roles });
|
||||||
await pool.query(
|
|
||||||
"UPDATE users SET role = $1, admin_temp = NULL WHERE id = $2",
|
|
||||||
["user", user.id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.json({ token });
|
|
||||||
} else {
|
} else {
|
||||||
return res.status(401).json({ error: "Auf Freischlatung warten" });
|
return res.status(401).json({ error: "Auf Freischlatung warten" });
|
||||||
}
|
}
|
||||||
|
@ -209,10 +217,40 @@ router.post("/update", requireAuth, async (req, res) => {
|
||||||
// Liste der User mit Rollen und is_active
|
// Liste der User mit Rollen und is_active
|
||||||
router.get("/admin", requireAuth, requireAdmin, async (req, res) => {
|
router.get("/admin", requireAuth, requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
// 1️⃣ Alle Benutzer abfragen:
|
||||||
"SELECT id, username, email, role, is_active FROM users ORDER BY is_active ASC, role DESC"
|
const usersResult = await pool.query(
|
||||||
|
"SELECT id, username, email, is_active FROM users ORDER BY is_active ASC"
|
||||||
);
|
);
|
||||||
res.json(result.rows);
|
|
||||||
|
const users = usersResult.rows;
|
||||||
|
|
||||||
|
// 2️⃣ Alle Rollen pro User abfragen:
|
||||||
|
const rolesResult = await pool.query(
|
||||||
|
`SELECT ur.fid_user, ur.fid_abteilung, ur.fid_role, r.role
|
||||||
|
FROM userrights ur
|
||||||
|
JOIN userroles r ON ur.fid_role = r.id`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3️⃣ Rollen für Benutzer gruppieren:
|
||||||
|
const userRolesMap = {};
|
||||||
|
rolesResult.rows.forEach((row) => {
|
||||||
|
if (!userRolesMap[row.fid_user]) {
|
||||||
|
userRolesMap[row.fid_user] = [];
|
||||||
|
}
|
||||||
|
userRolesMap[row.fid_user].push({
|
||||||
|
abteilung: row.fid_abteilung,
|
||||||
|
role_id: row.fid_role,
|
||||||
|
role_name: row.role,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4️⃣ Benutzer + Rollen zusammenführen:
|
||||||
|
const enrichedUsers = users.map((user) => ({
|
||||||
|
...user,
|
||||||
|
roles: userRolesMap[user.id] || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(enrichedUsers);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: "Interner Serverfehler" });
|
res.status(500).json({ error: "Interner Serverfehler" });
|
||||||
|
@ -258,6 +296,49 @@ router.put("/activate/:id", requireAuth, requireAdmin, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
"/rights/:userId/:role",
|
||||||
|
requireAuth,
|
||||||
|
requireRole(2),
|
||||||
|
async (req, res) => {
|
||||||
|
const { userId, role } = req.params;
|
||||||
|
const abteilung = req.abteilung; // Abteilung aus Middleware
|
||||||
|
|
||||||
|
if (!abteilung || !role) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Abteilung und Rolle müssen angegeben werden" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfen, ob bereits ein Recht für User & Abteilung existiert
|
||||||
|
const existing = await pool.query(
|
||||||
|
`SELECT id FROM userrights WHERE fid_user = $1 AND fid_abteilung = $2`,
|
||||||
|
[userId, abteilung]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
// Eintrag existiert → UPDATE
|
||||||
|
await pool.query(`UPDATE userrights SET fid_role = $1 WHERE id = $2`, [
|
||||||
|
role,
|
||||||
|
existing.rows[0].id,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Kein Eintrag → INSERT
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO userrights (fid_user, fid_abteilung, fid_role) VALUES ($1, $2, $3)`,
|
||||||
|
[userId, abteilung, role]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: "Rolle erfolgreich vergeben/geändert" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Interner Serverfehler" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
|
router.delete("/:id", requireAuth, requireAdmin, async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue