Expose Launcher session validation for service handoff

This commit is contained in:
DCCONSTRUCTIONS 2026-06-23 16:19:22 +03:00
parent 34ffa8ca4d
commit 71c62579dc
1 changed files with 73 additions and 8 deletions

View File

@ -436,7 +436,7 @@ app.get("/api/services/:serviceSlug/optional-launch", (req, res) => {
return; return;
} }
redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/"); redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/", session);
}); });
app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => { app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => {
@ -449,7 +449,7 @@ app.get("/api/services/:serviceSlug/launch", requireSession, (req, res) => {
return; return;
} }
redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/"); redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, req.query.next_path || req.query.returnTo || "/", req.nodedcSession);
}); });
app.post("/api/internal/handoff/consume", (req, res) => { app.post("/api/internal/handoff/consume", (req, res) => {
@ -486,6 +486,7 @@ app.post("/api/internal/handoff/consume", (req, res) => {
res.json({ res.json({
ok: true, ok: true,
serviceSlug, serviceSlug,
launcherSessionId: handoff.launcherSessionId ?? null,
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email,
@ -498,6 +499,69 @@ app.post("/api/internal/handoff/consume", (req, res) => {
}); });
}); });
app.post("/api/internal/session/validate", (req, res) => {
if (!isInternalRequestAuthorized(req)) {
res.status(config.internalAccessToken ? 401 : 503).json({
ok: false,
active: false,
error: config.internalAccessToken ? "internal_session_unauthorized" : "internal_session_not_configured",
});
return;
}
const serviceSlug = sanitizeServiceSlug(req.body?.serviceSlug);
const launcherSessionId = typeof req.body?.launcherSessionId === "string" ? req.body.launcherSessionId : "";
if (!launcherSessionId) {
res.json({ ok: true, active: false, error: "launcher_session_missing" });
return;
}
const session = sessions.get(launcherSessionId);
if (!session || session.expiresAt < Date.now()) {
if (session) sessions.delete(session.id);
res.json({ ok: true, active: false, error: "launcher_session_not_found" });
return;
}
const sessionAccess = getSessionAccessState(session);
if (!sessionAccess.ok) {
sessions.delete(session.id);
res.json({ ok: true, active: false, error: sessionAccess.error || "launcher_session_inactive" });
return;
}
const snapshot = controlPlaneStore.getSnapshot({ name: "NODE.DC session validation" });
const runtimeContext = sessionAccess.runtimeContext;
const user = findInternalAccessUser(snapshot.data, {
subject: runtimeContext.user.sub,
email: runtimeContext.user.email,
});
const groups = user ? resolveRequiredGroups(snapshot.data, user) : runtimeContext.groups;
const app = getAppsForUser(groups).find((candidate) => candidate.slug === serviceSlug);
if (!user || !app?.hasAccess || app.status !== "active") {
res.json({ ok: true, active: false, error: "service_access_denied" });
return;
}
res.json({
ok: true,
active: true,
serviceSlug,
launcherSessionId,
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: resolveUserAvatarPublicUrl(user),
subject: user.authentikUserId || runtimeContext.user.sub,
authentikUserId: user.authentikUserId ?? null,
groups,
},
});
});
app.get("/api/profile", requireSession, (req, res) => { app.get("/api/profile", requireSession, (req, res) => {
const { actor, data } = getLauncherProfileContext(req.nodedcSession); const { actor, data } = getLauncherProfileContext(req.nodedcSession);
const user = findLauncherUser(data, actor.id); const user = findLauncherUser(data, actor.id);
@ -1111,7 +1175,7 @@ app.get("/tasker-workspace-invite/:taskerInviteRequestId", (req, res) => {
return; return;
} }
const handoffToken = createServiceHandoff("task-manager", runtimeContext.user); const handoffToken = createServiceHandoff("task-manager", runtimeContext.user, { launcherSessionId: session.id });
const taskBaseUrl = getTaskBaseUrl(); const taskBaseUrl = getTaskBaseUrl();
const targetUrl = new URL("/auth/nodedc/handoff/", taskBaseUrl); const targetUrl = new URL("/auth/nodedc/handoff/", taskBaseUrl);
targetUrl.searchParams.set("token", handoffToken); targetUrl.searchParams.set("token", handoffToken);
@ -3147,9 +3211,9 @@ function buildOptionalServiceFallbackUrl(serviceSlug, app, fallbackPath) {
} }
} }
function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, returnTo = "/") { function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, returnTo = "/", session = null) {
if (isEngineServiceSlug(serviceSlug)) { if (isEngineServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff("nodedc", runtimeContext.user); const handoffToken = createServiceHandoff("nodedc", runtimeContext.user, { launcherSessionId: session?.id });
const targetUrl = new URL("/auth/nodedc/handoff/", getEngineLaunchTargetUrl(app)); const targetUrl = new URL("/auth/nodedc/handoff/", getEngineLaunchTargetUrl(app));
const nextPath = sanitizeReturnTo(returnTo); const nextPath = sanitizeReturnTo(returnTo);
@ -3164,7 +3228,7 @@ function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, re
} }
if (isBimViewerServiceSlug(serviceSlug)) { if (isBimViewerServiceSlug(serviceSlug)) {
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user); const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id });
const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app)); const targetUrl = new URL("/auth/nodedc/handoff", getBimViewerBaseUrl(app));
const nextPath = sanitizeReturnTo(returnTo); const nextPath = sanitizeReturnTo(returnTo);
@ -3183,7 +3247,7 @@ function redirectToServiceLaunchTarget(res, serviceSlug, runtimeContext, app, re
return; return;
} }
const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user); const handoffToken = createServiceHandoff(serviceSlug, runtimeContext.user, { launcherSessionId: session?.id });
const targetUrl = new URL("/auth/nodedc/handoff/", getTaskBaseUrl()); const targetUrl = new URL("/auth/nodedc/handoff/", getTaskBaseUrl());
const nextPath = sanitizeReturnTo(returnTo); const nextPath = sanitizeReturnTo(returnTo);
@ -3626,12 +3690,13 @@ function resolveTaskManagerWorkspaceAssignments(data, user) {
return [...bySlug.values()]; return [...bySlug.values()];
} }
function createServiceHandoff(serviceSlug, user) { function createServiceHandoff(serviceSlug, user, { launcherSessionId = null } = {}) {
pruneExpiredServiceHandoffs(); pruneExpiredServiceHandoffs();
const token = randomBase64Url(48); const token = randomBase64Url(48);
serviceHandoffs.set(token, { serviceHandoffs.set(token, {
serviceSlug, serviceSlug,
launcherSessionId: typeof launcherSessionId === "string" && launcherSessionId ? launcherSessionId : null,
user: { user: {
sub: user.sub, sub: user.sub,
email: user.email, email: user.email,