Add Auto-Login setup card to portfolio page
- New AutoLoginSetup component: form to save Zerodha credentials and TOTP secret, shows last refresh time, error state, manual refresh and remove buttons - New autoLogin.ts API client for all auto-login endpoints - AutoLoginSetup card rendered in PortfolioSection when broker is connected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46cf062fe2
commit
98894617e2
33
src/api/autoLogin.ts
Normal file
33
src/api/autoLogin.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
export type AutoLoginStatus = {
|
||||
configured: boolean;
|
||||
last_refreshed_at: string | null;
|
||||
last_error: string | null;
|
||||
};
|
||||
|
||||
export type AutoLoginSetupRequest = {
|
||||
zerodha_login_id: string;
|
||||
password: string;
|
||||
totp_secret: string;
|
||||
};
|
||||
|
||||
export async function getAutoLoginStatus(): Promise<AutoLoginStatus> {
|
||||
const res = await apiRequest("GET", "/auto-login/status");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function setupAutoLogin(data: AutoLoginSetupRequest): Promise<{ configured: boolean; message: string }> {
|
||||
const res = await apiRequest("POST", "/auto-login/setup", data);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function removeAutoLogin(): Promise<{ configured: boolean; message: string }> {
|
||||
const res = await apiRequest("DELETE", "/auto-login/setup");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function triggerAutoLogin(): Promise<{ success: boolean; message: string }> {
|
||||
const res = await apiRequest("POST", "/auto-login/trigger");
|
||||
return res.json();
|
||||
}
|
||||
227
src/components/AutoLoginSetup.tsx
Normal file
227
src/components/AutoLoginSetup.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ShieldCheck, ShieldOff, RefreshCw, Eye, EyeOff, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import {
|
||||
getAutoLoginStatus,
|
||||
setupAutoLogin,
|
||||
removeAutoLogin,
|
||||
triggerAutoLogin,
|
||||
} from "@/api/autoLogin";
|
||||
|
||||
export default function AutoLoginSetup() {
|
||||
const queryClient = useQueryClient();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showTotp, setShowTotp] = useState(false);
|
||||
const [form, setForm] = useState({ zerodha_login_id: "", password: "", totp_secret: "" });
|
||||
|
||||
const { data: status, isLoading } = useQuery({
|
||||
queryKey: ["/auto-login/status"],
|
||||
queryFn: getAutoLoginStatus,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
const setupMutation = useMutation({
|
||||
mutationFn: setupAutoLogin,
|
||||
onSuccess: () => {
|
||||
toast({ title: "Auto-login enabled", description: "Your Zerodha session will refresh automatically every morning." });
|
||||
setShowForm(false);
|
||||
setForm({ zerodha_login_id: "", password: "", totp_secret: "" });
|
||||
queryClient.invalidateQueries({ queryKey: ["/auto-login/status"] });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({ title: "Setup failed", description: err?.message ?? "Could not verify credentials. Check and try again.", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: removeAutoLogin,
|
||||
onSuccess: () => {
|
||||
toast({ title: "Auto-login removed", description: "You will need to reconnect Zerodha manually each day." });
|
||||
queryClient.invalidateQueries({ queryKey: ["/auto-login/status"] });
|
||||
},
|
||||
});
|
||||
|
||||
const triggerMutation = useMutation({
|
||||
mutationFn: triggerAutoLogin,
|
||||
onSuccess: () => {
|
||||
toast({ title: "Session refreshed", description: "Zerodha token has been refreshed successfully." });
|
||||
queryClient.invalidateQueries({ queryKey: ["/auto-login/status"] });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast({ title: "Refresh failed", description: err?.message ?? "Could not refresh session.", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.zerodha_login_id || !form.password || !form.totp_secret) {
|
||||
toast({ title: "All fields are required", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
setupMutation.mutate(form);
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
const configured = status?.configured ?? false;
|
||||
const lastRefreshed = status?.last_refreshed_at
|
||||
? new Date(status.last_refreshed_at).toLocaleString("en-IN", { timeZone: "Asia/Kolkata", dateStyle: "medium", timeStyle: "short" })
|
||||
: null;
|
||||
const hasError = !!status?.last_error;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-5 space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">Auto-Login</span>
|
||||
</div>
|
||||
{configured ? (
|
||||
<Badge variant="outline" className="text-xs border-green-500 text-green-500">
|
||||
Active
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
Not configured
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status info */}
|
||||
{configured && (
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
{lastRefreshed && <p>Last refreshed: {lastRefreshed} IST</p>}
|
||||
{hasError && (
|
||||
<p className="text-red-400">Last error: {status!.last_error}</p>
|
||||
)}
|
||||
<p className="text-muted-foreground/70">Token refreshes automatically at 6:05 AM IST daily.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!configured && !showForm && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enable auto-login so QuantFortune can refresh your Zerodha session every morning — your strategy runs uninterrupted.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action buttons when configured */}
|
||||
{configured && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs flex-1"
|
||||
onClick={() => triggerMutation.mutate()}
|
||||
disabled={triggerMutation.isPending}
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${triggerMutation.isPending ? "animate-spin" : ""}`} />
|
||||
Refresh Now
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs text-red-400 border-red-400/30 hover:bg-red-400/10"
|
||||
onClick={() => removeMutation.mutate()}
|
||||
disabled={removeMutation.isPending}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Setup button */}
|
||||
{!configured && !showForm && (
|
||||
<Button size="sm" className="w-full text-xs" onClick={() => setShowForm(true)}>
|
||||
<ShieldCheck className="h-3 w-3 mr-1" />
|
||||
Enable Auto-Login
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Setup form */}
|
||||
{!configured && showForm && (
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Zerodha Login ID</Label>
|
||||
<Input
|
||||
className="text-xs h-8"
|
||||
placeholder="e.g. AB1234"
|
||||
value={form.zerodha_login_id}
|
||||
onChange={(e) => setForm((f) => ({ ...f, zerodha_login_id: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Zerodha Password</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="text-xs h-8 pr-8"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Your Zerodha login password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">TOTP Secret Key</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="text-xs h-8 pr-8"
|
||||
type={showTotp ? "text" : "password"}
|
||||
placeholder="From Zerodha 2FA setup (not the 6-digit OTP)"
|
||||
value={form.totp_secret}
|
||||
onChange={(e) => setForm((f) => ({ ...f, totp_secret: e.target.value }))}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
onClick={() => setShowTotp((v) => !v)}
|
||||
>
|
||||
{showTotp ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
Find this in Zerodha → My Profile → Account Security → 2FA → View TOTP secret key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="flex-1 text-xs"
|
||||
disabled={setupMutation.isPending}
|
||||
>
|
||||
{setupMutation.isPending ? "Verifying..." : "Save & Verify"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
onClick={() => { setShowForm(false); setForm({ zerodha_login_id: "", password: "", totp_secret: "" }); }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -19,6 +19,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import StrategyTimeline from "@/components/StrategyTimeline";
|
||||
import AutoLoginSetup from "@/components/AutoLoginSetup";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import {
|
||||
ChartContainer,
|
||||
@ -1667,6 +1668,15 @@ export default function PortfolioSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isConnected && (
|
||||
<div
|
||||
className={`${revealTransition} ${cardRevealClass}`}
|
||||
style={prefersReducedMotion ? undefined : { transitionDelay: "650ms" }}
|
||||
>
|
||||
<AutoLoginSetup />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl p-6 space-y-4 ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
||||
style={prefersReducedMotion ? undefined : { transitionDelay: "700ms" }}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user