ФУНКЦИИ - NODEDC LAUNCHER: direct auth redirect

This commit is contained in:
DCCONSTRUCTIONS 2026-05-04 21:39:56 +03:00
parent a1fb2d0f83
commit a1e8cacd88
3 changed files with 47 additions and 10 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
*.tsbuildinfo
public/storage/backups/

View File

@ -16,6 +16,7 @@ const maxStorageJsonBodyBytes = "260mb";
const pendingLoginTtlMs = 10 * 60 * 1000;
const sessionTtlMs = 12 * 60 * 60 * 1000;
const oidcStateCookieName = "nodedc_oidc_state";
const maxOidcStateCookieEntries = 8;
const sessionCookieName = "nodedc_session";
loadEnvFiles([
@ -66,7 +67,7 @@ app.get("/auth/login", asyncRoute(async (req, res) => {
expiresAt: Date.now() + pendingLoginTtlMs,
});
res.cookie(oidcStateCookieName, state, cookieOptions(pendingLoginTtlMs));
setOidcStateCookie(res, [state, ...getValidOidcCookieStates(req)].slice(0, maxOidcStateCookieEntries));
const authorizationUrl = new URL(discovery.authorization_endpoint);
authorizationUrl.searchParams.set("response_type", "code");
@ -97,15 +98,17 @@ app.get("/auth/callback", asyncRoute(async (req, res) => {
const code = typeof req.query.code === "string" ? req.query.code : null;
const state = typeof req.query.state === "string" ? req.query.state : null;
const cookieState = parseCookies(req.headers.cookie)[oidcStateCookieName];
const cookieStates = getValidOidcCookieStates(req);
if (!code || !state || state !== cookieState) {
throw new Error("OIDC callback state validation failed");
if (!code || !state || !cookieStates.includes(state)) {
res.clearCookie(oidcStateCookieName, clearCookieOptions());
res.redirect("/auth/login?returnTo=/");
return;
}
const pendingLogin = pendingLogins.get(state);
pendingLogins.delete(state);
res.clearCookie(oidcStateCookieName, clearCookieOptions());
setOidcStateCookie(res, cookieStates.filter((cookieState) => cookieState !== state));
if (!pendingLogin || pendingLogin.expiresAt < Date.now()) {
throw new Error("OIDC login state expired");
@ -1073,6 +1076,36 @@ function pruneExpiredState() {
}
}
function getValidOidcCookieStates(req) {
const rawValue = parseCookies(req.headers.cookie)[oidcStateCookieName];
if (!rawValue) return [];
const seen = new Set();
return rawValue
.split(".")
.filter((state) => /^[A-Za-z0-9_-]{32,256}$/.test(state))
.filter((state) => {
if (seen.has(state)) return false;
seen.add(state);
return true;
})
.filter((state) => {
const pendingLogin = pendingLogins.get(state);
return Boolean(pendingLogin && pendingLogin.expiresAt >= Date.now());
});
}
function setOidcStateCookie(res, states) {
if (!states.length) {
res.clearCookie(oidcStateCookieName, clearCookieOptions());
return;
}
res.cookie(oidcStateCookieName, states.join("."), cookieOptions(pendingLoginTtlMs));
}
function parseCookies(cookieHeader) {
if (!cookieHeader) return {};

View File

@ -62,7 +62,6 @@ export function LauncherApp() {
const [adminOpen, setAdminOpen] = useState(false);
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
const [authError, setAuthError] = useState<string | null>(null);
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
@ -160,7 +159,6 @@ export function LauncherApp() {
if (!isMounted) return;
setAuthSession(session);
setAuthError(null);
if (!session.authenticated) {
setAuthApps([]);
@ -178,7 +176,7 @@ export function LauncherApp() {
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
setAuthApps([]);
setAuthError(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
});
return () => {
@ -186,6 +184,12 @@ export function LauncherApp() {
};
}, []);
useEffect(() => {
if (!authSession || authSession.authenticated) return;
window.location.replace(authSession.loginUrl || "/auth/login");
}, [authSession]);
useEffect(() => {
if (!authSession?.authenticated) return;
@ -247,7 +251,6 @@ export function LauncherApp() {
if (!isMounted) return;
setAuthSession(nextSession);
setAuthError(null);
if (!nextSession.authenticated) {
setAuthApps([]);
@ -473,7 +476,7 @@ export function LauncherApp() {
}
if (!authSession.authenticated) {
return <AuthStateScreen title="Вход на платформу NODE.DC" description="Войдите, чтобы открыть рабочую область и доступы." error={authError} loginUrl={authSession.loginUrl} />;
return null;
}
return (