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