mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 12:52:25 +03:00
feat(volcengine): phone/SMS auto-login for console with MFA + identity selection
- Session-based headless login service (volcengineConsoleAutoLogin)
- API: POST /connect {phone} + /code /status /cancel /resend /identity sub-routes
- Dashboard modal: phone → SMS code → MFA step-up → identity selection
- Falls back to the legacy headful manual flow on risk-control/TOTP-binding
- Route guard: connect subtree stays LOCAL_ONLY + spawn-capable
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
/**
|
||||
* POST /api/providers/volcengine-plan/connect/[sessionId]/cancel
|
||||
* Cancel an auto phone login session and close its headless browser.
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
): Promise<NextResponse> {
|
||||
const auth = await requireManagementAuth(request);
|
||||
if (auth) return auth;
|
||||
|
||||
const { sessionId } = await params;
|
||||
|
||||
try {
|
||||
const { volcengineConsoleAutoLoginService } =
|
||||
await import("@omniroute/open-sse/services/volcengineConsoleAutoLogin.ts");
|
||||
const session = await volcengineConsoleAutoLoginService.cancel(sessionId);
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, session });
|
||||
} catch {
|
||||
return NextResponse.json({ success: false, error: "Cancel failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { bindVolcenginePlansFromConsoleCredentials } from "@/lib/providers/volcenginePlanBinding";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
/**
|
||||
* POST /api/providers/volcengine-plan/connect/[sessionId]/code
|
||||
* Submit the SMS verification code (plus image captcha when required) for an
|
||||
* auto phone login session. Returns the session view; binding runs lazily on
|
||||
* the next status poll once credentials are extracted.
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
): Promise<NextResponse> {
|
||||
const auth = await requireManagementAuth(request);
|
||||
if (auth) return auth;
|
||||
|
||||
const { sessionId } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
|
||||
try {
|
||||
const { volcengineConsoleAutoLoginService } =
|
||||
await import("@omniroute/open-sse/services/volcengineConsoleAutoLogin.ts");
|
||||
|
||||
if (!volcengineConsoleAutoLoginService.getStatus(sessionId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
|
||||
const session = await volcengineConsoleAutoLoginService.submitCode(
|
||||
sessionId,
|
||||
String(body.code ?? ""),
|
||||
typeof body.captcha === "string" ? body.captcha : undefined,
|
||||
{ timeout }
|
||||
);
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Credentials ready → bind immediately so the response carries the outcome.
|
||||
if (session.phase === "success") {
|
||||
const bound = await volcengineConsoleAutoLoginService.withBinding(sessionId, (credentials) =>
|
||||
bindVolcenginePlansFromConsoleCredentials(credentials)
|
||||
);
|
||||
return NextResponse.json({ success: true, session: bound ?? session });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, session });
|
||||
} catch (error) {
|
||||
const message = sanitizeErrorMessage(error instanceof Error ? error.message : error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Volcano code submission failed: ${message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { bindVolcenginePlansFromConsoleCredentials } from "@/lib/providers/volcenginePlanBinding";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
/**
|
||||
* POST /api/providers/volcengine-plan/connect/[sessionId]/identity
|
||||
* Pick an identity on the console's select_identity page (the phone maps to
|
||||
* multiple accounts) and finish the login + plan binding.
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
): Promise<NextResponse> {
|
||||
const auth = await requireManagementAuth(request);
|
||||
if (auth) return auth;
|
||||
|
||||
const { sessionId } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
|
||||
try {
|
||||
const { volcengineConsoleAutoLoginService } =
|
||||
await import("@omniroute/open-sse/services/volcengineConsoleAutoLogin.ts");
|
||||
|
||||
if (!volcengineConsoleAutoLoginService.getStatus(sessionId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const index = Number(body.index);
|
||||
if (!Number.isInteger(index) || index < 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid identity index" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
|
||||
const session = await volcengineConsoleAutoLoginService.selectIdentity(sessionId, index, {
|
||||
timeout,
|
||||
});
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Credentials ready → bind immediately so the response carries the outcome.
|
||||
if (session.phase === "success") {
|
||||
const bound = await volcengineConsoleAutoLoginService.withBinding(sessionId, (credentials) =>
|
||||
bindVolcenginePlansFromConsoleCredentials(credentials)
|
||||
);
|
||||
return NextResponse.json({ success: true, session: bound ?? session });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, session });
|
||||
} catch (error) {
|
||||
const message = sanitizeErrorMessage(error instanceof Error ? error.message : error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Volcano identity selection failed: ${message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
|
||||
/**
|
||||
* POST /api/providers/volcengine-plan/connect/[sessionId]/resend
|
||||
* Re-trigger the SMS verification code for an active login session.
|
||||
*/
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
): Promise<NextResponse> {
|
||||
const auth = await requireManagementAuth(request);
|
||||
if (auth) return auth;
|
||||
|
||||
const { sessionId } = await params;
|
||||
|
||||
try {
|
||||
const { volcengineConsoleAutoLoginService } =
|
||||
await import("@omniroute/open-sse/services/volcengineConsoleAutoLogin.ts");
|
||||
const session = await volcengineConsoleAutoLoginService.resendCode(sessionId);
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, session });
|
||||
} catch {
|
||||
return NextResponse.json({ success: false, error: "Resend failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { bindVolcenginePlansFromConsoleCredentials } from "@/lib/providers/volcenginePlanBinding";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
/**
|
||||
* GET /api/providers/volcengine-plan/connect/[sessionId]/status
|
||||
* Poll an auto phone login session. When credentials have been extracted, the
|
||||
* plan binding runs lazily (deduped) and its result is attached to the view.
|
||||
*/
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
): Promise<NextResponse> {
|
||||
const auth = await requireManagementAuth(request);
|
||||
if (auth) return auth;
|
||||
|
||||
const { sessionId } = await params;
|
||||
|
||||
try {
|
||||
const { volcengineConsoleAutoLoginService } =
|
||||
await import("@omniroute/open-sse/services/volcengineConsoleAutoLogin.ts");
|
||||
|
||||
const session = await volcengineConsoleAutoLoginService.withBinding(sessionId, (credentials) =>
|
||||
bindVolcenginePlansFromConsoleCredentials(credentials)
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Unknown or expired Volcano login session" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: session.phase === "success", session });
|
||||
} catch (error) {
|
||||
const message = sanitizeErrorMessage(error instanceof Error ? error.message : error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Volcano login status failed: ${message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,26 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
|
||||
|
||||
// Auto flow: phone present → start a session-based headless phone/SMS login.
|
||||
if (typeof body.phone === "string" && body.phone.trim()) {
|
||||
try {
|
||||
const { volcengineConsoleAutoLoginService } =
|
||||
await import("@omniroute/open-sse/services/volcengineConsoleAutoLogin.ts");
|
||||
const started = await volcengineConsoleAutoLoginService.startLogin(body.phone, { timeout });
|
||||
if (!started.ok) {
|
||||
return NextResponse.json({ success: false, error: started.error }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ success: true, session: started.session });
|
||||
} catch (error) {
|
||||
const message = sanitizeErrorMessage(error instanceof Error ? error.message : error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Volcano auto login failed to start: ${message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy manual flow: headful browser login on the server machine.
|
||||
try {
|
||||
const { inAppLoginService } = await import("@omniroute/open-sse/services/inAppLoginService.ts");
|
||||
const login = await inAppLoginService.startLogin("volcengine-console", { timeout });
|
||||
|
||||
Reference in New Issue
Block a user