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,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import StrategyTimeline from "@/components/StrategyTimeline";
|
import StrategyTimeline from "@/components/StrategyTimeline";
|
||||||
|
import AutoLoginSetup from "@/components/AutoLoginSetup";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
@ -1667,6 +1668,15 @@ export default function PortfolioSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isConnected && (
|
||||||
|
<div
|
||||||
|
className={`${revealTransition} ${cardRevealClass}`}
|
||||||
|
style={prefersReducedMotion ? undefined : { transitionDelay: "650ms" }}
|
||||||
|
>
|
||||||
|
<AutoLoginSetup />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl p-6 space-y-4 ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
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" }}
|
style={prefersReducedMotion ? undefined : { transitionDelay: "700ms" }}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user