diff --git a/backend/api.py b/backend/api.py index 3a3bbde3..fe2ba88c 100644 --- a/backend/api.py +++ b/backend/api.py @@ -187,6 +187,9 @@ api_router.include_router(workflows_router, prefix="/workflows") from pipedream import api as pipedream_api api_router.include_router(pipedream_api.router) +from auth import phone_verification_supabase_mfa +api_router.include_router(phone_verification_supabase_mfa.router) + @api_router.get("/health") async def health_check(): logger.info("Health check endpoint called") diff --git a/backend/auth/__init__.py b/backend/auth/__init__.py new file mode 100644 index 00000000..3b3ce42e --- /dev/null +++ b/backend/auth/__init__.py @@ -0,0 +1 @@ +# Auth Module \ No newline at end of file diff --git a/backend/auth/phone_verification_supabase_mfa.py b/backend/auth/phone_verification_supabase_mfa.py new file mode 100644 index 00000000..5e835b16 --- /dev/null +++ b/backend/auth/phone_verification_supabase_mfa.py @@ -0,0 +1,290 @@ +""" +Auth MFA endpoints for Supabase TOTP-based Multi-Factor Authentication (MFA). + +Currently, only TOTP is supported as a second factor. Users can enroll up to 10 TOTP factors. +No recovery codes are supported, but users can enroll multiple TOTP factors for backup. + +This API provides endpoints to: +- Enroll a TOTP factor +- Create a challenge for a factor +- Verify a challenge +- Create and verify a challenge in a single step +- List enrolled factors +- Unenroll a factor +- Get Authenticator Assurance Level (AAL) +""" + +from fastapi import APIRouter, Depends, HTTPException, Request +from pydantic import BaseModel, Field +from typing import List, Optional +import jwt +from supabase import create_client, Client +from utils.auth_utils import get_current_user_id_from_jwt +from utils.config import config + +router = APIRouter(prefix="/mfa", tags=["MFA"]) + +# Initialize Supabase client with anon key for user operations +supabase_url = config.SUPABASE_URL +supabase_anon_key = config.SUPABASE_ANON_KEY + +def get_authenticated_client(request: Request) -> Client: + """ + Create a Supabase client authenticated with the user's JWT token. + This approach uses the JWT token directly for server-side authentication. + """ + # Extract the JWT token from the Authorization header + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing or invalid Authorization header") + + token = auth_header.split(" ")[1] + + # Create a new Supabase client with the anon key + client = create_client(supabase_url, supabase_anon_key) + + # Set the session with the JWT token + # For server-side operations, we can use the token directly + try: + # Verify the token is valid by getting the user + user_response = client.auth.get_user(token) + if not user_response.user: + raise HTTPException(status_code=401, detail="Invalid token") + + # Set the session with the token + client.auth.set_session(token, None) + return client + except Exception as e: + raise HTTPException(status_code=401, detail=f"Authentication failed: {str(e)}") + +# Request/Response Models +class EnrollFactorRequest(BaseModel): + factor_type: str = Field(default="totp", description="Type of factor to enroll") + friendly_name: str = Field(..., description="User-friendly name for the factor") + +class EnrollFactorResponse(BaseModel): + id: str + friendly_name: str + factor_type: str + status: str + created_at: str + updated_at: str + +class ChallengeRequest(BaseModel): + factor_id: str = Field(..., description="ID of the factor to challenge") + +class ChallengeResponse(BaseModel): + id: str + factor_type: str + created_at: str + expires_at: str + +class VerifyRequest(BaseModel): + factor_id: str = Field(..., description="ID of the factor to verify") + challenge_id: str = Field(..., description="ID of the challenge to verify") + code: str = Field(..., description="TOTP code to verify") + +class ChallengeAndVerifyRequest(BaseModel): + factor_id: str = Field(..., description="ID of the factor to challenge and verify") + code: str = Field(..., description="TOTP code to verify") + +class FactorInfo(BaseModel): + id: str + friendly_name: str + factor_type: str + status: str + created_at: str + updated_at: str + +class ListFactorsResponse(BaseModel): + factors: List[FactorInfo] + +class UnenrollRequest(BaseModel): + factor_id: str = Field(..., description="ID of the factor to unenroll") + +class AALResponse(BaseModel): + current_level: str + next_level: str + current_authentication_methods: List[str] + +@router.post("/enroll", response_model=EnrollFactorResponse) +async def enroll_factor( + request_data: EnrollFactorRequest, + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + Enroll a new TOTP factor for the authenticated user. + + Currently only supports 'totp' factor type. + Users can enroll up to 10 TOTP factors. + """ + try: + response = client.auth.mfa.enroll({ + "factor_type": request_data.factor_type, + "friendly_name": request_data.friendly_name + }) + + return EnrollFactorResponse( + id=response.id, + friendly_name=response.friendly_name, + factor_type=response.type, + ) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to enroll factor: {str(e)}") + +@router.post("/challenge", response_model=ChallengeResponse) +async def create_challenge( + request_data: ChallengeRequest, + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + Create a challenge for an enrolled factor. + + The challenge must be verified within the time limit. + """ + try: + response = client.auth.mfa.challenge({ + "factor_id": request_data.factor_id, + }) + + return ChallengeResponse( + id=response.id, + factor_type=response.factor_type, + expires_at=response.expires_at + ) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to create challenge: {str(e)}") + +@router.post("/verify") +async def verify_challenge( + request_data: VerifyRequest, + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + Verify a challenge with a TOTP code. + + The challenge must be active and the code must be valid. + """ + try: + response = client.auth.mfa.verify({ + "factor_id": request_data.factor_id, + "challenge_id": request_data.challenge_id, + "code": request_data.code + }) + + return { + "success": True, + "message": "Challenge verified successfully", + "session": response, + } + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to verify challenge: {str(e)}") + +@router.post("/challenge-and-verify") +async def challenge_and_verify( + request_data: ChallengeAndVerifyRequest, + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + Create a challenge and verify it in a single step. + + This is a convenience method that combines challenge creation and verification. + """ + try: + response = client.auth.mfa.challenge_and_verify({ + "factor_id": request_data.factor_id, + "code": request_data.code + }) + + return { + "success": True, + "message": "Challenge created and verified successfully", + "session": response, + } + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to challenge and verify: {str(e)}") + +@router.get("/factors", response_model=ListFactorsResponse) +async def list_factors( + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + List all enrolled factors for the authenticated user. + """ + try: + # Get the current session to access user's factors + session = client.auth.get_session() + if not session: + raise HTTPException(status_code=401, detail="No active session") + + # Get user info which includes factors + user_response = client.auth.get_user() + if not user_response.user: + raise HTTPException(status_code=401, detail="User not found") + + # Extract factors from user data + factors = [] + if hasattr(user_response.user, 'factors') and user_response.user.factors: + for factor in user_response.user.factors: + factors.append(FactorInfo( + id=factor.id, + friendly_name=factor.friendly_name, + factor_type=factor.factor_type, + status=factor.status, + created_at=factor.created_at, + updated_at=factor.updated_at + )) + + return ListFactorsResponse(factors=factors) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to list factors: {str(e)}") + +@router.delete("/unenroll") +async def unenroll_factor( + request_data: UnenrollRequest, + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + Unenroll a factor for the authenticated user. + + This will remove the factor and invalidate any active sessions if the factor was verified. + """ + try: + response = client.auth.mfa.unenroll({ + "factor_id": request_data.factor_id + }) + + return { + "success": True, + "message": "Factor unenrolled successfully" + } + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to unenroll factor: {str(e)}") + +@router.get("/aal", response_model=AALResponse) +async def get_authenticator_assurance_level( + client: Client = Depends(get_authenticated_client), + user_id: str = Depends(get_current_user_id_from_jwt) +): + """ + Get the Authenticator Assurance Level (AAL) for the current session. + + AAL1: First factor only (email/password, OAuth) + AAL2: Second factor required (TOTP) + """ + try: + response = client.auth.mfa.get_authenticator_assurance_level() + + return AALResponse( + current_level=response.current_level, + next_level=response.next_level, + current_authentication_methods=response.current_authentication_methods + ) + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to get AAL: {str(e)}") diff --git a/frontend/src/app/auth/phone-verification/page.tsx b/frontend/src/app/auth/phone-verification/page.tsx new file mode 100644 index 00000000..c4ee4b38 --- /dev/null +++ b/frontend/src/app/auth/phone-verification/page.tsx @@ -0,0 +1,5 @@ +import { PhoneVerificationPage } from "@/components/auth/phone-verification/phone-verification-page"; + +export default function PhoneVerificationRoute() { + return ; +} \ No newline at end of file diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index b88f159a..d2afe88a 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -5,6 +5,7 @@ import { useState, createContext, useEffect } from 'react'; import { AuthProvider } from '@/components/AuthProvider'; import { ReactQueryProvider } from '@/providers/react-query-provider'; import { dehydrate, QueryClient } from '@tanstack/react-query'; +import { PhoneVerificationGuard } from '@/components/auth/phone-verification/phone-verification-guard'; export interface ParsedTag { tagName: string; @@ -45,7 +46,9 @@ export function Providers({ children }: { children: React.ReactNode }) { - {children} + + {children} + diff --git a/frontend/src/components/auth/phone-verification/otp-verification.tsx b/frontend/src/components/auth/phone-verification/otp-verification.tsx new file mode 100644 index 00000000..f677076b --- /dev/null +++ b/frontend/src/components/auth/phone-verification/otp-verification.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2 } from "lucide-react"; + +interface OtpVerificationProps { + phoneNumber: string; + onVerify: (otp: string) => Promise; + onResend: () => Promise; + isLoading?: boolean; + error?: string | null; +} + +export function OtpVerification({ + phoneNumber, + onVerify, + onResend, + isLoading = false, + error = null +}: OtpVerificationProps) { + const [otp, setOtp] = useState(["", "", "", "", "", ""]); + const [localError, setLocalError] = useState(null); + const [canResend, setCanResend] = useState(false); + const [countdown, setCountdown] = useState(30); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + // Focus first input on mount + inputRefs.current[0]?.focus(); + + // Start countdown timer + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + setCanResend(true); + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, []); + + const handleOtpChange = (index: number, value: string) => { + setLocalError(null); + + // Only allow single digit + if (value.length > 1) { + value = value.slice(-1); + } + + // Only allow digits + if (value && !/^\d$/.test(value)) { + return; + } + + const newOtp = [...otp]; + newOtp[index] = value; + setOtp(newOtp); + + // Auto-focus next input + if (value && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = (index: number, e: React.KeyboardEvent) => { + if (e.key === "Backspace" && !otp[index] && index > 0) { + // Move to previous input on backspace if current is empty + inputRefs.current[index - 1]?.focus(); + } + }; + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const pastedData = e.clipboardData.getData("text"); + const digits = pastedData.replace(/\D/g, "").slice(0, 6); + + if (digits.length === 6) { + const newOtp = digits.split(""); + setOtp(newOtp); + inputRefs.current[5]?.focus(); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLocalError(null); + + const otpCode = otp.join(""); + + if (otpCode.length !== 6) { + setLocalError("Please enter a 6-digit code"); + return; + } + + await onVerify(otpCode); + }; + + const handleResend = async () => { + setCanResend(false); + setCountdown(30); + setOtp(["", "", "", "", "", ""]); + setLocalError(null); + + await onResend(); + + // Restart countdown + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + setCanResend(true); + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + }; + + return ( + + + Enter Verification Code + + We've sent a 6-digit code to {phoneNumber} + + + +
+
+ +
+ {otp.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleOtpChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + className="w-12 h-12 text-center text-lg font-bold" + disabled={isLoading} + /> + ))} +
+
+ + {(error || localError) && ( + + {error || localError} + + )} + + + +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/auth/phone-verification/phone-input.tsx b/frontend/src/components/auth/phone-verification/phone-input.tsx new file mode 100644 index 00000000..8a3af77c --- /dev/null +++ b/frontend/src/components/auth/phone-verification/phone-input.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2 } from "lucide-react"; + +interface PhoneInputProps { + onSubmit: (phoneNumber: string) => Promise; + isLoading?: boolean; + error?: string | null; +} + +export function PhoneInput({ onSubmit, isLoading = false, error = null }: PhoneInputProps) { + const [phoneNumber, setPhoneNumber] = useState(""); + const [localError, setLocalError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLocalError(null); + + // Basic validation + if (!phoneNumber.trim()) { + setLocalError("Please enter a phone number"); + return; + } + + // Simple phone number validation (international format) + const phoneRegex = /^\+?[1-9]\d{1,14}$/; + if (!phoneRegex.test(phoneNumber.replace(/\s/g, ""))) { + setLocalError("Please enter a valid phone number"); + return; + } + + await onSubmit(phoneNumber); + }; + + return ( + + + Phone Verification + + Enter your phone number to receive a verification code via SMS + + + +
+
+ + setPhoneNumber(e.target.value)} + disabled={isLoading} + className="text-lg" + /> +

+ Include country code (e.g., +1 for US) +

+
+ + {(error || localError) && ( + + {error || localError} + + )} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/auth/phone-verification/phone-verification-guard.tsx b/frontend/src/components/auth/phone-verification/phone-verification-guard.tsx new file mode 100644 index 00000000..5da21268 --- /dev/null +++ b/frontend/src/components/auth/phone-verification/phone-verification-guard.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { usePhoneVerificationStatus } from '@/hooks/react-query/phone-verification'; +import { Loader2 } from 'lucide-react'; + +interface PhoneVerificationGuardProps { + children: React.ReactNode; +} + +export function PhoneVerificationGuard({ children }: PhoneVerificationGuardProps) { + const router = useRouter(); + const { data: status, isLoading, error, refetch } = usePhoneVerificationStatus(); + + useEffect(() => { + if (status && status.verification_required && !status.is_verified) { + router.push('/auth/phone-verification'); + } + }, [status, router]); + + if (isLoading) { + return ( +
+
+ +

Checking verification status...

+
+
+ ); + } + + if (error) { + return ( +
+
+

Verification Check Failed

+

{error.message}

+ +
+
+ ); + } + + // Only show children if user has verified phone or verification is not required + if (status?.is_verified || !status?.verification_required) { + return <>{children}; + } + + // Show full-screen lock for users requiring phone verification + return ( +
+
+
+ + + +
+ +
+

+ Phone Verification Required +

+

+ To ensure the security of your account, we need to verify your phone number before you can access the application. +

+
+ +
+ + +

+ This is a one-time verification process for account security. +

+
+
+
+ ); +} diff --git a/frontend/src/components/auth/phone-verification/phone-verification-page.tsx b/frontend/src/components/auth/phone-verification/phone-verification-page.tsx new file mode 100644 index 00000000..50ebc505 --- /dev/null +++ b/frontend/src/components/auth/phone-verification/phone-verification-page.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { PhoneInput } from "./phone-input"; +import { OtpVerification } from "./otp-verification"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2 } from "lucide-react"; + +interface PhoneVerificationPageProps { + onSuccess?: () => void; +} + +export function PhoneVerificationPage({ onSuccess }: PhoneVerificationPageProps) { + const [step, setStep] = useState<"phone" | "otp">("phone"); + const [phoneNumber, setPhoneNumber] = useState(""); + const [factorId, setFactorId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const router = useRouter(); + + const handlePhoneSubmit = async (phone: string) => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch("/api/phone/submit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ phone_number: phone }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to send verification code"); + } + + setPhoneNumber(phone); + setFactorId(data.factor_id); + setStep("otp"); + setSuccess("Verification code sent to your phone"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to send verification code"); + } finally { + setIsLoading(false); + } + }; + + const handleOtpVerify = async (otp: string) => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch("/api/phone/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + phone_number: phoneNumber, + otp_code: otp, + factor_id: factorId + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Invalid verification code"); + } + + setSuccess("Phone number verified successfully!"); + + // Redirect to dashboard after 1 second + setTimeout(() => { + if (onSuccess) { + onSuccess(); + } else { + router.push("/dashboard"); + } + }, 1000); + + } catch (err) { + setError(err instanceof Error ? err.message : "Invalid verification code"); + } finally { + setIsLoading(false); + } + }; + + const handleResendCode = async () => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch("/api/phone/submit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ phone_number: phoneNumber }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to resend verification code"); + } + + setSuccess("New verification code sent"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to resend verification code"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Phone Verification +

+

+ {step === "phone" + ? "Enter your phone number to receive a verification code" + : "Enter the 6-digit code sent to your phone" + } +

+
+ + {error && ( + + {error} + + )} + + {success && ( + + {success} + + )} + + {step === "phone" ? ( + + ) : ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/hooks/react-query/phone-verification.ts b/frontend/src/hooks/react-query/phone-verification.ts new file mode 100644 index 00000000..6259de36 --- /dev/null +++ b/frontend/src/hooks/react-query/phone-verification.ts @@ -0,0 +1,52 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { phoneVerificationService } from '@/lib/api/phone-verification'; + +interface PhoneSubmitData { + phone_number: string; + country_code: string; +} + +interface PhoneVerifyData { + phone_number: string; + otp_code: string; +} + +interface PhoneStatusResponse { + is_verified: boolean; + phone_number?: string; + verification_required: boolean; +} + +// React Query hooks +export const usePhoneVerificationStatus = () => { + return useQuery({ + queryKey: ['phone-verification-status'], + queryFn: phoneVerificationService.getStatus, + staleTime: 5 * 60 * 1000, // 5 minutes + retry: 2, + }); +}; + +export const useSubmitPhone = () => { + return useMutation({ + mutationFn: phoneVerificationService.submitPhoneNumber, + }); +}; + +export const useVerifyOtp = () => { + return useMutation({ + mutationFn: phoneVerificationService.verifyOTP, + }); +}; + +export const useResendOtp = () => { + return useMutation({ + mutationFn: phoneVerificationService.resendOTP, + }); +}; + +export const useRemovePhoneVerification = () => { + return useMutation({ + mutationFn: phoneVerificationService.removePhoneVerification, + }); +}; \ No newline at end of file diff --git a/frontend/src/lib/api/phone-verification.ts b/frontend/src/lib/api/phone-verification.ts new file mode 100644 index 00000000..224622c5 --- /dev/null +++ b/frontend/src/lib/api/phone-verification.ts @@ -0,0 +1,71 @@ +import { backendApi } from '@/lib/api-client'; + +export interface PhoneVerificationStatus { + is_verified: boolean; + phone_number?: string; + verification_required: boolean; +} + +export interface PhoneVerificationSubmit { + phone_number: string; + country_code: string; +} + +export interface PhoneVerificationVerify { + phone_number: string; + otp_code: string; +} + +export interface PhoneVerificationResponse { + success: boolean; + message?: string; + requires_otp?: boolean; +} + +export interface OTPVerificationResponse { + success: boolean; + message?: string; + is_verified?: boolean; +} + +export const phoneVerificationService = { + /** + * Get phone verification status for the current user + */ + async getStatus(): Promise { + const response = await backendApi.get('/mfa/phone/status'); + return response.data; + }, + + /** + * Submit phone number for verification + */ + async submitPhoneNumber(data: PhoneVerificationSubmit): Promise { + const response = await backendApi.post('/mfa/phone/submit', data); + return response.data; + }, + + /** + * Verify OTP code for phone verification + */ + async verifyOTP(data: PhoneVerificationVerify): Promise { + const response = await backendApi.post('/mfa/phone/verify', data); + return response.data; + }, + + /** + * Resend OTP code to phone number + */ + async resendOTP(phoneNumber: string): Promise { + const response = await backendApi.post('/mfa/phone/resend', { phone_number: phoneNumber }); + return response.data; + }, + + /** + * Remove phone verification from account + */ + async removePhoneVerification(): Promise { + const response = await backendApi.delete('/mfa/phone/remove'); + return response.data; + } +}; \ No newline at end of file