Skip to content

Commit

Permalink
BC-7466 - Switch SHD administration of schools (students) to new dele…
Browse files Browse the repository at this point in the history
…tion routines (#278)
  • Loading branch information
wolfganggreschus authored Feb 20, 2025
1 parent 50a6f6f commit 37cc76b
Show file tree
Hide file tree
Showing 15 changed files with 668 additions and 32 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ then clear build files and gulp cache with `gulp clear`
- PORT
- SC_THEME
- BACKEND_URL
- ADMIN_API_URL
- ADMIN_API_KEY
- API_KEY
- REDIS_URI

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ metadata:
app: shd
data:
BACKEND_URL: "http://api-svc:{{ PORT_SERVER }}/api/"
ADMIN_API_URL: "http://api-admin-svc:4030/admin/api/"
SC_NAV_TITLE: "{{ SC_NAV_TITLE }}"
HOST: "https://{{ DOMAIN }}"
FEATURE_SCHOOL_SANIS_USER_MIGRATION_ENABLED: "{{ FEATURE_SCHOOL_SANIS_USER_MIGRATION_ENABLED|default(false, true) }}"
Expand Down
44 changes: 27 additions & 17 deletions api.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
const request = require('request');
const rp = require('request-promise');
const request = require("request");
const rp = require("request-promise");

const api = (req, { useCallback = false, json = true, version = 'v1' } = {}) => {
const headers = {};
if(req && req.cookies && req.cookies.jwt) {
headers['Authorization'] = (req.cookies.jwt.startsWith('Bearer ') ? '' : 'Bearer ') + req.cookies.jwt;
}
if(process.env.API_KEY) {
headers['x-api-key'] = process.env.API_KEY;
}
const api = (
req,
{ useCallback = false, json = true, version = "v1", adminApi = false } = {}
) => {
let baseUrl = process.env.BACKEND_URL || "http://localhost:3030/api/";

const baseUrl = process.env.BACKEND_URL || 'http://localhost:3030/api/';
const handler = useCallback ? request : rp;
return handler.defaults({
baseUrl: new URL(version, baseUrl).href,
json,
headers
});
const headers = {};

if (adminApi) {
baseUrl = process.env.ADMIN_API_URL || "http://localhost:4030/admin/api/";
headers["x-api-key"] = process.env.ADMIN_API_KEY || "thisisasupersecureapikeythatisabsolutelysave";
} else if (req && req.cookies && req.cookies.jwt) {
headers["Authorization"] =
(req.cookies.jwt.startsWith("Bearer ") ? "" : "Bearer ") +
req.cookies.jwt;
}

const handler = useCallback ? request : rp;

const apiRequest = {
baseUrl: new URL(version, baseUrl).href,
json,
headers,
};

return handler.defaults(apiRequest);
};

module.exports = { api };
159 changes: 159 additions & 0 deletions controllers/batch-deletion/api-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const { api } = require("../../api");
const moment = require("moment");

moment.locale("de");

const getFormattedDate = (date) => {
const formattedDate = moment(date).format("DD.MM.YYYY, HH:mm");
return formattedDate;
};

const germanRoleNames = {
student: "Schüler",
teacher: "Lehrer",
administrator: "Admin",
expert: "Experte",
superhero: "Superhero",
invalid: "Ungültig",
};

const mapUserIds = (batch) => {
const invalidUsersCount = batch.invalidUsers.length;
const invalidUsers = {
roleName: "invalid",
userCount: invalidUsersCount,
};

const usersByRole = batch.usersByRole.concat(
batch.skippedUsersByRole,
invalidUsers
);

return usersByRole
.sort((a, b) => b.userCount - a.userCount)
.map((role) => {
return {
roleName: germanRoleNames[role.roleName],
userCount: role.userCount,
};
});
};

const mapBatches = (batches) => {
return batches.map((batch) => {
const formattedDate = getFormattedDate(batch.createdAt);
const batchTitle = `${batch.name} - ${formattedDate} Uhr`;

const isValidBatch = batch.usersByRole.length > 0;
const status = isValidBatch ? batch.status : "invalid";
const canDeleteBatch = status === "created" || status === "invalid";
const canStartDeletion = canDeleteBatch && isValidBatch;

const ids = mapUserIds(batch);
const overallCount = ids.reduce((acc, role) => {
return acc + role.userCount;
}, 0);

return {
id: batch.id,
status,
usersByRole: ids,
createdAt: formattedDate,
batchTitle,
overallCount,
canDeleteBatch,
canStartDeletion,
};
});
};

const getDeletionBatches = async (req, res, next) => {
try {
const response = await api(req, { adminApi: true }).get(
`/deletion-batches`
);

const formattedBatches = mapBatches(response.data);

res.render("batch-deletion/batch-deletion", {
title: "Sammellöschung von Schülern",
user: res.locals.currentUser,
themeTitle: process.env.SC_NAV_TITLE || "Schul-Cloud",
batches: formattedBatches,
});
} catch (error) {
next(error);
}
};

const getDeletionBatchDetails = async (req, res, next) => {
try {
const { id } = req.params;

const response = await api(req, { adminApi: true }).get(
`/deletion-batches/${id}`
);

res.status(200).json(response);
} catch (error) {
next(error);
}
};

const deleteBatch = async (req, res, next) => {
try {
const { id } = req.params;
await api(req, { adminApi: true }).delete(`/deletion-batches/${id}`);

res.sendStatus(200);
} catch (error) {
next(error);
}
};

const sendDeletionRequest = async (req, res, next) => {
try {
const { id } = req.params;
await api(req, { adminApi: true }).post(`/deletion-batches/${id}/execute`);

res.sendStatus(200);
} catch (error) {
next(error);
}
};

const sendFile = async (req, res, next) => {
const { fileContent, batchTitle } = req.body;

if (!fileContent || !batchTitle) {
return res
.status(400)
.send({ message: "No file content or batch title provided" });
}

const targetRefIds = fileContent.split("\n").map((item) => item.trim());
try {
const response = await api(req, { adminApi: true }).post(
"/deletion-batches/",
{
json: {
name: batchTitle,
targetRefDomain: "user",
targetRefIds,
},
}
);

res.status(200).send({ message: "File sent successfully" });
} catch (error) {
next(error);
}
};

module.exports = {
getDeletionBatches,
getDeletionBatchDetails,
deleteBatch,
sendDeletionRequest,
sendFile,
};
22 changes: 22 additions & 0 deletions controllers/batch-deletion/batch-deletion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* One Controller per layout view
*/

const express = require("express");
const router = express.Router();
const authHelper = require("../../helpers/authentication");
const apiRequests = require("./api-requests");

router.use(authHelper.authChecker);

router.get("/", apiRequests.getDeletionBatches);

router.post("/", apiRequests.sendFile);

router.get("/:id", apiRequests.getDeletionBatchDetails);

router.delete("/:id", apiRequests.deleteBatch);

router.post("/:id/execute", apiRequests.sendDeletionRequest);

module.exports = router;
1 change: 1 addition & 0 deletions controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ router.use('/statistics', require('./statistics'));
router.use('/ctltools', require('./ctltools'));
router.use('/storageproviders', require('./storageproviders'));
router.use('/base64files/', require('./base64files'));
router.use('/batch-deletion/', require('./batch-deletion/batch-deletion'));

module.exports = router;
2 changes: 2 additions & 0 deletions controllers/management/managementRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ router.post('/uploadConsent', controllerLogic.updateInstancePolicy);
router.get('/', controllerLogic.mainRoute);

module.exports = router;


5 changes: 5 additions & 0 deletions helpers/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ const restrictSidebar = (req, res) => {
icon: 'server',
link: '/storageproviders/'
},
{
name: 'Löschung',
icon: 'trash',
link: '/batch-deletion/'
},
];

res.locals.sidebarItems = res.locals.sidebarItems.filter((item) => item.enabled == null || item.enabled);
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions static/scripts/batch-deletion/batch-deletion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
$(document).ready(() => {
const setHTMLForIds = (ids, idType) => {
const section = document.querySelector(`#${idType}-ids-section`);

if (ids.length === 0) {
section.innerHTML = `<span class='no-ids-text'>Nothing ${idType}</span>`;
return;
}
const idsString = ids.join("\n");
const textAreaString = `<textarea id="${idType}-ids" class="id-list" rows="3" readonly>${idsString}</textarea>`;

section.innerHTML = textAreaString;
};

function copyToClipboard(event) {
const id = this.getAttribute("data-text-id");
const text = document.getElementById(id).innerHTML;

navigator.clipboard
.writeText(text)
.then(() => {
alert("IDs copied to clipboard!");
})
.catch((err) => {
console.error("Failed to copy text: ", err);
});
}

function fetchDeletionBatchDetails(batchId) {
fetch(`/batch-deletion/${batchId}`)
.then((res) => {
if (!res.ok) {
throw new Error(`${res.status} - ${res.statusText}`);
}
return res.json();
})
.then((data) => {
setHTMLForIds(data.pendingDeletions, "pending");
setHTMLForIds(data.successfulDeletions, "deleted");
setHTMLForIds(data.failedDeletions, "failed");
setHTMLForIds(data.invalidIds, "invalid");

const mappedSkippedIds = data.skippedDeletions
.map((listItem) => {
return ["\n" + listItem.roleName, ...listItem.ids];
})
.flat();
setHTMLForIds(mappedSkippedIds.flat(), "skipped");

document.querySelectorAll(".copy-btn").forEach((button) => {
button.addEventListener("click", copyToClipboard);
});
})
.catch((error) => {
console.error("error", error);
});
}

function deleteBatch(batchId) {
fetch(`/batch-deletion/${batchId}`, { method: "DELETE" })
.then((res) => {
if (res.ok) {
location.reload();
} else {
console.error("Error:", res.statusText);
}
})
.catch((error) => {
console.error("error", error);
});
}

function sendDeletionRequest(batchId) {
fetch(`/batch-deletion/${batchId}/execute`, { method: "POST" })
.then((res) => {
if (res.ok) {
location.reload();
} else {
console.error("Error:", res.statusText);
}
})
.catch((error) => {
console.error("error", error);
});
}

document.querySelectorAll(".details-toggle").forEach((button) => {
button.addEventListener("click", function () {
const title = this.getAttribute("data-title");
const batchId = this.getAttribute("data-batch-id");

document.querySelector(".modal-title").innerText = title;

fetchDeletionBatchDetails(batchId);
});
});

document.querySelectorAll(".delete-batch-btn").forEach((button) => {
button.addEventListener("click", function () {
const batchId = this.getAttribute("data-batch-id");

deleteBatch(batchId);
});
});

document.querySelectorAll(".start-deletion-btn").forEach((button) => {
button.addEventListener("click", function () {
const batchId = this.getAttribute("data-batch-id");
this.setAttribute("disabled", true);

sendDeletionRequest(batchId);
});
});
});
Loading

0 comments on commit 37cc76b

Please sign in to comment.