feat: Auth-guard custom tickers and config endpoints

master
isnowglobal-admin 2026-06-30 02:15:53 -04:00
parent 2949da2937
commit b8f590708d
2 changed files with 33 additions and 19 deletions

View File

@ -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">

View File

@ -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" });