feat: Auth-guard custom tickers and config endpoints
parent
2949da2937
commit
b8f590708d
|
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { SymbolInfo } from "@shared/schema";
|
import type { SymbolInfo } from "@shared/schema";
|
||||||
import { useSymbol } from "@/lib/symbol-context";
|
import { useSymbol } from "@/lib/symbol-context";
|
||||||
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -14,14 +15,16 @@ import {
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Trash2, Plus, X } from "lucide-react";
|
import { Trash2, Plus, X, Lock } from "lucide-react";
|
||||||
|
|
||||||
export function SymbolSelector() {
|
export function SymbolSelector() {
|
||||||
const { symbol, setSymbol } = useSymbol();
|
const { symbol, setSymbol } = useSymbol();
|
||||||
|
const { user } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [newTicker, setNewTicker] = useState("");
|
const [newTicker, setNewTicker] = useState("");
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const isAuthenticated = !!user;
|
||||||
|
|
||||||
const { data } = useQuery<SymbolInfo[]>({ queryKey: ["/api/symbols"] });
|
const { data } = useQuery<SymbolInfo[]>({ queryKey: ["/api/symbols"] });
|
||||||
const symbols = data ?? [];
|
const symbols = data ?? [];
|
||||||
|
|
@ -113,17 +116,21 @@ export function SymbolSelector() {
|
||||||
<span className="truncate text-xs text-muted-foreground">{s.name}</span>
|
<span className="truncate text-xs text-muted-foreground">{s.name}</span>
|
||||||
<Badge variant="secondary" className="text-[10px]">custom</Badge>
|
<Badge variant="secondary" className="text-[10px]">custom</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{isAuthenticated ? (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-5 w-5 -mr-2 hover:text-destructive"
|
size="icon"
|
||||||
onClick={(e) => {
|
className="h-5 w-5 -mr-2 hover:text-destructive"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
deleteMutation.mutate(s.ticker);
|
e.stopPropagation();
|
||||||
}}
|
deleteMutation.mutate(s.ticker);
|
||||||
>
|
}}
|
||||||
<Trash2 className="h-3 w-3" />
|
>
|
||||||
</Button>
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Lock className="h-3 w-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -139,10 +146,17 @@ export function SymbolSelector() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 text-xs gap-1"
|
className="h-7 text-xs gap-1"
|
||||||
onClick={() => setShowAdd(true)}
|
onClick={() => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
window.location.hash = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowAdd(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
Add Symbol
|
Add Symbol
|
||||||
|
{!isAuthenticated && <Lock className="h-3 w-3 text-muted-foreground" />}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1 w-full">
|
<div className="flex items-center gap-1 w-full">
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "./marketData";
|
} from "./marketData";
|
||||||
import { oratsClient } from "./oratsClient";
|
import { oratsClient } from "./oratsClient";
|
||||||
import { tanukiClient } from "./tanukiClient";
|
import { tanukiClient } from "./tanukiClient";
|
||||||
import { registerAuthRoutes } from "./authRoutes";
|
import { registerAuthRoutes, requireAuth } from "./authRoutes";
|
||||||
import { initDb, getAllCustomSymbols, createCustomSymbol, deleteCustomSymbol } from "./db";
|
import { initDb, getAllCustomSymbols, createCustomSymbol, deleteCustomSymbol } from "./db";
|
||||||
import { rateLimit, securityHeaders } from "./middleware";
|
import { rateLimit, securityHeaders } from "./middleware";
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ export async function registerRoutes(httpServer: Server, app: Express): Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a custom symbol
|
// Add a custom symbol
|
||||||
app.post("/api/symbols", async (req, res) => {
|
app.post("/api/symbols", requireAuth, async (req, res) => {
|
||||||
const { ticker, name, type, sector } = req.body as Record<string, string>;
|
const { ticker, name, type, sector } = req.body as Record<string, string>;
|
||||||
if (!ticker || !name) {
|
if (!ticker || !name) {
|
||||||
return res.status(400).json({ error: "ticker and name are required" });
|
return res.status(400).json({ error: "ticker and name are required" });
|
||||||
|
|
@ -141,7 +141,7 @@ export async function registerRoutes(httpServer: Server, app: Express): Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete a custom symbol
|
// Delete a custom symbol
|
||||||
app.delete("/api/symbols/:ticker", async (req, res) => {
|
app.delete("/api/symbols/:ticker", requireAuth, async (req, res) => {
|
||||||
const ticker = String(req.params.ticker || "").toUpperCase();
|
const ticker = String(req.params.ticker || "").toUpperCase();
|
||||||
if (BUILTIN_TICKERS.has(ticker)) {
|
if (BUILTIN_TICKERS.has(ticker)) {
|
||||||
return res.status(400).json({ error: "Cannot remove built-in symbol" });
|
return res.status(400).json({ error: "Cannot remove built-in symbol" });
|
||||||
|
|
@ -168,7 +168,7 @@ export async function registerRoutes(httpServer: Server, app: Express): Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save ORATS API key + base URL to session
|
// Save ORATS API key + base URL to session
|
||||||
app.post("/api/orats/config", (req, res) => {
|
app.post("/api/orats/config", requireAuth, (req, res) => {
|
||||||
const { apiKey, baseUrl } = req.body as Record<string, string>;
|
const { apiKey, baseUrl } = req.body as Record<string, string>;
|
||||||
const session = req.session as Record<string, unknown>;
|
const session = req.session as Record<string, unknown>;
|
||||||
if (!apiKey || typeof apiKey !== "string") {
|
if (!apiKey || typeof apiKey !== "string") {
|
||||||
|
|
@ -185,7 +185,7 @@ export async function registerRoutes(httpServer: Server, app: Express): Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear ORATS API key from session
|
// Clear ORATS API key from session
|
||||||
app.delete("/api/orats/config", (req, res) => {
|
app.delete("/api/orats/config", requireAuth, (req, res) => {
|
||||||
const session = req.session as Record<string, unknown>;
|
const session = req.session as Record<string, unknown>;
|
||||||
delete session.oratsApiKey;
|
delete session.oratsApiKey;
|
||||||
oratsClient.setApiKey("");
|
oratsClient.setApiKey("");
|
||||||
|
|
@ -202,7 +202,7 @@ export async function registerRoutes(httpServer: Server, app: Express): Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save Tanuki config
|
// Save Tanuki config
|
||||||
app.post("/api/tanuki/config", (req, res) => {
|
app.post("/api/tanuki/config", requireAuth, (req, res) => {
|
||||||
const { email, tvUser } = req.body as Record<string, string>;
|
const { email, tvUser } = req.body as Record<string, string>;
|
||||||
if (!email || !tvUser) {
|
if (!email || !tvUser) {
|
||||||
return res.status(400).json({ error: "email and tvUser are required" });
|
return res.status(400).json({ error: "email and tvUser are required" });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue