add OAuth defaults and admin reservation deletion support
Seed rms.auth.oauth plugin settings with a Google OIDC example while keeping the plugin disabled by default, add admin API/UI support to delete individual reservation entries, and extend auth flow handling for OAuth callback redirects and errors.
This commit is contained in:
113
server/index.js
113
server/index.js
@@ -491,6 +491,22 @@ async function routeRequest(req, res) {
|
||||
return handleDeleteOwnReservation(res, auth.user);
|
||||
}
|
||||
|
||||
if (method === "DELETE" && url.pathname.startsWith("/v1/station/reservations/")) {
|
||||
const auth = requireAuth(req, res);
|
||||
if (!auth) return;
|
||||
if (!hasRole(auth.user, ["admin"])) {
|
||||
return sendError(res, 403, "auth.forbidden", "Nicht berechtigt");
|
||||
}
|
||||
const encodedUserId = decodeURIComponent(url.pathname.slice("/v1/station/reservations/".length) || "").trim();
|
||||
if (!encodedUserId || encodedUserId === "next") {
|
||||
return sendError(res, 400, "station.reservation.user_missing", "Benutzer-ID fuer Reservierungsloeschung fehlt");
|
||||
}
|
||||
if (!enforceRateLimit(req, res, `station:reserve-delete-admin:${auth.user.id}`, config.actionRateLimit, config.actionRateWindowMs)) {
|
||||
return;
|
||||
}
|
||||
return handleDeleteReservationByUserId(res, auth.user, encodedUserId, true);
|
||||
}
|
||||
|
||||
if (method === "POST" && url.pathname === "/v1/openwebrx/session") {
|
||||
const auth = requireAuth(req, res);
|
||||
if (!auth) return;
|
||||
@@ -1752,12 +1768,40 @@ async function migratePluginSettingsDefaults() {
|
||||
changed = ensureVswrReportReaderSettings(dataDir) || changed;
|
||||
changed = ensureOpenWebRxBandmapSettings() || changed;
|
||||
changed = ensureMicrohamSettings() || changed;
|
||||
changed = ensureOauthSettings() || changed;
|
||||
|
||||
if (changed) {
|
||||
await savePluginState();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureOauthSettings() {
|
||||
const pluginId = "rms.auth.oauth";
|
||||
const current = runtime.pluginState.settings[pluginId] && typeof runtime.pluginState.settings[pluginId] === "object"
|
||||
? { ...runtime.pluginState.settings[pluginId] }
|
||||
: {};
|
||||
const next = {
|
||||
...current,
|
||||
authorizeUrl: current.authorizeUrl || "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
tokenUrl: current.tokenUrl || "https://oauth2.googleapis.com/token",
|
||||
userInfoUrl: current.userInfoUrl || "https://openidconnect.googleapis.com/v1/userinfo",
|
||||
clientId: current.clientId || "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com",
|
||||
clientSecret: current.clientSecret || "YOUR_GOOGLE_CLIENT_SECRET",
|
||||
scope: current.scope || "openid email profile",
|
||||
redirectUri: current.redirectUri || "",
|
||||
emailField: current.emailField || "email",
|
||||
authStyle: current.authStyle || "body",
|
||||
audience: current.audience || "",
|
||||
extraAuthorizeParams: current.extraAuthorizeParams || "",
|
||||
extraTokenParams: current.extraTokenParams || ""
|
||||
};
|
||||
if (JSON.stringify(current) !== JSON.stringify(next)) {
|
||||
runtime.pluginState.settings[pluginId] = next;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function ensureOpenWebRxBandmapSettings() {
|
||||
const pluginId = "rms.openwebrx.bandmap";
|
||||
const current = runtime.pluginState.settings[pluginId] && typeof runtime.pluginState.settings[pluginId] === "object"
|
||||
@@ -3182,28 +3226,44 @@ async function handleReserveNextSlot(res, user) {
|
||||
}
|
||||
|
||||
async function handleDeleteOwnReservation(res, user) {
|
||||
return handleDeleteReservationByUserId(res, user, user.id, false);
|
||||
}
|
||||
|
||||
async function handleDeleteReservationByUserId(res, actorUser, targetUserId, adminMode = false) {
|
||||
return withLock(async () => {
|
||||
const nowMs = Date.now();
|
||||
const reservations = getNormalizedStationReservations(nowMs);
|
||||
const ownEntry = reservations.find((entry) => String(entry.userId || "") === String(user.id || ""));
|
||||
if (!ownEntry) {
|
||||
return sendError(res, 404, "station.reservation.not_found", "Keine eigene Reservierung gefunden");
|
||||
const targetId = String(targetUserId || "");
|
||||
const targetEntry = reservations.find((entry) => String(entry.userId || "") === targetId);
|
||||
if (!targetEntry) {
|
||||
return sendError(
|
||||
res,
|
||||
404,
|
||||
"station.reservation.not_found",
|
||||
adminMode ? "Reservierung nicht gefunden" : "Keine eigene Reservierung gefunden"
|
||||
);
|
||||
}
|
||||
const removed = removeReservationByUserId(reservations, user.id);
|
||||
const removed = removeReservationByUserId(reservations, targetId);
|
||||
if (!removed.changed) {
|
||||
return sendError(res, 404, "station.reservation.not_found", "Keine eigene Reservierung gefunden");
|
||||
return sendError(
|
||||
res,
|
||||
404,
|
||||
"station.reservation.not_found",
|
||||
adminMode ? "Reservierung nicht gefunden" : "Keine eigene Reservierung gefunden"
|
||||
);
|
||||
}
|
||||
|
||||
const ownFromMs = parseIsoMs(ownEntry.from);
|
||||
const ownToMs = parseIsoMs(ownEntry.to);
|
||||
const deletingActiveOwnSlot = Number.isFinite(ownFromMs)
|
||||
&& Number.isFinite(ownToMs)
|
||||
&& ownFromMs <= nowMs
|
||||
&& nowMs < ownToMs;
|
||||
const fromMs = parseIsoMs(targetEntry.from);
|
||||
const toMs = parseIsoMs(targetEntry.to);
|
||||
const deletingActiveSlot = Number.isFinite(fromMs)
|
||||
&& Number.isFinite(toMs)
|
||||
&& fromMs <= nowMs
|
||||
&& nowMs < toMs;
|
||||
const targetIsCurrentOwner = runtime.station.isInUse && String(runtime.station.activeByUserId || "") === targetId;
|
||||
|
||||
if (deletingActiveOwnSlot && runtime.station.isInUse && String(runtime.station.activeByUserId || "") === String(user.id || "")) {
|
||||
if (deletingActiveSlot && targetIsCurrentOwner) {
|
||||
try {
|
||||
await safeShutdownStationSession(user, "reservation-delete-current");
|
||||
await safeShutdownStationSession(actorUser, "reservation-delete-current");
|
||||
} catch (error) {
|
||||
if (error && error.code === "TX_DISABLE_FAILED") {
|
||||
return sendError(res, 409, "tx.disable_failed", String(error.message || error), error.details || null);
|
||||
@@ -3218,20 +3278,23 @@ async function handleDeleteOwnReservation(res, user) {
|
||||
startedAt: null,
|
||||
endsAt: null,
|
||||
reservations: removed.reservations,
|
||||
manualReleaseReservationUserId: String(runtime.station.manualReleaseReservationUserId || "") === String(user && user.id ? user.id : "")
|
||||
manualReleaseReservationUserId: String(runtime.station.manualReleaseReservationUserId || "") === targetId
|
||||
? null
|
||||
: (runtime.station.manualReleaseReservationUserId || null),
|
||||
manualReleaseReservationUntil: String(runtime.station.manualReleaseReservationUserId || "") === String(user && user.id ? user.id : "")
|
||||
manualReleaseReservationUntil: String(runtime.station.manualReleaseReservationUserId || "") === targetId
|
||||
? null
|
||||
: (runtime.station.manualReleaseReservationUntil || null),
|
||||
updatedAt: new Date().toISOString(),
|
||||
lastAction: "reserve-remove-current"
|
||||
lastAction: adminMode ? "reserve-remove-current-admin" : "reserve-remove-current"
|
||||
};
|
||||
await writeJson(files.station, runtime.station);
|
||||
await appendAudit("station.reserve.remove", user, { queueLength: removed.reservations.length, currentSlot: true });
|
||||
const autoActivated = await autoActivateReservationSlotIfNeededUnlocked({
|
||||
excludeUserId: user && user.id ? String(user.id) : ""
|
||||
await appendAudit("station.reserve.remove", actorUser, {
|
||||
queueLength: removed.reservations.length,
|
||||
currentSlot: true,
|
||||
targetUserId: targetId,
|
||||
mode: adminMode ? "admin" : "self"
|
||||
});
|
||||
const autoActivated = await autoActivateReservationSlotIfNeededUnlocked({ excludeUserId: targetId });
|
||||
if (!autoActivated) {
|
||||
broadcastEvent("station.status.changed", buildStationStatusView());
|
||||
}
|
||||
@@ -3241,17 +3304,21 @@ async function handleDeleteOwnReservation(res, user) {
|
||||
runtime.station = {
|
||||
...runtime.station,
|
||||
reservations: removed.reservations,
|
||||
manualReleaseReservationUserId: String(runtime.station.manualReleaseReservationUserId || "") === String(user && user.id ? user.id : "")
|
||||
manualReleaseReservationUserId: String(runtime.station.manualReleaseReservationUserId || "") === targetId
|
||||
? null
|
||||
: (runtime.station.manualReleaseReservationUserId || null),
|
||||
manualReleaseReservationUntil: String(runtime.station.manualReleaseReservationUserId || "") === String(user && user.id ? user.id : "")
|
||||
manualReleaseReservationUntil: String(runtime.station.manualReleaseReservationUserId || "") === targetId
|
||||
? null
|
||||
: (runtime.station.manualReleaseReservationUntil || null),
|
||||
updatedAt: new Date().toISOString(),
|
||||
lastAction: "reserve-remove"
|
||||
lastAction: adminMode ? "reserve-remove-admin" : "reserve-remove"
|
||||
};
|
||||
await writeJson(files.station, runtime.station);
|
||||
await appendAudit("station.reserve.remove", user, { queueLength: removed.reservations.length });
|
||||
await appendAudit("station.reserve.remove", actorUser, {
|
||||
queueLength: removed.reservations.length,
|
||||
targetUserId: targetId,
|
||||
mode: adminMode ? "admin" : "self"
|
||||
});
|
||||
broadcastEvent("station.status.changed", buildStationStatusView());
|
||||
return sendJson(res, 200, { ok: true, status: buildStationStatusView() });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user