implement better-auth auth with postgres and route protection

This commit is contained in:
2026-02-24 13:32:43 -05:00
parent fd168f607c
commit 52a4ab38d3
31 changed files with 1202 additions and 89 deletions

View File

@@ -7,6 +7,7 @@ import { fetchFilingMetrics, fetchRecentFilings } from '@/lib/server/sec';
import { getStoreSnapshot, withStore } from '@/lib/server/store';
type EnqueueTaskInput = {
userId: string;
taskType: TaskType;
payload?: Record<string, unknown>;
priority?: number;
@@ -137,9 +138,15 @@ async function processSyncFilings(task: Task) {
};
}
async function processRefreshPrices() {
async function processRefreshPrices(task: Task) {
const userId = task.user_id;
if (!userId) {
throw new Error('Task is missing user scope');
}
const snapshot = await getStoreSnapshot();
const tickers = [...new Set(snapshot.holdings.map((holding) => holding.ticker))];
const userHoldings = snapshot.holdings.filter((holding) => holding.user_id === userId);
const tickers = [...new Set(userHoldings.map((holding) => holding.ticker))];
const quotes = new Map<string, number>();
for (const ticker of tickers) {
@@ -152,6 +159,10 @@ async function processRefreshPrices() {
await withStore((store) => {
store.holdings = store.holdings.map((holding) => {
if (holding.user_id !== userId) {
return holding;
}
const quote = quotes.get(holding.ticker);
if (quote === undefined) {
return holding;
@@ -236,14 +247,20 @@ function holdingDigest(holdings: Holding[]) {
}));
}
async function processPortfolioInsights() {
async function processPortfolioInsights(task: Task) {
const userId = task.user_id;
if (!userId) {
throw new Error('Task is missing user scope');
}
const snapshot = await getStoreSnapshot();
const summary = buildPortfolioSummary(snapshot.holdings);
const userHoldings = snapshot.holdings.filter((holding) => holding.user_id === userId);
const summary = buildPortfolioSummary(userHoldings);
const prompt = [
'Generate portfolio intelligence with actionable recommendations.',
`Portfolio summary: ${JSON.stringify(summary)}`,
`Holdings: ${JSON.stringify(holdingDigest(snapshot.holdings))}`,
`Holdings: ${JSON.stringify(holdingDigest(userHoldings))}`,
'Respond with: 1) health score (0-100), 2) top 3 risks, 3) top 3 opportunities, 4) next actions in 7 days.'
].join('\n');
@@ -255,7 +272,7 @@ async function processPortfolioInsights() {
const insight: PortfolioInsight = {
id: store.counters.insights,
user_id: 1,
user_id: userId,
provider: analysis.provider,
model: analysis.model,
content: analysis.text,
@@ -277,11 +294,11 @@ async function runTaskProcessor(task: Task) {
case 'sync_filings':
return await processSyncFilings(task);
case 'refresh_prices':
return await processRefreshPrices();
return await processRefreshPrices(task);
case 'analyze_filing':
return await processAnalyzeFiling(task);
case 'portfolio_insights':
return await processPortfolioInsights();
return await processPortfolioInsights(task);
default:
throw new Error(`Unsupported task type: ${task.task_type}`);
}
@@ -356,6 +373,7 @@ export async function enqueueTask(input: EnqueueTaskInput) {
const task: Task = {
id: randomUUID(),
user_id: input.userId,
task_type: input.taskType,
status: 'queued',
priority: input.priority ?? 50,
@@ -384,18 +402,19 @@ export async function enqueueTask(input: EnqueueTaskInput) {
return task;
}
export async function getTaskById(taskId: string) {
export async function getTaskById(taskId: string, userId: string) {
const snapshot = await getStoreSnapshot();
return snapshot.tasks.find((task) => task.id === taskId) ?? null;
return snapshot.tasks.find((task) => task.id === taskId && task.user_id === userId) ?? null;
}
export async function listRecentTasks(limit = 20, statuses?: TaskStatus[]) {
export async function listRecentTasks(userId: string, limit = 20, statuses?: TaskStatus[]) {
const safeLimit = Math.min(Math.max(Math.trunc(limit), 1), 200);
const snapshot = await getStoreSnapshot();
const scoped = snapshot.tasks.filter((task) => task.user_id === userId);
const filtered = statuses && statuses.length > 0
? snapshot.tasks.filter((task) => statuses.includes(task.status))
: snapshot.tasks;
? scoped.filter((task) => statuses.includes(task.status))
: scoped;
return filtered
.slice()