Integrate agents: header user menu, session bridge, migration 0001, bundler resolution
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7 changed files +8,570 −10
modified
apps/web/src/components/layout/site-header.tsx
+2 −3
@@ -3,6 +3,7 @@ import { NAV, NAV_GROUPS, PRIMARY_NAV } from '@/config/nav'; | |||||
| 3 | import { ThemeToggle } from './theme-toggle'; | 3 | import { ThemeToggle } from './theme-toggle'; | ||
| 4 | import { MobileNav } from './mobile-nav'; | 4 | import { MobileNav } from './mobile-nav'; | ||
| 5 | import { HeaderSearch } from './header-search'; | 5 | import { HeaderSearch } from './header-search'; | ||
| 6 | +import { UserMenu } from '@/components/account/user-menu'; | ||||
| 6 | 7 | ||||
| 7 | export function Logo({ className }: { className?: string }) { | 8 | export function Logo({ className }: { className?: string }) { | ||
| 8 | return ( | 9 | return ( | ||
@@ -50,9 +51,7 @@ export function SiteHeader() { | |||||
| 50 | <div className="ml-auto flex items-center gap-1.5"> | 51 | <div className="ml-auto flex items-center gap-1.5"> | ||
| 51 | <HeaderSearch /> | 52 | <HeaderSearch /> | ||
| 52 | <ThemeToggle /> | 53 | <ThemeToggle /> | ||
| 53 | − <Link href="/account" className="hidden rounded-md border border-border px-2.5 py-1.5 text-[13px] font-medium text-fg hover:bg-inset sm:inline-flex"> | 54 | + <UserMenu /> | ||
| 54 | − Sign in | ||||
| 55 | − </Link> | ||||
| 56 | </div> | 55 | </div> | ||
| 57 | </div> | 56 | </div> | ||
| 58 | </header> | 57 | </header> | ||
modified
apps/web/src/lib/ai/request.ts
+4 −5
@@ -1,6 +1,7 @@ | |||||
| 1 | import 'server-only'; | 1 | import 'server-only'; | ||
| 2 | import { createHmac, randomUUID } from 'node:crypto'; | 2 | import { createHmac, randomUUID } from 'node:crypto'; | ||
| 3 | import { cookies, headers } from 'next/headers'; | 3 | import { cookies, headers } from 'next/headers'; | ||
| 4 | +import { getCurrentUser } from '@/lib/auth/session'; | ||||
| 4 | import { getDb, aiQuotas, sql } from '@rareindex/database'; | 5 | import { getDb, aiQuotas, sql } from '@rareindex/database'; | ||
| 5 | 6 | ||||
| 6 | const SECRET = () => process.env.SESSION_SECRET ?? 'dev-only'; | 7 | const SECRET = () => process.env.SESSION_SECRET ?? 'dev-only'; | ||
@@ -37,11 +38,9 @@ export async function getAnonId(create = true): Promise<string | null> { | |||||
| 37 | */ | 38 | */ | ||
| 38 | export async function currentUserId(): Promise<string | null> { | 39 | export async function currentUserId(): Promise<string | null> { | ||
| 39 | try { | 40 | try { | ||
| 40 | − const jar = await cookies(); | 41 | + // Delegates to the account module (hashed session id, revocation, expiry) so both agree. | ||
| 41 | − const token = jar.get('ri_session')?.value ?? jar.get('session')?.value; | 42 | + const user = await getCurrentUser(); | ||
| 42 | − if (!token || token.length > 200) return null; | 43 | + return user?.id ?? null; | ||
| 43 | − const [row] = await getDb().execute(sql`select user_id from sessions where id = ${token} and expires_at > now() limit 1`); | ||||
| 44 | − return ((row as { user_id?: string } | undefined)?.user_id) ?? null; | ||||
| 45 | } catch { | 44 | } catch { | ||
| 46 | return null; | 45 | return null; | ||
| 47 | } | 46 | } | ||
modified
apps/web/tsconfig.json
+2 −2
@@ -8,8 +8,8 @@ | |||||
| 8 | "noUncheckedIndexedAccess": true, | 8 | "noUncheckedIndexedAccess": true, | ||
| 9 | "noEmit": true, | 9 | "noEmit": true, | ||
| 10 | "esModuleInterop": true, | 10 | "esModuleInterop": true, | ||
| 11 | − "module": "nodenext", | 11 | + "module": "esnext", | ||
| 12 | − "moduleResolution": "nodenext", | 12 | + "moduleResolution": "bundler", | ||
| 13 | "resolveJsonModule": true, | 13 | "resolveJsonModule": true, | ||
| 14 | "isolatedModules": true, | 14 | "isolatedModules": true, | ||
| 15 | "jsx": "react-jsx", | 15 | "jsx": "react-jsx", | ||
added
packages/database/migrations/0001_living_prima.sql
+215 −0
@@ -0,0 +1,215 @@ | |||||
| 1 | +CREATE TABLE "ai_quotas" ( | ||||
| 2 | + "key" text NOT NULL, | ||||
| 3 | + "feature" text NOT NULL, | ||||
| 4 | + "date" text NOT NULL, | ||||
| 5 | + "count" integer DEFAULT 0 NOT NULL | ||||
| 6 | +); | ||||
| 7 | +--> statement-breakpoint | ||||
| 8 | +CREATE TABLE "research_messages" ( | ||||
| 9 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 10 | + "session_id" text NOT NULL, | ||||
| 11 | + "role" text NOT NULL, | ||||
| 12 | + "content" text NOT NULL, | ||||
| 13 | + "tool_calls" jsonb DEFAULT '[]'::jsonb NOT NULL, | ||||
| 14 | + "usage" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 15 | + "usd_est" real DEFAULT 0 NOT NULL, | ||||
| 16 | + "model" text, | ||||
| 17 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 18 | +); | ||||
| 19 | +--> statement-breakpoint | ||||
| 20 | +CREATE TABLE "research_sessions" ( | ||||
| 21 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 22 | + "user_id" text, | ||||
| 23 | + "anon_id" text, | ||||
| 24 | + "title" text, | ||||
| 25 | + "model" text, | ||||
| 26 | + "message_count" integer DEFAULT 0 NOT NULL, | ||||
| 27 | + "usd_est" real DEFAULT 0 NOT NULL, | ||||
| 28 | + "archived" boolean DEFAULT false NOT NULL, | ||||
| 29 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | ||||
| 30 | + "updated_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 31 | +); | ||||
| 32 | +--> statement-breakpoint | ||||
| 33 | +CREATE TABLE "scanner_sessions" ( | ||||
| 34 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 35 | + "user_id" text, | ||||
| 36 | + "anon_id" text, | ||||
| 37 | + "ip_hash" text, | ||||
| 38 | + "mode" text NOT NULL, | ||||
| 39 | + "input_url" text, | ||||
| 40 | + "input_text" text, | ||||
| 41 | + "image_count" integer DEFAULT 0 NOT NULL, | ||||
| 42 | + "thumbnails" jsonb DEFAULT '[]'::jsonb NOT NULL, | ||||
| 43 | + "guess" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 44 | + "guess_confidence" real, | ||||
| 45 | + "candidates" jsonb DEFAULT '[]'::jsonb NOT NULL, | ||||
| 46 | + "chosen_asset_id" text, | ||||
| 47 | + "listing" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 48 | + "model" text, | ||||
| 49 | + "usd_est" real DEFAULT 0 NOT NULL, | ||||
| 50 | + "duration_ms" integer, | ||||
| 51 | + "error" text, | ||||
| 52 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | ||||
| 53 | + "chosen_at" timestamp with time zone | ||||
| 54 | +); | ||||
| 55 | +--> statement-breakpoint | ||||
| 56 | +CREATE TABLE "auth_codes" ( | ||||
| 57 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 58 | + "user_id" text, | ||||
| 59 | + "email" text NOT NULL, | ||||
| 60 | + "purpose" text NOT NULL, | ||||
| 61 | + "code_hash" text NOT NULL, | ||||
| 62 | + "expires_at" timestamp with time zone NOT NULL, | ||||
| 63 | + "attempts" integer DEFAULT 0 NOT NULL, | ||||
| 64 | + "consumed_at" timestamp with time zone, | ||||
| 65 | + "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 66 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 67 | +); | ||||
| 68 | +--> statement-breakpoint | ||||
| 69 | +CREATE TABLE "login_events" ( | ||||
| 70 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 71 | + "user_id" text, | ||||
| 72 | + "email" text, | ||||
| 73 | + "ip" text, | ||||
| 74 | + "user_agent" text, | ||||
| 75 | + "outcome" text NOT NULL, | ||||
| 76 | + "method" text, | ||||
| 77 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 78 | +); | ||||
| 79 | +--> statement-breakpoint | ||||
| 80 | +CREATE TABLE "notifications" ( | ||||
| 81 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 82 | + "user_id" text NOT NULL, | ||||
| 83 | + "kind" text NOT NULL, | ||||
| 84 | + "title" text NOT NULL, | ||||
| 85 | + "body" text, | ||||
| 86 | + "href" text, | ||||
| 87 | + "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 88 | + "read_at" timestamp with time zone, | ||||
| 89 | + "emailed_at" timestamp with time zone, | ||||
| 90 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 91 | +); | ||||
| 92 | +--> statement-breakpoint | ||||
| 93 | +CREATE TABLE "price_targets" ( | ||||
| 94 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 95 | + "user_id" text NOT NULL, | ||||
| 96 | + "asset_id" text NOT NULL, | ||||
| 97 | + "variant_id" text, | ||||
| 98 | + "direction" text DEFAULT 'above' NOT NULL, | ||||
| 99 | + "target_usd" numeric(18, 4) NOT NULL, | ||||
| 100 | + "baseline_usd" numeric(18, 4), | ||||
| 101 | + "note" text, | ||||
| 102 | + "hit_at" timestamp with time zone, | ||||
| 103 | + "notified_at" timestamp with time zone, | ||||
| 104 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 105 | +); | ||||
| 106 | +--> statement-breakpoint | ||||
| 107 | +CREATE TABLE "rate_limits" ( | ||||
| 108 | + "key" text PRIMARY KEY NOT NULL, | ||||
| 109 | + "count" integer DEFAULT 0 NOT NULL, | ||||
| 110 | + "reset_at" timestamp with time zone NOT NULL | ||||
| 111 | +); | ||||
| 112 | +--> statement-breakpoint | ||||
| 113 | +CREATE TABLE "recovery_codes" ( | ||||
| 114 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 115 | + "user_id" text NOT NULL, | ||||
| 116 | + "code_hash" text NOT NULL, | ||||
| 117 | + "used_at" timestamp with time zone, | ||||
| 118 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 119 | +); | ||||
| 120 | +--> statement-breakpoint | ||||
| 121 | +CREATE TABLE "saved_searches" ( | ||||
| 122 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 123 | + "user_id" text NOT NULL, | ||||
| 124 | + "name" text NOT NULL, | ||||
| 125 | + "url" text NOT NULL, | ||||
| 126 | + "params" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 127 | + "notify" boolean DEFAULT false NOT NULL, | ||||
| 128 | + "last_run_at" timestamp with time zone, | ||||
| 129 | + "last_count" integer, | ||||
| 130 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 131 | +); | ||||
| 132 | +--> statement-breakpoint | ||||
| 133 | +CREATE TABLE "trusted_devices" ( | ||||
| 134 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 135 | + "user_id" text NOT NULL, | ||||
| 136 | + "token_hash" text NOT NULL, | ||||
| 137 | + "label" text, | ||||
| 138 | + "user_agent" text, | ||||
| 139 | + "ip" text, | ||||
| 140 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL, | ||||
| 141 | + "last_used_at" timestamp with time zone, | ||||
| 142 | + "expires_at" timestamp with time zone NOT NULL, | ||||
| 143 | + "revoked_at" timestamp with time zone | ||||
| 144 | +); | ||||
| 145 | +--> statement-breakpoint | ||||
| 146 | +CREATE TABLE "uploads" ( | ||||
| 147 | + "id" text PRIMARY KEY NOT NULL, | ||||
| 148 | + "user_id" text NOT NULL, | ||||
| 149 | + "kind" text NOT NULL, | ||||
| 150 | + "path" text NOT NULL, | ||||
| 151 | + "mime" text NOT NULL, | ||||
| 152 | + "bytes" integer NOT NULL, | ||||
| 153 | + "width" integer, | ||||
| 154 | + "height" integer, | ||||
| 155 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 156 | +); | ||||
| 157 | +--> statement-breakpoint | ||||
| 158 | +CREATE TABLE "user_badges" ( | ||||
| 159 | + "user_id" text NOT NULL, | ||||
| 160 | + "badge" text NOT NULL, | ||||
| 161 | + "evidence" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
| 162 | + "created_at" timestamp with time zone DEFAULT now() NOT NULL | ||||
| 163 | +); | ||||
| 164 | +--> statement-breakpoint | ||||
| 165 | +DROP INDEX "normalized_records_raw_uq";--> statement-breakpoint | ||||
| 166 | +ALTER TABLE "normalized_records" ADD COLUMN "seq" integer DEFAULT 0 NOT NULL;--> statement-breakpoint | ||||
| 167 | +ALTER TABLE "alerts" ADD COLUMN "name" text;--> statement-breakpoint | ||||
| 168 | +ALTER TABLE "alerts" ADD COLUMN "cooldown_minutes" integer DEFAULT 1440 NOT NULL;--> statement-breakpoint | ||||
| 169 | +ALTER TABLE "alerts" ADD COLUMN "trigger_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint | ||||
| 170 | +ALTER TABLE "collection_items" ADD COLUMN "tags" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint | ||||
| 171 | +ALTER TABLE "collection_items" ADD COLUMN "condition" text;--> statement-breakpoint | ||||
| 172 | +ALTER TABLE "collection_items" ADD COLUMN "manual_value_usd" numeric(18, 4);--> statement-breakpoint | ||||
| 173 | +ALTER TABLE "collection_items" ADD COLUMN "sold_at" date;--> statement-breakpoint | ||||
| 174 | +ALTER TABLE "collection_items" ADD COLUMN "sold_price_usd" numeric(18, 4);--> statement-breakpoint | ||||
| 175 | +ALTER TABLE "collections" ADD COLUMN "kind" text DEFAULT 'collection' NOT NULL;--> statement-breakpoint | ||||
| 176 | +ALTER TABLE "collections" ADD COLUMN "budget_usd" numeric(18, 4);--> statement-breakpoint | ||||
| 177 | +ALTER TABLE "collections" ADD COLUMN "color" text;--> statement-breakpoint | ||||
| 178 | +ALTER TABLE "sessions" ADD COLUMN "ip" text;--> statement-breakpoint | ||||
| 179 | +ALTER TABLE "sessions" ADD COLUMN "last_seen_at" timestamp with time zone;--> statement-breakpoint | ||||
| 180 | +ALTER TABLE "sessions" ADD COLUMN "revoked_at" timestamp with time zone;--> statement-breakpoint | ||||
| 181 | +ALTER TABLE "users" ADD COLUMN "handle" text;--> statement-breakpoint | ||||
| 182 | +ALTER TABLE "users" ADD COLUMN "avatar_url" text;--> statement-breakpoint | ||||
| 183 | +ALTER TABLE "users" ADD COLUMN "bio" text;--> statement-breakpoint | ||||
| 184 | +ALTER TABLE "users" ADD COLUMN "mfa_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint | ||||
| 185 | +ALTER TABLE "users" ADD COLUMN "totp_secret_enc" text;--> statement-breakpoint | ||||
| 186 | +ALTER TABLE "users" ADD COLUMN "always_ask_code" boolean DEFAULT true NOT NULL;--> statement-breakpoint | ||||
| 187 | +ALTER TABLE "users" ADD COLUMN "password_changed_at" timestamp with time zone;--> statement-breakpoint | ||||
| 188 | +ALTER TABLE "users" ADD COLUMN "pending_email" text;--> statement-breakpoint | ||||
| 189 | +ALTER TABLE "users" ADD COLUMN "deleted_at" timestamp with time zone;--> statement-breakpoint | ||||
| 190 | +ALTER TABLE "users" ADD COLUMN "purge_after" timestamp with time zone;--> statement-breakpoint | ||||
| 191 | +ALTER TABLE "watchlist_items" ADD COLUMN "label" text;--> statement-breakpoint | ||||
| 192 | +ALTER TABLE "watchlist_items" ADD COLUMN "note" text;--> statement-breakpoint | ||||
| 193 | +ALTER TABLE "watchlist_items" ADD COLUMN "target_price_usd" numeric(18, 4);--> statement-breakpoint | ||||
| 194 | +ALTER TABLE "watchlist_items" ADD COLUMN "baseline_usd" numeric(18, 4);--> statement-breakpoint | ||||
| 195 | +CREATE INDEX "ai_quotas_uq" ON "ai_quotas" USING btree ("key","feature","date");--> statement-breakpoint | ||||
| 196 | +CREATE INDEX "research_messages_session_idx" ON "research_messages" USING btree ("session_id","created_at");--> statement-breakpoint | ||||
| 197 | +CREATE INDEX "research_sessions_anon_idx" ON "research_sessions" USING btree ("anon_id","updated_at");--> statement-breakpoint | ||||
| 198 | +CREATE INDEX "research_sessions_user_idx" ON "research_sessions" USING btree ("user_id","updated_at");--> statement-breakpoint | ||||
| 199 | +CREATE INDEX "scanner_sessions_created_idx" ON "scanner_sessions" USING btree ("created_at");--> statement-breakpoint | ||||
| 200 | +CREATE INDEX "scanner_sessions_ip_idx" ON "scanner_sessions" USING btree ("ip_hash","created_at");--> statement-breakpoint | ||||
| 201 | +CREATE INDEX "scanner_sessions_user_idx" ON "scanner_sessions" USING btree ("user_id");--> statement-breakpoint | ||||
| 202 | +CREATE INDEX "auth_codes_lookup_idx" ON "auth_codes" USING btree ("email","purpose","created_at");--> statement-breakpoint | ||||
| 203 | +CREATE INDEX "login_events_user_idx" ON "login_events" USING btree ("user_id","created_at");--> statement-breakpoint | ||||
| 204 | +CREATE INDEX "notifications_user_idx" ON "notifications" USING btree ("user_id","created_at");--> statement-breakpoint | ||||
| 205 | +CREATE INDEX "notifications_unread_idx" ON "notifications" USING btree ("user_id","read_at");--> statement-breakpoint | ||||
| 206 | +CREATE INDEX "price_targets_user_idx" ON "price_targets" USING btree ("user_id");--> statement-breakpoint | ||||
| 207 | +CREATE INDEX "price_targets_asset_idx" ON "price_targets" USING btree ("asset_id","hit_at");--> statement-breakpoint | ||||
| 208 | +CREATE INDEX "recovery_codes_user_idx" ON "recovery_codes" USING btree ("user_id");--> statement-breakpoint | ||||
| 209 | +CREATE INDEX "saved_searches_user_idx" ON "saved_searches" USING btree ("user_id");--> statement-breakpoint | ||||
| 210 | +CREATE INDEX "trusted_devices_user_idx" ON "trusted_devices" USING btree ("user_id");--> statement-breakpoint | ||||
| 211 | +CREATE UNIQUE INDEX "trusted_devices_token_uq" ON "trusted_devices" USING btree ("token_hash");--> statement-breakpoint | ||||
| 212 | +CREATE INDEX "uploads_user_idx" ON "uploads" USING btree ("user_id");--> statement-breakpoint | ||||
| 213 | +CREATE UNIQUE INDEX "user_badges_uq" ON "user_badges" USING btree ("user_id","badge");--> statement-breakpoint | ||||
| 214 | +CREATE UNIQUE INDEX "normalized_records_raw_uq" ON "normalized_records" USING btree ("raw_record_id","kind","seq");--> statement-breakpoint | ||||
| 215 | +ALTER TABLE "users" ADD CONSTRAINT "users_handle_unique" UNIQUE("handle"); | ||||
| \ No newline at end of file | |||||
added
packages/database/migrations/meta/0001_snapshot.json
+8103 −0
Large diff (8,104 lines) is not loaded by default to keep the page fast.Load diff
modified
packages/database/migrations/meta/_journal.json
+7 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
modified
pnpm-lock.yaml
+237 −0
Large diff (566 lines) is not loaded by default to keep the page fast.Load diff