support magic link login (#3702)
This commit is contained in:
@@ -91,7 +91,7 @@ from skyvern.forge.sdk.schemas.runs import Run
|
||||
from skyvern.forge.sdk.schemas.task_generations import TaskGeneration
|
||||
from skyvern.forge.sdk.schemas.task_v2 import TaskV2, TaskV2Status, Thought, ThoughtType
|
||||
from skyvern.forge.sdk.schemas.tasks import OrderBy, SortDirection, Task, TaskStatus
|
||||
from skyvern.forge.sdk.schemas.totp_codes import TOTPCode
|
||||
from skyvern.forge.sdk.schemas.totp_codes import OTPType, TOTPCode
|
||||
from skyvern.forge.sdk.schemas.workflow_runs import WorkflowRunBlock
|
||||
from skyvern.forge.sdk.workflow.models.parameter import (
|
||||
AWSSecretParameter,
|
||||
@@ -2610,11 +2610,12 @@ class AgentDB:
|
||||
return None
|
||||
return TaskGeneration.model_validate(task_generation)
|
||||
|
||||
async def get_totp_codes(
|
||||
async def get_otp_codes(
|
||||
self,
|
||||
organization_id: str,
|
||||
totp_identifier: str,
|
||||
valid_lifespan_minutes: int = settings.TOTP_LIFESPAN_MINUTES,
|
||||
otp_type: OTPType | None = None,
|
||||
) -> list[TOTPCode]:
|
||||
"""
|
||||
1. filter by:
|
||||
@@ -2634,17 +2635,20 @@ class AgentDB:
|
||||
.filter_by(organization_id=organization_id)
|
||||
.filter_by(totp_identifier=totp_identifier)
|
||||
.filter(TOTPCodeModel.created_at > datetime.utcnow() - timedelta(minutes=valid_lifespan_minutes))
|
||||
.order_by(asc(all_null), TOTPCodeModel.created_at.desc())
|
||||
)
|
||||
if otp_type:
|
||||
query = query.filter(TOTPCodeModel.otp_type == otp_type)
|
||||
query = query.order_by(asc(all_null), TOTPCodeModel.created_at.desc())
|
||||
totp_code = (await session.scalars(query)).all()
|
||||
return [TOTPCode.model_validate(totp_code) for totp_code in totp_code]
|
||||
|
||||
async def create_totp_code(
|
||||
async def create_otp_code(
|
||||
self,
|
||||
organization_id: str,
|
||||
totp_identifier: str,
|
||||
content: str,
|
||||
code: str,
|
||||
otp_type: OTPType,
|
||||
task_id: str | None = None,
|
||||
workflow_id: str | None = None,
|
||||
workflow_run_id: str | None = None,
|
||||
@@ -2662,6 +2666,7 @@ class AgentDB:
|
||||
workflow_run_id=workflow_run_id,
|
||||
source=source,
|
||||
expired_at=expired_at,
|
||||
otp_type=otp_type,
|
||||
)
|
||||
session.add(new_totp_code)
|
||||
await session.commit()
|
||||
|
||||
@@ -54,7 +54,7 @@ STEP_PREFIX = "stp"
|
||||
TASK_GENERATION_PREFIX = "tg"
|
||||
TASK_PREFIX = "tsk"
|
||||
TASK_RUN_PREFIX = "tr"
|
||||
TOTP_CODE_PREFIX = "totp"
|
||||
OTP_CODE_PREFIX = "otp"
|
||||
USER_PREFIX = "u"
|
||||
WORKFLOW_PARAMETER_PREFIX = "wp"
|
||||
WORKFLOW_PERMANENT_ID_PREFIX = "wpid"
|
||||
@@ -169,9 +169,9 @@ def generate_ai_suggestion_id() -> str:
|
||||
return f"{AI_SUGGESTION_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_totp_code_id() -> str:
|
||||
def generate_otp_code_id() -> str:
|
||||
int_id = generate_id()
|
||||
return f"{TOTP_CODE_PREFIX}_{int_id}"
|
||||
return f"{OTP_CODE_PREFIX}_{int_id}"
|
||||
|
||||
|
||||
def generate_action_id() -> str:
|
||||
|
||||
@@ -35,6 +35,7 @@ from skyvern.forge.sdk.db.id import (
|
||||
generate_org_id,
|
||||
generate_organization_auth_token_id,
|
||||
generate_organization_bitwarden_collection_id,
|
||||
generate_otp_code_id,
|
||||
generate_output_parameter_id,
|
||||
generate_persistent_browser_session_id,
|
||||
generate_script_block_id,
|
||||
@@ -47,7 +48,6 @@ from skyvern.forge.sdk.db.id import (
|
||||
generate_task_run_id,
|
||||
generate_task_v2_id,
|
||||
generate_thought_id,
|
||||
generate_totp_code_id,
|
||||
generate_workflow_id,
|
||||
generate_workflow_parameter_id,
|
||||
generate_workflow_permanent_id,
|
||||
@@ -579,9 +579,12 @@ class AISuggestionModel(Base):
|
||||
|
||||
class TOTPCodeModel(Base):
|
||||
__tablename__ = "totp_codes"
|
||||
__table_args__ = (Index("ix_totp_codes_org_created_at", "organization_id", "created_at"),)
|
||||
__table_args__ = (
|
||||
Index("ix_totp_codes_org_created_at", "organization_id", "created_at"),
|
||||
Index("ix_totp_codes_otp_type", "organization_id", "otp_type"),
|
||||
)
|
||||
|
||||
totp_code_id = Column(String, primary_key=True, default=generate_totp_code_id)
|
||||
totp_code_id = Column(String, primary_key=True, default=generate_otp_code_id)
|
||||
totp_identifier = Column(String, nullable=False, index=True)
|
||||
organization_id = Column(String, ForeignKey("organizations.organization_id"))
|
||||
task_id = Column(String, ForeignKey("tasks.task_id"))
|
||||
@@ -593,6 +596,7 @@ class TOTPCodeModel(Base):
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False, index=True)
|
||||
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
|
||||
expired_at = Column(DateTime, index=True)
|
||||
otp_type = Column(String, server_default=sqlalchemy.text("'totp'"))
|
||||
|
||||
|
||||
class ActionModel(Base):
|
||||
|
||||
@@ -67,6 +67,7 @@ from skyvern.webeye.actions.actions import (
|
||||
DownloadFileAction,
|
||||
DragAction,
|
||||
ExtractAction,
|
||||
GotoUrlAction,
|
||||
InputTextAction,
|
||||
KeypressAction,
|
||||
LeftMouseAction,
|
||||
@@ -105,6 +106,7 @@ ACTION_TYPE_TO_CLASS = {
|
||||
ActionType.DRAG: DragAction,
|
||||
ActionType.VERIFICATION_CODE: VerificationCodeAction,
|
||||
ActionType.LEFT_MOUSE: LeftMouseAction,
|
||||
ActionType.GOTO_URL: GotoUrlAction,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,10 +29,11 @@ from skyvern.forge.sdk.schemas.organizations import (
|
||||
CreateOnePasswordTokenResponse,
|
||||
Organization,
|
||||
)
|
||||
from skyvern.forge.sdk.schemas.totp_codes import TOTPCode, TOTPCodeCreate
|
||||
from skyvern.forge.sdk.schemas.totp_codes import OTPType, TOTPCode, TOTPCodeCreate
|
||||
from skyvern.forge.sdk.services import org_auth_service
|
||||
from skyvern.forge.sdk.services.bitwarden import BitwardenService
|
||||
from skyvern.forge.sdk.services.credential.credential_vault_service import CredentialVaultService
|
||||
from skyvern.services.otp_service import OTPValue, parse_otp_login
|
||||
|
||||
LOG = structlog.get_logger()
|
||||
|
||||
@@ -59,6 +60,58 @@ async def parse_totp_code(content: str, organization_id: str) -> str | None:
|
||||
return code_resp.get("code", None)
|
||||
|
||||
|
||||
@legacy_base_router.post("/otp")
|
||||
@legacy_base_router.post("/otp/", include_in_schema=False)
|
||||
@base_router.post(
|
||||
"/credentials/otp",
|
||||
response_model=TOTPCode,
|
||||
summary="Send OTP content",
|
||||
description="Forward a OTP (TOTP, Magic Link) email or sms message containing otp login data to Skyvern. This endpoint stores the otp login data in database so that Skyvern can use it while running tasks/workflows.",
|
||||
tags=["Credentials"],
|
||||
openapi_extra={
|
||||
"x-fern-sdk-method-name": "send_otp_content",
|
||||
},
|
||||
)
|
||||
@base_router.post(
|
||||
"/credentials/otp/",
|
||||
response_model=TOTPCode,
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def send_otp_content(
|
||||
data: TOTPCodeCreate,
|
||||
curr_org: Organization = Depends(org_auth_service.get_current_org),
|
||||
) -> TOTPCode:
|
||||
content = data.content.strip()
|
||||
otp_value: OTPValue | None = OTPValue(value=content, type=OTPType.TOTP)
|
||||
# We assume the user is sending the code directly when the length of code is less than or equal to 10
|
||||
if len(content) > 10:
|
||||
otp_value = await parse_otp_login(content, curr_org.organization_id)
|
||||
|
||||
if not otp_value:
|
||||
LOG.error(
|
||||
"Failed to parse otp login",
|
||||
totp_identifier=data.totp_identifier,
|
||||
task_id=data.task_id,
|
||||
workflow_id=data.workflow_id,
|
||||
workflow_run_id=data.workflow_run_id,
|
||||
content=data.content,
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Failed to parse otp login")
|
||||
|
||||
return await app.DATABASE.create_otp_code(
|
||||
organization_id=curr_org.organization_id,
|
||||
totp_identifier=data.totp_identifier,
|
||||
content=data.content,
|
||||
code=otp_value.value,
|
||||
task_id=data.task_id,
|
||||
workflow_id=data.workflow_id,
|
||||
workflow_run_id=data.workflow_run_id,
|
||||
source=data.source,
|
||||
expired_at=data.expired_at,
|
||||
otp_type=otp_value.get_otp_type(),
|
||||
)
|
||||
|
||||
|
||||
@legacy_base_router.post("/totp")
|
||||
@legacy_base_router.post("/totp/", include_in_schema=False)
|
||||
@base_router.post(
|
||||
@@ -104,7 +157,7 @@ async def send_totp_code(
|
||||
content=data.content,
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Failed to parse totp code")
|
||||
return await app.DATABASE.create_totp_code(
|
||||
return await app.DATABASE.create_otp_code(
|
||||
organization_id=curr_org.organization_id,
|
||||
totp_identifier=data.totp_identifier,
|
||||
content=data.content,
|
||||
@@ -114,6 +167,7 @@ async def send_totp_code(
|
||||
workflow_run_id=data.workflow_run_id,
|
||||
source=data.source,
|
||||
expired_at=data.expired_at,
|
||||
otp_type=OTPType.TOTP,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
@@ -65,9 +66,15 @@ class TOTPCodeCreate(TOTPCodeBase):
|
||||
return sanitize_postgres_text(value)
|
||||
|
||||
|
||||
class OTPType(StrEnum):
|
||||
TOTP = "totp"
|
||||
MAGIC_LINK = "magic_link"
|
||||
|
||||
|
||||
class TOTPCode(TOTPCodeCreate):
|
||||
totp_code_id: str = Field(..., description="The skyvern ID of the TOTP code.")
|
||||
code: str = Field(..., description="The TOTP code extracted from the content.")
|
||||
organization_id: str = Field(..., description="The ID of the organization that the TOTP code is for.")
|
||||
created_at: datetime = Field(..., description="The timestamp when the TOTP code was created.")
|
||||
modified_at: datetime = Field(..., description="The timestamp when the TOTP code was modified.")
|
||||
otp_type: OTPType | None = Field(None, description="The type of the OTP code.")
|
||||
|
||||
Reference in New Issue
Block a user