[{"data":1,"prerenderedAt":811},["ShallowReactive",2],{"$fSjWup0jYSUfKxFz2sk88jlk-pyZwgq3Y5u7VG3RHdPo":3},{"newThing":4,"project":5,"cycle":8,"sections":22,"threads":802,"otherDocs":803,"documents":805,"images":808,"archiveItems":809,"links":810},null,{"id":6,"label":7,"liveBase":4},"market-analyst","Market Analyst",{"id":9,"num":10,"dir":11,"name":12,"title":12,"focus":13,"status":14,"statusNote":15,"created":16,"completed":4,"mode":4,"report":4,"recordless":17,"thumbnail":4,"imageCount":18,"artifactCount":19,"lastActivity":20,"hasPickup":21,"hasInitialPrompt":17},"c3",3,"c3.oauth","oauth","Add Google OAuth to restrict dashboard access to @qpoint.io team members.","parked","parked — see PICKUP.md","2026-02-20",false,0,1,"2026-08-20",true,[23,37,64,107,197,537,714,737],{"key":24,"title":25,"empty":17,"ast":26},"why","Why",{"type":27,"children":28},"root",[29],{"type":30,"tag":31,"props":32,"children":33},"element","p",{},[34],{"type":35,"value":36},"text","The market analysis dashboard is currently open to anyone with the URL. As the data grows and the dashboard gets shared more broadly, we need to restrict access to the Qpoint team. Google OAuth is the natural fit since the team already uses Google Workspace.",{"key":38,"title":39,"empty":17,"ast":40},"objective","Objective",{"type":27,"children":41},[42],{"type":30,"tag":31,"props":43,"children":44},{},[45,47,54,56,62],{"type":35,"value":46},"Add Google OAuth login using ",{"type":30,"tag":48,"props":49,"children":51},"code",{"className":50},[],[52],{"type":35,"value":53},"nuxt-auth-utils",{"type":35,"value":55},". \"Done\" means: only users with ",{"type":30,"tag":48,"props":57,"children":59},{"className":58},[],[60],{"type":35,"value":61},"@qpoint.io",{"type":35,"value":63}," Google accounts can access the dashboard, sessions persist via encrypted cookies, and the nav shows the logged-in user with a sign-out option.",{"key":65,"title":66,"empty":17,"ast":67},"scope","Scope",{"type":27,"children":68},[69],{"type":30,"tag":70,"props":71,"children":72},"ul",{},[73,85,96],{"type":30,"tag":74,"props":75,"children":76},"li",{},[77,83],{"type":30,"tag":48,"props":78,"children":80},{"className":79},[],[81],{"type":35,"value":82},"site\u002F",{"type":35,"value":84}," — Nuxt 3 dashboard (auth module, server routes, middleware, login page, layout)",{"type":30,"tag":74,"props":86,"children":87},{},[88,94],{"type":30,"tag":48,"props":89,"children":91},{"className":90},[],[92],{"type":35,"value":93},".env",{"type":35,"value":95}," — New OAuth credentials",{"type":30,"tag":74,"props":97,"children":98},{},[99,105],{"type":30,"tag":48,"props":100,"children":102},{"className":101},[],[103],{"type":35,"value":104},"package.json",{"type":35,"value":106}," (root) — Update dev\u002Fbuild scripts for .env loading",{"key":108,"title":109,"empty":17,"ast":110},"extra","Prerequisites (manual)",{"type":27,"children":111},[112],{"type":30,"tag":113,"props":114,"children":115},"ol",{},[116,138,150,180,192],{"type":30,"tag":74,"props":117,"children":118},{},[119,121,130,132],{"type":35,"value":120},"In ",{"type":30,"tag":122,"props":123,"children":127},"a",{"href":124,"rel":125},"https:\u002F\u002Fconsole.cloud.google.com\u002F",[126],"nofollow",[128],{"type":35,"value":129},"Google Cloud Console",{"type":35,"value":131},", go to ",{"type":30,"tag":133,"props":134,"children":135},"strong",{},[136],{"type":35,"value":137},"APIs & Services > Credentials",{"type":30,"tag":74,"props":139,"children":140},{},[141,143,148],{"type":35,"value":142},"Create an ",{"type":30,"tag":133,"props":144,"children":145},{},[146],{"type":35,"value":147},"OAuth 2.0 Client ID",{"type":35,"value":149}," (Web application)",{"type":30,"tag":74,"props":151,"children":152},{},[153,155],{"type":35,"value":154},"Authorized redirect URIs:\n",{"type":30,"tag":70,"props":156,"children":157},{},[158,169],{"type":30,"tag":74,"props":159,"children":160},{},[161,167],{"type":30,"tag":48,"props":162,"children":164},{"className":163},[],[165],{"type":35,"value":166},"http:\u002F\u002Flocalhost:3000\u002Fauth\u002Fgoogle",{"type":35,"value":168}," (dev)",{"type":30,"tag":74,"props":170,"children":171},{},[172,178],{"type":30,"tag":48,"props":173,"children":175},{"className":174},[],[176],{"type":35,"value":177},"https:\u002F\u002F\u003Cproduction-domain>\u002Fauth\u002Fgoogle",{"type":35,"value":179}," (prod)",{"type":30,"tag":74,"props":181,"children":182},{},[183,185,190],{"type":35,"value":184},"Set OAuth consent screen to ",{"type":30,"tag":133,"props":186,"children":187},{},[188],{"type":35,"value":189},"Internal",{"type":35,"value":191}," (restricts to Qpoint Workspace org)",{"type":30,"tag":74,"props":193,"children":194},{},[195],{"type":35,"value":196},"Note the Client ID and Client Secret",{"key":108,"title":198,"empty":17,"ast":199},"Implementation Plan",{"type":27,"children":200},[201,208,228,239,251,257,283,289,309,315,331,376,382,396,432,438,452,476,482,497,515,521],{"type":30,"tag":202,"props":203,"children":205},"h3",{"id":204},"_1-install-nuxt-auth-utils",[206],{"type":35,"value":207},"1. Install nuxt-auth-utils",{"type":30,"tag":31,"props":209,"children":210},{},[211,213,219,221,226],{"type":35,"value":212},"Run ",{"type":30,"tag":48,"props":214,"children":216},{"className":215},[],[217],{"type":35,"value":218},"npx nuxi@latest module add auth-utils",{"type":35,"value":220}," from ",{"type":30,"tag":48,"props":222,"children":224},{"className":223},[],[225],{"type":35,"value":82},{"type":35,"value":227},".",{"type":30,"tag":202,"props":229,"children":231},{"id":230},"_2-add-env-vars-to-env",[232,234],{"type":35,"value":233},"2. Add env vars to ",{"type":30,"tag":48,"props":235,"children":237},{"className":236},[],[238],{"type":35,"value":93},{"type":30,"tag":240,"props":241,"children":245},"pre",{"className":242,"code":244,"language":35},[243],"language-text","NUXT_SESSION_PASSWORD=\u003Crandom-32+-char-string>\nNUXT_OAUTH_GOOGLE_CLIENT_ID=\u003Cfrom-google-cloud>\nNUXT_OAUTH_GOOGLE_CLIENT_SECRET=\u003Cfrom-google-cloud>\n",[246],{"type":30,"tag":48,"props":247,"children":249},{"__ignoreMap":248},"",[250],{"type":35,"value":244},{"type":30,"tag":202,"props":252,"children":254},{"id":253},"_3-update-devbuild-scripts",[255],{"type":35,"value":256},"3. Update dev\u002Fbuild scripts",{"type":30,"tag":31,"props":258,"children":259},{},[260,262,267,269,275,277,282],{"type":35,"value":261},"Root ",{"type":30,"tag":48,"props":263,"children":265},{"className":264},[],[266],{"type":35,"value":104},{"type":35,"value":268}," — add ",{"type":30,"tag":48,"props":270,"children":272},{"className":271},[],[273],{"type":35,"value":274},"--dotenv ..\u002F.env",{"type":35,"value":276}," to nuxt commands so they find the root ",{"type":30,"tag":48,"props":278,"children":280},{"className":279},[],[281],{"type":35,"value":93},{"type":35,"value":227},{"type":30,"tag":202,"props":284,"children":286},{"id":285},"_4-update-nuxtconfigts",[287],{"type":35,"value":288},"4. Update nuxt.config.ts",{"type":30,"tag":31,"props":290,"children":291},{},[292,294,299,301,307],{"type":35,"value":293},"Add ",{"type":30,"tag":48,"props":295,"children":297},{"className":296},[],[298],{"type":35,"value":53},{"type":35,"value":300}," to modules, add ",{"type":30,"tag":48,"props":302,"children":304},{"className":303},[],[305],{"type":35,"value":306},"runtimeConfig",{"type":35,"value":308}," block for session\u002Foauth defaults.",{"type":30,"tag":202,"props":310,"children":312},{"id":311},"_5-create-google-oauth-server-route",[313],{"type":35,"value":314},"5. Create Google OAuth server route",{"type":30,"tag":31,"props":316,"children":317},{},[318,323,325],{"type":30,"tag":133,"props":319,"children":320},{},[321],{"type":35,"value":322},"New",{"type":35,"value":324},": ",{"type":30,"tag":48,"props":326,"children":328},{"className":327},[],[329],{"type":35,"value":330},"site\u002Fserver\u002Froutes\u002Fauth\u002Fgoogle.get.ts",{"type":30,"tag":70,"props":332,"children":333},{},[334,345,363],{"type":30,"tag":74,"props":335,"children":336},{},[337,343],{"type":30,"tag":48,"props":338,"children":340},{"className":339},[],[341],{"type":35,"value":342},"defineOAuthGoogleEventHandler",{"type":35,"value":344}," with email+profile scopes",{"type":30,"tag":74,"props":346,"children":347},{},[348,350,355,357],{"type":35,"value":349},"Check email ends with ",{"type":30,"tag":48,"props":351,"children":353},{"className":352},[],[354],{"type":35,"value":61},{"type":35,"value":356},", reject others → ",{"type":30,"tag":48,"props":358,"children":360},{"className":359},[],[361],{"type":35,"value":362},"\u002Flogin?error=unauthorized",{"type":30,"tag":74,"props":364,"children":365},{},[366,368,374],{"type":35,"value":367},"Store ",{"type":30,"tag":48,"props":369,"children":371},{"className":370},[],[372],{"type":35,"value":373},"{ email, name, avatar }",{"type":35,"value":375}," in encrypted cookie session",{"type":30,"tag":202,"props":377,"children":379},{"id":378},"_6-create-global-auth-middleware",[380],{"type":35,"value":381},"6. Create global auth middleware",{"type":30,"tag":31,"props":383,"children":384},{},[385,389,390],{"type":30,"tag":133,"props":386,"children":387},{},[388],{"type":35,"value":322},{"type":35,"value":324},{"type":30,"tag":48,"props":391,"children":393},{"className":392},[],[394],{"type":35,"value":395},"site\u002Fmiddleware\u002Fauth.global.ts",{"type":30,"tag":70,"props":397,"children":398},{},[399,412],{"type":30,"tag":74,"props":400,"children":401},{},[402,404,410],{"type":35,"value":403},"Check ",{"type":30,"tag":48,"props":405,"children":407},{"className":406},[],[408],{"type":35,"value":409},"useUserSession().loggedIn",{"type":35,"value":411}," on every route",{"type":30,"tag":74,"props":413,"children":414},{},[415,417,423,425,430],{"type":35,"value":416},"Redirect to ",{"type":30,"tag":48,"props":418,"children":420},{"className":419},[],[421],{"type":35,"value":422},"\u002Flogin",{"type":35,"value":424}," if not authenticated (skip ",{"type":30,"tag":48,"props":426,"children":428},{"className":427},[],[429],{"type":35,"value":422},{"type":35,"value":431}," itself)",{"type":30,"tag":202,"props":433,"children":435},{"id":434},"_7-create-login-page",[436],{"type":35,"value":437},"7. Create login page",{"type":30,"tag":31,"props":439,"children":440},{},[441,445,446],{"type":30,"tag":133,"props":442,"children":443},{},[444],{"type":35,"value":322},{"type":35,"value":324},{"type":30,"tag":48,"props":447,"children":449},{"className":448},[],[450],{"type":35,"value":451},"site\u002Fpages\u002Flogin.vue",{"type":30,"tag":70,"props":453,"children":454},{},[455,460,471],{"type":30,"tag":74,"props":456,"children":457},{},[458],{"type":35,"value":459},"Full-screen dark layout (no nav bar)",{"type":30,"tag":74,"props":461,"children":462},{},[463,465],{"type":35,"value":464},"Qpoint logo + \"Sign in with Google\" button → ",{"type":30,"tag":48,"props":466,"children":468},{"className":467},[],[469],{"type":35,"value":470},"\u002Fauth\u002Fgoogle",{"type":30,"tag":74,"props":472,"children":473},{},[474],{"type":35,"value":475},"Error messages for unauthorized\u002Foauth-failure query params",{"type":30,"tag":202,"props":477,"children":479},{"id":478},"_8-add-user-info-logout-to-nav",[480],{"type":35,"value":481},"8. Add user info + logout to nav",{"type":30,"tag":31,"props":483,"children":484},{},[485,490,491],{"type":30,"tag":133,"props":486,"children":487},{},[488],{"type":35,"value":489},"Modify",{"type":35,"value":324},{"type":30,"tag":48,"props":492,"children":494},{"className":493},[],[495],{"type":35,"value":496},"site\u002Flayouts\u002Fdefault.vue",{"type":30,"tag":70,"props":498,"children":499},{},[500,505],{"type":30,"tag":74,"props":501,"children":502},{},[503],{"type":35,"value":504},"Show user avatar, name, and \"Sign out\" button after nav links",{"type":30,"tag":74,"props":506,"children":507},{},[508,510],{"type":35,"value":509},"Sign out clears session and redirects to ",{"type":30,"tag":48,"props":511,"children":513},{"className":512},[],[514],{"type":35,"value":422},{"type":30,"tag":202,"props":516,"children":518},{"id":517},"_9-add-session-type-definition",[519],{"type":35,"value":520},"9. Add session type definition",{"type":30,"tag":31,"props":522,"children":523},{},[524,528,529,535],{"type":30,"tag":133,"props":525,"children":526},{},[527],{"type":35,"value":322},{"type":35,"value":324},{"type":30,"tag":48,"props":530,"children":532},{"className":531},[],[533],{"type":35,"value":534},"site\u002Fauth.d.ts",{"type":35,"value":536}," — type the User shape for autocomplete",{"key":108,"title":538,"empty":17,"ast":539},"Files",{"type":27,"children":540},[541],{"type":30,"tag":542,"props":543,"children":544},"table",{},[545,564],{"type":30,"tag":546,"props":547,"children":548},"thead",{},[549],{"type":30,"tag":550,"props":551,"children":552},"tr",{},[553,559],{"type":30,"tag":554,"props":555,"children":556},"th",{},[557],{"type":35,"value":558},"Action",{"type":30,"tag":554,"props":560,"children":561},{},[562],{"type":35,"value":563},"File",{"type":30,"tag":565,"props":566,"children":567},"tbody",{},[568,593,611,628,645,663,680,697],{"type":30,"tag":550,"props":569,"children":570},{},[571,576],{"type":30,"tag":572,"props":573,"children":574},"td",{},[575],{"type":35,"value":489},{"type":30,"tag":572,"props":577,"children":578},{},[579,584,586,591],{"type":30,"tag":48,"props":580,"children":582},{"className":581},[],[583],{"type":35,"value":104},{"type":35,"value":585}," (root) — ",{"type":30,"tag":48,"props":587,"children":589},{"className":588},[],[590],{"type":35,"value":274},{"type":35,"value":592}," in site scripts",{"type":30,"tag":550,"props":594,"children":595},{},[596,600],{"type":30,"tag":572,"props":597,"children":598},{},[599],{"type":35,"value":489},{"type":30,"tag":572,"props":601,"children":602},{},[603,609],{"type":30,"tag":48,"props":604,"children":606},{"className":605},[],[607],{"type":35,"value":608},"site\u002Fnuxt.config.ts",{"type":35,"value":610}," — module + runtimeConfig",{"type":30,"tag":550,"props":612,"children":613},{},[614,618],{"type":30,"tag":572,"props":615,"children":616},{},[617],{"type":35,"value":489},{"type":30,"tag":572,"props":619,"children":620},{},[621,626],{"type":30,"tag":48,"props":622,"children":624},{"className":623},[],[625],{"type":35,"value":93},{"type":35,"value":627}," — 3 new OAuth env vars",{"type":30,"tag":550,"props":629,"children":630},{},[631,635],{"type":30,"tag":572,"props":632,"children":633},{},[634],{"type":35,"value":489},{"type":30,"tag":572,"props":636,"children":637},{},[638,643],{"type":30,"tag":48,"props":639,"children":641},{"className":640},[],[642],{"type":35,"value":496},{"type":35,"value":644}," — user info + logout",{"type":30,"tag":550,"props":646,"children":647},{},[648,653],{"type":30,"tag":572,"props":649,"children":650},{},[651],{"type":35,"value":652},"Create",{"type":30,"tag":572,"props":654,"children":655},{},[656,661],{"type":30,"tag":48,"props":657,"children":659},{"className":658},[],[660],{"type":35,"value":330},{"type":35,"value":662}," — OAuth handler",{"type":30,"tag":550,"props":664,"children":665},{},[666,670],{"type":30,"tag":572,"props":667,"children":668},{},[669],{"type":35,"value":652},{"type":30,"tag":572,"props":671,"children":672},{},[673,678],{"type":30,"tag":48,"props":674,"children":676},{"className":675},[],[677],{"type":35,"value":395},{"type":35,"value":679}," — route protection",{"type":30,"tag":550,"props":681,"children":682},{},[683,687],{"type":30,"tag":572,"props":684,"children":685},{},[686],{"type":35,"value":652},{"type":30,"tag":572,"props":688,"children":689},{},[690,695],{"type":30,"tag":48,"props":691,"children":693},{"className":692},[],[694],{"type":35,"value":451},{"type":35,"value":696}," — login page",{"type":30,"tag":550,"props":698,"children":699},{},[700,704],{"type":30,"tag":572,"props":701,"children":702},{},[703],{"type":35,"value":652},{"type":30,"tag":572,"props":705,"children":706},{},[707,712],{"type":30,"tag":48,"props":708,"children":710},{"className":709},[],[711],{"type":35,"value":534},{"type":35,"value":713}," — TypeScript types",{"key":108,"title":715,"empty":17,"ast":716},"Notes",{"type":27,"children":717},[718],{"type":30,"tag":70,"props":719,"children":720},{},[721,732],{"type":30,"tag":74,"props":722,"children":723},{},[724,730],{"type":30,"tag":48,"props":725,"children":727},{"className":726},[],[728],{"type":35,"value":729},"\u002Fdata\u002F*.json",{"type":35,"value":731}," is served as Nitro public assets (bypasses auth). Fine for now — auth protects the UI. Can move to authenticated API routes later if needed.",{"type":30,"tag":74,"props":733,"children":734},{},[735],{"type":35,"value":736},"Setting the Google consent screen to \"Internal\" adds a second layer of domain restriction at Google's level.",{"key":108,"title":738,"empty":17,"ast":739},"Verification",{"type":27,"children":740},[741],{"type":30,"tag":113,"props":742,"children":743},{},[744,760,765,775,787,792],{"type":30,"tag":74,"props":745,"children":746},{},[747,753,755],{"type":30,"tag":48,"props":748,"children":750},{"className":749},[],[751],{"type":35,"value":752},"npm run dev",{"type":35,"value":754}," → redirects to ",{"type":30,"tag":48,"props":756,"children":758},{"className":757},[],[759],{"type":35,"value":422},{"type":30,"tag":74,"props":761,"children":762},{},[763],{"type":35,"value":764},"\"Sign in with Google\" → Google consent screen",{"type":30,"tag":74,"props":766,"children":767},{},[768,773],{"type":30,"tag":48,"props":769,"children":771},{"className":770},[],[772],{"type":35,"value":61},{"type":35,"value":774}," account → dashboard with name\u002Favatar in nav",{"type":30,"tag":74,"props":776,"children":777},{},[778,780,785],{"type":35,"value":779},"Non-",{"type":30,"tag":48,"props":781,"children":783},{"className":782},[],[784],{"type":35,"value":61},{"type":35,"value":786}," account → \"Access denied\" on login page",{"type":30,"tag":74,"props":788,"children":789},{},[790],{"type":35,"value":791},"\"Sign out\" → clears session, back to login",{"type":30,"tag":74,"props":793,"children":794},{},[795,797],{"type":35,"value":796},"Direct URL while logged out → redirects to ",{"type":30,"tag":48,"props":798,"children":800},{"className":799},[],[801],{"type":35,"value":422},[],[804],"PICKUP.md",[806],{"file":804,"title":807},"Pickup Note",[],[],[],1794599368002]