fastapi-fullstack 0.2.17__py3-none-any.whl → 0.2.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_fullstack-0.2.17.dist-info → fastapi_fullstack-0.2.18.dist-info}/METADATA +4 -2
- {fastapi_fullstack-0.2.17.dist-info → fastapi_fullstack-0.2.18.dist-info}/RECORD +31 -29
- fastapi_gen/UPGRADES.yaml +5 -1
- fastapi_gen/template/hooks/post_gen_project.py +18 -12
- fastapi_gen/template/{{cookiecutter.project_slug}}/ENV_VARS.md +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +3 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +9 -4
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +8 -5
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +10 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +12 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +16 -10
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +35 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/deploy.md +39 -2
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/permissions.md +1 -1
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +13 -12
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +6 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +24 -9
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +4 -14
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +2 -14
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/magic-link/verify/route.ts +4 -14
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +6 -18
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +4 -14
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +6 -31
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/auth-cookies.test.ts +68 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/auth-cookies.ts +61 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +5 -2
- {fastapi_fullstack-0.2.17.dist-info → fastapi_fullstack-0.2.18.dist-info}/WHEEL +0 -0
- {fastapi_fullstack-0.2.17.dist-info → fastapi_fullstack-0.2.18.dist-info}/entry_points.txt +0 -0
- {fastapi_fullstack-0.2.17.dist-info → fastapi_fullstack-0.2.18.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fullstack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
4
4
|
Summary: Full-stack FastAPI + Next.js template generator with PydanticAI/LangChain agents, WebSocket streaming, 20+ enterprise integrations, and Logfire/LangSmith observability. Ship AI apps fast. CLI tool to generate production-ready FastAPI + Next.js projects with AI agents, auth, and observability.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vstorm-co/full-stack-ai-agent-template
|
|
6
6
|
Project-URL: Documentation, https://github.com/vstorm-co/full-stack-ai-agent-template#readme
|
|
@@ -298,7 +298,9 @@ For frontend deployment to **Vercel**:
|
|
|
298
298
|
cd frontend && npx vercel --prod
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
In the Vercel dashboard set `BACKEND_URL
|
|
301
|
+
In the Vercel dashboard set `BACKEND_URL` (server-side), plus `NEXT_PUBLIC_API_URL`,
|
|
302
|
+
`NEXT_PUBLIC_WS_URL` (`wss://…`) and `NEXT_PUBLIC_SITE_URL` — the `NEXT_PUBLIC_*` ones are
|
|
303
|
+
inlined at build time, so they need a redeploy to take effect.
|
|
302
304
|
|
|
303
305
|
</details>
|
|
304
306
|
|
|
@@ -17,24 +17,24 @@ fastapi_gen/upgrade/rename_guard.py,sha256=ap0ds5k93Hh6hhQhOxrDPsh-utTE1BeBYysrT
|
|
|
17
17
|
fastapi_gen/upgrade/render.py,sha256=PmEm7BPmEfUK29RcVgPN9PttTX7oHeWnDgf_vgGja1s,5497
|
|
18
18
|
fastapi_gen/upgrade/report.py,sha256=49v9kq2tkbeqqbdH-kRIM3kccaFfPVfjhi7Tm5cCVvE,3802
|
|
19
19
|
fastapi_gen/upgrade/runner.py,sha256=msnj2RYii6low6gPeZ4TRMlL01Hq8a0zJyLBzPotxCI,19302
|
|
20
|
-
fastapi_gen/UPGRADES.yaml,sha256=
|
|
20
|
+
fastapi_gen/UPGRADES.yaml,sha256=DciwcDB-T5LBX-PVTLBqSrm3t6gVL3Mhm5McBkOkORo,1524
|
|
21
21
|
fastapi_gen/template/VARIABLES.md,sha256=9CcimCDgXLshogzr1pjRYUTTmECCL9NAb1OzDFNTfgw,34322
|
|
22
22
|
fastapi_gen/template/cookiecutter.json,sha256=xikYtcbOLG4VOwZXAk-0acbELW8jXhHAGEg4kGEgRdg,6337
|
|
23
|
-
fastapi_gen/template/hooks/post_gen_project.py,sha256=
|
|
23
|
+
fastapi_gen/template/hooks/post_gen_project.py,sha256=o79caVy6BKSjgK2rpEYlLx0TEA3kAOg03yPDePuGfkY,58054
|
|
24
24
|
fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore,sha256=yC3ZUp8Txp_1TPDabDlz4pGa2p1cNwr49de0dCcctOw,1515
|
|
25
25
|
fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml,sha256=dZh-VSLL5beOtj8_UxBbD86sBdE9ubTpmWvMcrRqr-M,4651
|
|
26
26
|
fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md,sha256=HqhXNdBq1ZXlFJCC4V3CY8DorTkZkwFJcvv2O77Pwk4,2454
|
|
27
27
|
fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md,sha256=OwM3tVeznjnSr8h0Ors27AIqB7DqC8X1rYLw1fQE0x0,3657
|
|
28
28
|
fastapi_gen/template/{{cookiecutter.project_slug}}/CONTRIBUTING.md,sha256=pxP7KcIbmZbao1HPnFEykJbCHa7VfW2RQWfBMNSE_kE,3304
|
|
29
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/ENV_VARS.md,sha256=
|
|
29
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/ENV_VARS.md,sha256=NSNkHTEphs6TCKgY5kzhnLmejw9PQKOP3KmWmvCnCSA,8975
|
|
30
30
|
fastapi_gen/template/{{cookiecutter.project_slug}}/MANUAL_STEPS.md,sha256=6HJW2WE6-U4jbyQT7BjH17N6I2xAXbaAKbANiNEFNRA,7775
|
|
31
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile,sha256=
|
|
32
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/README.md,sha256=
|
|
31
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile,sha256=Pjqb8ph2XTiuEWhC99ECjYGMjq_ZiT8epdN5YgjzLos,18018
|
|
32
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/README.md,sha256=nXNh5TKj2wMqxZDxoFp7Q1ej87IL-Rt_VTGBkQLfKCw,14314
|
|
33
33
|
fastapi_gen/template/{{cookiecutter.project_slug}}/SECURITY.md,sha256=7X5u7feq2AXxWv2CMMNQZf5QQ5QkwBnzKlrcLz3RJ_Q,4735
|
|
34
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml,sha256=
|
|
35
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml,sha256=
|
|
36
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml,sha256=
|
|
37
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml,sha256=
|
|
34
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml,sha256=0LdkV4HIJX9lEtqTfYZsjbYARAvxSCeIjZ_VSRrN5LU,12347
|
|
35
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml,sha256=5Iu2j3ChoUGn7VTVKLkXbGwtI21z1WdHg0IPhV3dPOQ,2726
|
|
36
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml,sha256=WJrgVG04QZpuD0j0iOmM7id4gurbFfQu6YJGSwaDWyM,24589
|
|
37
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml,sha256=rcYyrFsuRdY6yMTfVQvnFbOeeRIRkLLdnX2u_x0_ORs,13634
|
|
38
38
|
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/settings.json,sha256=i6XfBDlx6N1AXg4-MrjmIzPhxRXvQC9PRXJFZyfNs1Y,363
|
|
39
39
|
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/add-endpoint.md,sha256=p0EsXDvW23v4uOSPRlwA84nVrB7_VVL_FnKFEE442AQ,1648
|
|
40
40
|
fastapi_gen/template/{{cookiecutter.project_slug}}/.claude/commands/fix-issue.md,sha256=3bcQwYzf_EXTit9nlkplUd0-G5T9-gGranogIDS_kjU,800
|
|
@@ -60,7 +60,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example,sha256=H
|
|
|
60
60
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml,sha256=22sYeJOrYywl_5PWgPzCyIwhpqmymIDIzx2hlBRQ-yI,681
|
|
61
61
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile,sha256=CKyuUKwt41LHIH9VB28TEzes5_TQkUEo9rIBjE3v8a8,2184
|
|
62
62
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini,sha256=vncO8XHTAW1ospWsdvtXD5PyMhp91CJmdVfv-6TakAY,737
|
|
63
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml,sha256=
|
|
63
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml,sha256=nWgclrugZfrxqEA1dgSjMNTMNfCU11Qf0Vs9AjRnpHE,14780
|
|
64
64
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py,sha256=ifDazpmfYDUI0KGxaGcAgJbz6HNzu9bQYB6li88OMcU,3163
|
|
65
65
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako,sha256=ZCfgWrEMJRDF_WF_GRGptwAIJI6YnAditBG1axwAP9Q,700
|
|
66
66
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -96,7 +96,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py,sha256=Q
|
|
|
96
96
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py,sha256=CcUQKjWvANuNCRIS_vEry6ZglCs1wIwD76HkbMa3BHw,18948
|
|
97
97
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py,sha256=DP_Hrjq0WlyuWhrkplLKmS3pVsoGZgFQgXhAcsjqBlQ,1889
|
|
98
98
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py,sha256=NXZTvQBn7iaqQVcXCUwAusSKx1PmBpcDiYl2uUa6hKA,22196
|
|
99
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py,sha256=
|
|
99
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py,sha256=vziHT47EmLWSh7RJWCAUHGu5orxm1PrINwLEc7OJJN8,30372
|
|
100
100
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py,sha256=sX9dbzwR-X-HQvPWLgEunbzEv-DcJp7Db6VsCYwf0eY,13158
|
|
101
101
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py,sha256=uSDnHa7VvbPwQ_csqjMpmTlv_RTym4GkHUasbWlQdpU,15363
|
|
102
102
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/mcp.py,sha256=g3-R_dWEwNUzNN8BPsgc1sqNR1n7JoNiwtUYRgTAm8o,8614
|
|
@@ -150,7 +150,7 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/tel
|
|
|
150
150
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py,sha256=maIccIgybZvB_54IaE9wrI8-TxdAoSb1ATWTDbk3sZU,3802
|
|
151
151
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py,sha256=1q3lkSy7cEr8KY2PNSbWYhOrJjqHKC3ujMbvxi4mkMs,5647
|
|
152
152
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py,sha256=gA4EUVDQQoTzeaLzFHZEA6Ps0eO7eo8NYjhNUDkzwzs,281
|
|
153
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py,sha256=
|
|
153
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py,sha256=Q67nC1xl-ImX-oWh-Ii1QBsl9srd9IZt3v8-Pr0bwzw,3075
|
|
154
154
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py,sha256=P912XHMGspKPltxdR7Ac7anC76a6DyDVXc_o-clV_94,3097
|
|
155
155
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/channel.py,sha256=l5tHqHirVT00g0kkPASvcYQANrsm-fq5_F2yTb-3De8,6182
|
|
156
156
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py,sha256=ocirlds_LqKbzB-s0Jbz6n0CJdv3Uec34ysZHkm16rQ,2110
|
|
@@ -390,10 +390,10 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md,sha25
|
|
|
390
390
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md,sha256=Lg8RzpLDO9tA-zVE3lFnllnTkW-FvQwc7-XMseneTq4,11806
|
|
391
391
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/commands.md,sha256=fwd58qBEN_mRS4ipOaUnR6A4B3SlqE5piMdsUXXQULg,11621
|
|
392
392
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/configuration.md,sha256=dKeJ3tmZp15HbK1ndnfQ3dVVfVGpcj9CSBXQutoxv4A,17645
|
|
393
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/deploy.md,sha256=
|
|
393
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/deploy.md,sha256=AvOHofpCqSXTNtt_7LagS5PVIxMQXorALiPe9FtTTU4,7607
|
|
394
394
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/file-processing.md,sha256=ss5pGrKvcKPqi2hU31iKd35JS2FzqPWObavfD5DpMmU,11003
|
|
395
395
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md,sha256=FiEkdtRMkhPcU32spCnnEQFts0gh-3kGchKZoA1QK9A,5976
|
|
396
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/permissions.md,sha256=
|
|
396
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/permissions.md,sha256=lzXlt-qE2p2R1tXs-Oc5EKJ4OfFDORsdyTYg5oFfYSc,9877
|
|
397
397
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md,sha256=aWdWyLDRJ7g25KXZqDM1c9OR8BlHE3XrCcB1Awz9imE,2835
|
|
398
398
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/howto/add-agent-tool.md,sha256=vQan0rrqcxNWu0GlWt3_DWN81WUddasINoUzxS3jlhE,2188
|
|
399
399
|
fastapi_gen/template/{{cookiecutter.project_slug}}/docs/howto/add-api-endpoint.md,sha256=vk4vpjvMxuqpDXNC4_KpJpGsluj6BUtgR-If20B3Egg,5883
|
|
@@ -431,12 +431,12 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/emails/compiled/welcome.html,
|
|
|
431
431
|
fastapi_gen/template/{{cookiecutter.project_slug}}/emails/compiled/welcome.txt,sha256=WzYZXM4pbV22E_C8KjSCRevn1j7wbx7ojIXYUuzp1e4,240
|
|
432
432
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore,sha256=aPWb-zCIZu6TI0jiXFs481-eMrOO-FxVvRL3992jrDY,343
|
|
433
433
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.editorconfig,sha256=bWeW8XS3NrzXpomZ4w0vGaNxmtkjVdzAz9_OimaGRVM,188
|
|
434
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example,sha256
|
|
434
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example,sha256=kKFRnghhxAoGij0jMyzwqycIGyjVwGV-dGgI3OUuErA,1481
|
|
435
435
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore,sha256=fZEN8HisQjcSmxEB3IDWoKA5dTYDzEAXLtqYKvdsprs,439
|
|
436
436
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore,sha256=wYnkkZcvXw2mtuFVzYAjH-eFldFkK5Xn26fsZZ19BK0,161
|
|
437
437
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc,sha256=1byVwTV0G71t2x5Ko6ryNm2oZ8753NfLLqhrCcon4v0,227
|
|
438
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile,sha256=
|
|
439
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md,sha256=
|
|
438
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile,sha256=aRLL3IL_5b-v0Jkev_w9Mif6D3tBa-L_GAwWW5iN1vg,2323
|
|
439
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md,sha256=2vyKiIM-umf7-RlKkgeNLIJhPAyWsan22OhSsiKyDJs,3689
|
|
440
440
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/eslint.config.mjs,sha256=y48aSi3BZvVi6GvePRqKmfaMJdK6ymN9X5fyeHFlpPU,2663
|
|
441
441
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts,sha256=KKUhYC1lApvs-Vtw9kkzqYiZBaW5axDvHhxeHznljM4,344
|
|
442
442
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts,sha256=5LYPHgUWe0H8oDoQ7aFBT-OlwC0SSvFJMmoroPAYqp0,3497
|
|
@@ -564,15 +564,15 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/admin/st
|
|
|
564
564
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/admin/users/route.ts,sha256=OHFRS6yqJ0Dssp_2JILdMrlWPOwN9IObsETNlrcQYyA,1260
|
|
565
565
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/admin/users/[userId]/route.ts,sha256=n7qz_WZIQsM9982wk8ggIB0KrcioEJ-nnujB_ElrQzQ,2493
|
|
566
566
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/admin/users/[userId]/impersonate/route.ts,sha256=IoQ5agh5YZHujKHcEfuiBRF-yL1WALXM6gkR7c0jLWM,962
|
|
567
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts,sha256=
|
|
568
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts,sha256=
|
|
567
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts,sha256=5RaTlPXW0OqhAXyxPBZc4ACbd0zlxskqVE5wkM3FHL0,1600
|
|
568
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts,sha256=UzERrsLNPhLVYcD6rSAjpqtdol9txwLdReZAW8xvqpo,905
|
|
569
569
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/magic-link/request/route.ts,sha256=NEeaJx8y4KzSCAoYMYy3BxaeIh5P6hnXj4Z3EGe-RIA,684
|
|
570
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/magic-link/verify/route.ts,sha256=
|
|
571
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts,sha256=
|
|
572
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts,sha256=
|
|
570
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/magic-link/verify/route.ts,sha256=2PQ6sHiMH3XcJsQUOs0_T9UEUF1JhR-RNEzwXAbl0bk,1215
|
|
571
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts,sha256=MEo19GyVCzg8E0SiPvf8pmLxbiKgj_80565XKiJ7Azc,2238
|
|
572
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts,sha256=Frn_q5_2SY-KrtrqKt2m0fLiDZeGtn1i6GK1X74FT-I,1252
|
|
573
573
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/password-reset/confirm/route.ts,sha256=u96o_UzWdjLkTFnHfe39BndXK6aNY4F_H-gNjOTwU1M,688
|
|
574
574
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/password-reset/request/route.ts,sha256=Udibt_OsmUVlF4KhrhoptCg7nc3X58Qt16mwb-Lg9cQ,688
|
|
575
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts,sha256=
|
|
575
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts,sha256=siZmFcGWKSQHnxBacQ4Zw0kUdMMFHnaMdzSV1wqYB8A,1307
|
|
576
576
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts,sha256=nZVHtIebfXLSFQo1YKiwRSG1sq8sx057O6Y_ufmVF94,792
|
|
577
577
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/billing/checkout/route.ts,sha256=p1M8tlu9k2OWCcImClgU8_9HuvQ9RNGzo3Nh8HSvwEg,886
|
|
578
578
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/billing/invoices/route.ts,sha256=-x41bnrREpw__eKDQLVvswWo3xaBG80AFKpS2zLPvEo,860
|
|
@@ -858,6 +858,8 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/active-route
|
|
|
858
858
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/admin-auth.ts,sha256=2K09o5o8Q24yn1dn1uAfqIIlrCgQQDz4tTFFdJVSCTU,904
|
|
859
859
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/agent-step-captions.ts,sha256=JjD-uhglLL3D9aUoNElMkjbdD-r4P9AayGUC2YmVO44,2388
|
|
860
860
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts,sha256=v9qO6pac2VH5j-o0oMhawpOUXDKqGQNrPo4KPUlLFX4,4107
|
|
861
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/auth-cookies.test.ts,sha256=dFoaULW2It1Iv9SZy5MnSAdvYEZHVJuxaOp6yBgLbzM,2802
|
|
862
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/auth-cookies.ts,sha256=p9O-xSVIzk0zpJlQ8K2-ANdYxDNhvmzMcQA8ymbKsfA,2344
|
|
861
863
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/blog.ts,sha256=xaVKtXjwO53zzvVSPEfaifwfVXQzHnpNvpM8zDfElns,4304
|
|
862
864
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/changelog.ts,sha256=1z3pSuUEks_KmO-E9u5VUt1yXqQFDOrsXeJ-RvUHB0s,4340
|
|
863
865
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/chat-sources.ts,sha256=P7P2-8OvQQTvAzSaRiqRvOb3IkFF6CDgBnRdEAsL9rU,2035
|
|
@@ -911,11 +913,11 @@ fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml
|
|
|
911
913
|
fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml,sha256=hS9-TeXJNgu28AB0Pv43ob6lLDiavlYu0bwSRI804sA,360
|
|
912
914
|
fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml,sha256=IyRrqpENJb5bQon8kxhxbJV0ohKPFyihWJ9tGPFDuVI,1524
|
|
913
915
|
fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml,sha256=o3bCXguj8E5aQ0H1o3cBD3z_iZ_RA_Pmx1bc0sVzK28,682
|
|
914
|
-
fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf,sha256=
|
|
916
|
+
fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf,sha256=2bWaxnv_rN4driEdtWgVz9eVfoKhB_Ohd_G8eOC3P3M,7619
|
|
915
917
|
fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep,sha256=o_WQQOgVGOLttT48x0EXSC4BbbDFemApFpmwhybcVUU,617
|
|
916
918
|
fastapi_gen/template/{{cookiecutter.project_slug}}/scripts/export_demo_html.py,sha256=H1tbIfr1xrh2pRuoy5NwvAOYEu0S2vgnw_XsKNMKXeM,10414
|
|
917
|
-
fastapi_fullstack-0.2.
|
|
918
|
-
fastapi_fullstack-0.2.
|
|
919
|
-
fastapi_fullstack-0.2.
|
|
920
|
-
fastapi_fullstack-0.2.
|
|
921
|
-
fastapi_fullstack-0.2.
|
|
919
|
+
fastapi_fullstack-0.2.18.dist-info/METADATA,sha256=LJac85BvO3ex5oIo2gj3B0A2qB7fLY8e1s855n2Jb7c,60593
|
|
920
|
+
fastapi_fullstack-0.2.18.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
921
|
+
fastapi_fullstack-0.2.18.dist-info/entry_points.txt,sha256=s9JXrISZp8LMYJGeVofOAd1wPTzpq-jwjSgSf4hWzjs,59
|
|
922
|
+
fastapi_fullstack-0.2.18.dist-info/licenses/LICENSE,sha256=bL4JuK_rcA8y__Gg7PEuTs3g2Qf6VvSz2w2Jajd6nVU,1063
|
|
923
|
+
fastapi_fullstack-0.2.18.dist-info/RECORD,,
|
fastapi_gen/UPGRADES.yaml
CHANGED
|
@@ -18,4 +18,8 @@
|
|
|
18
18
|
# manual_steps:
|
|
19
19
|
# - "Run `alembic upgrade head` (new billing tables)."
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
- version: "0.2.18"
|
|
22
|
+
manual_steps:
|
|
23
|
+
- "Rebuild the frontend image: `docker compose -f docker-compose.prod.yml build --no-cache frontend`. NEXT_PUBLIC_* is inlined into the browser bundle at build time and now comes from compose build args, so an image built before this upgrade still has the old ws://localhost URL baked in."
|
|
24
|
+
- "Serving the frontend over plain http:// with no TLS? Set COOKIE_SECURE=false on the frontend service. The auth cookies are Secure in production and a browser discards a Secure cookie sent over http://, which makes login appear to succeed while every request after it returns 401."
|
|
25
|
+
- "BACKEND_WS_URL and NEXT_PUBLIC_AUTH_ENABLED were never read by any code and are gone from the env examples. If your deployment sets them, the browser-facing WebSocket origin is NEXT_PUBLIC_WS_URL (build-time)."
|
|
@@ -623,19 +623,26 @@ else:
|
|
|
623
623
|
"# Backend API URL (server-side only - not exposed to browser)",
|
|
624
624
|
"BACKEND_URL=http://localhost:{{ cookiecutter.backend_port }}",
|
|
625
625
|
"",
|
|
626
|
-
"#
|
|
627
|
-
"
|
|
626
|
+
"# Send the auth cookies with the Secure flag. Unset follows NODE_ENV,",
|
|
627
|
+
"# so production means Secure — and a browser discards a Secure cookie",
|
|
628
|
+
"# served over plain http://, which logs you in and then 401s every",
|
|
629
|
+
"# request after it. Only set false for http:// on a trusted network.",
|
|
630
|
+
"# COOKIE_SECURE=false",
|
|
631
|
+
"",
|
|
632
|
+
"# WebSocket URL for the chat stream. Read by the BROWSER, so it must",
|
|
633
|
+
"# be an address the browser can reach — and NEXT_PUBLIC_* is inlined",
|
|
634
|
+
"# at build time, so in Docker it has to be passed as a build arg.",
|
|
635
|
+
"NEXT_PUBLIC_WS_URL=ws://localhost:{{ cookiecutter.backend_port }}",
|
|
636
|
+
"",
|
|
637
|
+
"# Public API URL, exposed to the browser (OAuth redirects, API docs",
|
|
638
|
+
"# links). Same build-time rule as NEXT_PUBLIC_WS_URL above.",
|
|
639
|
+
"NEXT_PUBLIC_API_URL=http://localhost:{{ cookiecutter.backend_port }}",
|
|
628
640
|
"",
|
|
629
641
|
"# Canonical site URL — used for SEO metadata, OG tags, sitemap.xml,",
|
|
630
642
|
"# robots.txt, schema.org organization. Override in production with",
|
|
631
643
|
"# the real https origin.",
|
|
632
644
|
"NEXT_PUBLIC_SITE_URL=http://localhost:{{ cookiecutter.frontend_port }}",
|
|
633
645
|
]
|
|
634
|
-
env_lines.extend([
|
|
635
|
-
"",
|
|
636
|
-
"# Authentication (always enabled)",
|
|
637
|
-
"NEXT_PUBLIC_AUTH_ENABLED=true",
|
|
638
|
-
])
|
|
639
646
|
if enable_oauth:
|
|
640
647
|
# Build the comma-separated provider list the OAuth buttons read.
|
|
641
648
|
# Currently only Google has full backend wiring; expand here when
|
|
@@ -644,9 +651,6 @@ else:
|
|
|
644
651
|
if "{{ cookiecutter.enable_oauth_google }}" == "True":
|
|
645
652
|
providers.append("google")
|
|
646
653
|
env_lines.extend([
|
|
647
|
-
"",
|
|
648
|
-
"# Public API URL for OAuth redirects (exposed to browser)",
|
|
649
|
-
"NEXT_PUBLIC_API_URL=http://localhost:{{ cookiecutter.backend_port }}",
|
|
650
654
|
"",
|
|
651
655
|
"# OAuth providers shown on /login + /register (comma-separated).",
|
|
652
656
|
"# Drives the <OAuthButtons> component — must include only providers",
|
|
@@ -670,8 +674,10 @@ else:
|
|
|
670
674
|
if "{{ cookiecutter.enable_brand_from_config }}" == "True":
|
|
671
675
|
env_lines.extend([
|
|
672
676
|
"",
|
|
673
|
-
"#
|
|
674
|
-
"#
|
|
677
|
+
"# Brand override (white-label). NEXT_PUBLIC_* is inlined at build",
|
|
678
|
+
"# time, so changing these needs a rebuild — in Docker they are",
|
|
679
|
+
"# passed as build args (see docker-compose.frontend.yml).",
|
|
680
|
+
"# NEXT_PUBLIC_BRAND_COLOR: blue, green, red, violet or orange",
|
|
675
681
|
"NEXT_PUBLIC_BRAND_COLOR={{ cookiecutter.brand_color }}",
|
|
676
682
|
"NEXT_PUBLIC_BRAND_LOGO_URL=",
|
|
677
683
|
])
|
|
@@ -174,6 +174,19 @@ group is for and which are required vs optional.
|
|
|
174
174
|
| `S3_REGION` | optional | `us-east-1` | AWS region |
|
|
175
175
|
{%- endif %}
|
|
176
176
|
|
|
177
|
+
{%- if cookiecutter.use_frontend %}
|
|
178
|
+
|
|
179
|
+
## Frontend (`frontend/.env.local`)
|
|
180
|
+
|
|
181
|
+
Separate from the backend `.env` above — `frontend/README.md` has the full table.
|
|
182
|
+
The two that decide whether a deployment works at all:
|
|
183
|
+
|
|
184
|
+
| Variable | Read by | Description |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `NEXT_PUBLIC_WS_URL` | browser | WebSocket origin for chat. Inlined at **build** time, so it needs a rebuild to change, and it must be an address the browser can reach — not a Docker service name |
|
|
187
|
+
| `COOKIE_SECURE` | server | `Secure` flag on the auth cookies. Unset follows `NODE_ENV`. A browser drops a `Secure` cookie served over plain `http://`, which makes every request after login return 401 — set `false` only for `http://` on a trusted network |
|
|
188
|
+
{%- endif %}
|
|
189
|
+
|
|
177
190
|
## Validation
|
|
178
191
|
|
|
179
192
|
```bash
|
|
@@ -387,10 +387,9 @@ vercel-deploy:
|
|
|
387
387
|
@echo "✅ Frontend deployed to Vercel!"
|
|
388
388
|
@echo " Set environment variables in Vercel dashboard:"
|
|
389
389
|
@echo " BACKEND_URL=https://api.your-domain.com"
|
|
390
|
-
@echo "
|
|
391
|
-
|
|
392
|
-
@echo "
|
|
393
|
-
{%- endif %}
|
|
390
|
+
@echo " NEXT_PUBLIC_API_URL=https://api.your-domain.com"
|
|
391
|
+
@echo " NEXT_PUBLIC_WS_URL=wss://api.your-domain.com"
|
|
392
|
+
@echo " NEXT_PUBLIC_SITE_URL=https://your-domain.com"
|
|
394
393
|
{%- if cookiecutter.enable_rag %}
|
|
395
394
|
@echo " NEXT_PUBLIC_RAG_ENABLED=true"
|
|
396
395
|
{%- endif %}
|
|
@@ -318,7 +318,7 @@ bun run lint
|
|
|
318
318
|
bun run build
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
-
The frontend talks to the backend through Next.js API route handlers in `src/app/api/*` (server-side proxy that forwards auth cookies to the FastAPI backend).
|
|
321
|
+
The frontend talks to the backend through Next.js API route handlers in `src/app/api/*` (server-side proxy that forwards auth cookies to the FastAPI backend), so HTTP traffic never leaves the frontend origin. The one exception is the chat WebSocket: the browser opens it straight against the backend at `NEXT_PUBLIC_WS_URL`, so that origin must be reachable from the browser and the backend port (or a proxy route to it) has to be exposed.
|
|
322
322
|
|
|
323
323
|
i18n (PL + EN) ships out of the box via `next-intl`. Add a new locale by extending `messages/<lang>.json` and `src/i18n.ts`.
|
|
324
324
|
{%- endif %}
|
|
@@ -335,13 +335,18 @@ cd frontend && npx vercel --prod
|
|
|
335
335
|
|
|
336
336
|
Set in the Vercel dashboard:
|
|
337
337
|
|
|
338
|
-
- `BACKEND_URL` = `https://api.your-domain.com`
|
|
339
|
-
- `
|
|
340
|
-
- `
|
|
338
|
+
- `BACKEND_URL` = `https://api.your-domain.com` (server-side only)
|
|
339
|
+
- `NEXT_PUBLIC_API_URL` = `https://api.your-domain.com`
|
|
340
|
+
- `NEXT_PUBLIC_WS_URL` = `wss://api.your-domain.com`
|
|
341
|
+
- `NEXT_PUBLIC_SITE_URL` = `https://your-domain.com`
|
|
341
342
|
{%- if cookiecutter.enable_rag %}
|
|
342
343
|
- `NEXT_PUBLIC_RAG_ENABLED` = `true`
|
|
343
344
|
{%- endif %}
|
|
344
345
|
|
|
346
|
+
`NEXT_PUBLIC_*` values are inlined into the browser bundle at build time, so
|
|
347
|
+
changing one needs a redeploy, and each must be an address the **browser** can
|
|
348
|
+
reach.
|
|
349
|
+
|
|
345
350
|
### Backend → your server
|
|
346
351
|
|
|
347
352
|
```bash
|
fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py
CHANGED
|
@@ -25,11 +25,10 @@ Configuration via settings:
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
import logging
|
|
28
|
-
from collections.abc import Callable
|
|
29
28
|
from typing import Any, TypedDict
|
|
30
29
|
|
|
31
30
|
from deepagents import create_deep_agent
|
|
32
|
-
from deepagents.backends import StateBackend
|
|
31
|
+
from deepagents.backends import BackendProtocol, StateBackend
|
|
33
32
|
from langchain_core.language_models import BaseChatModel
|
|
34
33
|
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
|
35
34
|
{%- if cookiecutter.enable_rag or cookiecutter.enable_web_search or cookiecutter.web_fetch_tool or cookiecutter.enable_charts %}
|
|
@@ -307,13 +306,17 @@ class DeepAgentsAssistant:
|
|
|
307
306
|
self._graph = None
|
|
308
307
|
self._checkpointer = MemorySaver()
|
|
309
308
|
|
|
310
|
-
def _create_backend(self) ->
|
|
309
|
+
def _create_backend(self) -> BackendProtocol:
|
|
311
310
|
"""Create the file-storage backend.
|
|
312
311
|
|
|
312
|
+
`create_deep_agent` takes a backend instance, and StateBackend reads the
|
|
313
|
+
current run's state out of the LangGraph config itself, so it needs no
|
|
314
|
+
constructor argument.
|
|
315
|
+
|
|
313
316
|
Returns:
|
|
314
|
-
|
|
317
|
+
StateBackend (in-memory file state, ephemeral, no external deps).
|
|
315
318
|
"""
|
|
316
|
-
return
|
|
319
|
+
return StateBackend()
|
|
317
320
|
|
|
318
321
|
def _create_model(self) -> BaseChatModel:
|
|
319
322
|
"""Create the LLM model for DeepAgents."""
|
|
@@ -39,10 +39,18 @@ class RedisClient:
|
|
|
39
39
|
self.client = None
|
|
40
40
|
|
|
41
41
|
async def get(self, key: str) -> str | None:
|
|
42
|
-
"""Get a value by key.
|
|
42
|
+
"""Get a value by key.
|
|
43
|
+
|
|
44
|
+
`connect()` sets decode_responses=True, so the value comes back decoded —
|
|
45
|
+
but redis-py's annotation cannot express that and still admits `bytes`.
|
|
46
|
+
Decode explicitly rather than asserting the flag through a type: ignore.
|
|
47
|
+
"""
|
|
43
48
|
if not self.client:
|
|
44
49
|
raise RuntimeError("Redis client not connected")
|
|
45
|
-
|
|
50
|
+
value: str | bytes | None = await self.client.get(key)
|
|
51
|
+
if isinstance(value, bytes):
|
|
52
|
+
return value.decode("utf-8")
|
|
53
|
+
return value
|
|
46
54
|
|
|
47
55
|
async def set(
|
|
48
56
|
self,
|
|
@@ -75,7 +75,13 @@ dependencies = [
|
|
|
75
75
|
"aiosmtplib>=3.0.0",
|
|
76
76
|
{%- endif %}
|
|
77
77
|
{%- if cookiecutter.enable_pagination %}
|
|
78
|
-
|
|
78
|
+
# 0.15.16 is broken against every FastAPI without the private _get_body_field
|
|
79
|
+
# helpers (i.e. anything below 0.140.5, which includes the pin above): its
|
|
80
|
+
# ImportError fallback binds the public get_body_field but still sets
|
|
81
|
+
# _get_body_field_new_signature = True, so add_pagination() calls it with a
|
|
82
|
+
# body_params kwarg it does not accept and the app dies on import. Lift the
|
|
83
|
+
# cap once upstream fixes the fallback or the FastAPI pin moves past 0.140.5.
|
|
84
|
+
"fastapi-pagination>=0.12.31,<0.15.16",
|
|
79
85
|
{%- endif %}
|
|
80
86
|
{%- if cookiecutter.enable_sentry %}
|
|
81
87
|
"sentry-sdk[fastapi]>=2.53.0",
|
|
@@ -154,7 +160,11 @@ dependencies = [
|
|
|
154
160
|
"langgraph-checkpoint>=4.0.0",
|
|
155
161
|
{%- endif %}
|
|
156
162
|
{%- if cookiecutter.use_deepagents %}
|
|
157
|
-
|
|
163
|
+
# Capped: the backend API is still moving between minors (0.7 dropped the
|
|
164
|
+
# runtime argument from StateBackend and switched create_deep_agent to take
|
|
165
|
+
# an instance instead of a factory), so an unbounded pin breaks generation
|
|
166
|
+
# on release day.
|
|
167
|
+
"deepagents>=0.7.1,<0.8",
|
|
158
168
|
{%- if cookiecutter.use_openai %}
|
|
159
169
|
"langchain-openai>=1.1.0",
|
|
160
170
|
{%- endif %}
|
|
@@ -11,26 +11,32 @@ services:
|
|
|
11
11
|
context: ./frontend
|
|
12
12
|
dockerfile: Dockerfile
|
|
13
13
|
args:
|
|
14
|
-
# NEXT_PUBLIC_* are baked into the browser bundle at build time
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
14
|
+
# NEXT_PUBLIC_* are baked into the browser bundle at build time, so a
|
|
15
|
+
# runtime `environment:` entry cannot change them — after editing these
|
|
16
|
+
# you must rebuild: docker compose -f docker-compose.frontend.yml build
|
|
17
|
+
# --no-cache frontend. They must point at an address the BROWSER can
|
|
18
|
+
# reach; the defaults assume the browser runs on the Docker host. When
|
|
19
|
+
# you open the app from another machine, set PUBLIC_HOST to this host's
|
|
20
|
+
# address (e.g. PUBLIC_HOST=10.0.0.5) or override each URL directly.
|
|
21
|
+
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://${PUBLIC_HOST:-localhost}:{{ cookiecutter.backend_port }}}
|
|
22
|
+
- NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL:-ws://${PUBLIC_HOST:-localhost}:{{ cookiecutter.backend_port }}}
|
|
23
|
+
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://${PUBLIC_HOST:-localhost}:{{ cookiecutter.frontend_port }}}
|
|
19
24
|
- NEXT_PUBLIC_MAX_UPLOAD_SIZE_MB=50
|
|
20
25
|
{%- if cookiecutter.enable_oauth %}
|
|
21
26
|
- NEXT_PUBLIC_OAUTH_PROVIDERS={% if cookiecutter.enable_oauth_google %}google{% endif %}
|
|
22
27
|
{%- endif %}
|
|
23
28
|
- NEXT_PUBLIC_RAG_ENABLED={% if cookiecutter.enable_rag %}true{% else %}false{% endif %}
|
|
29
|
+
{%- if cookiecutter.enable_brand_from_config %}
|
|
30
|
+
- NEXT_PUBLIC_BRAND_COLOR=${NEXT_PUBLIC_BRAND_COLOR:-{{ cookiecutter.brand_color }}}
|
|
31
|
+
- NEXT_PUBLIC_BRAND_LOGO_URL=${NEXT_PUBLIC_BRAND_LOGO_URL:-}
|
|
32
|
+
{%- endif %}
|
|
24
33
|
container_name: {{ cookiecutter.project_slug }}_frontend
|
|
25
34
|
ports:
|
|
26
35
|
- "{{ cookiecutter.frontend_port }}:{{ cookiecutter.frontend_port }}"
|
|
27
36
|
environment:
|
|
28
37
|
- NODE_ENV=development
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
{%- if cookiecutter.enable_rag %}
|
|
32
|
-
- NEXT_PUBLIC_RAG_ENABLED=true
|
|
33
|
-
{%- endif %}
|
|
38
|
+
# Server-side only: resolved inside this container, never by the browser.
|
|
39
|
+
- BACKEND_URL=${BACKEND_URL:-http://host.docker.internal:{{ cookiecutter.backend_port }}}
|
|
34
40
|
extra_hosts:
|
|
35
41
|
- "host.docker.internal:host-gateway"
|
|
36
42
|
restart: unless-stopped
|
|
@@ -313,7 +313,7 @@ services:
|
|
|
313
313
|
memory: 256M
|
|
314
314
|
|
|
315
315
|
milvus:
|
|
316
|
-
image: milvusdb/milvus:v2.6.
|
|
316
|
+
image: milvusdb/milvus:v2.6.22
|
|
317
317
|
container_name: {{ cookiecutter.project_slug }}_milvus
|
|
318
318
|
command: ["milvus", "run", "standalone"]
|
|
319
319
|
# NO external ports - only accessible within backend-internal network
|
|
@@ -655,11 +655,44 @@ services:
|
|
|
655
655
|
build:
|
|
656
656
|
context: ./frontend
|
|
657
657
|
dockerfile: Dockerfile
|
|
658
|
+
# NEXT_PUBLIC_* are inlined into the browser bundle at build time, so they
|
|
659
|
+
# must be build args — an `environment:` entry cannot override them, and
|
|
660
|
+
# changing one needs a rebuild (docker compose -f docker-compose.prod.yml
|
|
661
|
+
# build --no-cache frontend). Every value here has to be reachable from
|
|
662
|
+
# the BROWSER, not from inside the compose network.
|
|
663
|
+
args:
|
|
664
|
+
{%- if cookiecutter.include_traefik_labels or cookiecutter.include_nginx_service %}
|
|
665
|
+
# The proxy terminates TLS and serves the backend on api.DOMAIN.
|
|
666
|
+
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-https://api.${DOMAIN:-localhost}}
|
|
667
|
+
- NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL:-wss://api.${DOMAIN:-localhost}}
|
|
668
|
+
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-https://${DOMAIN:-localhost}}
|
|
669
|
+
{%- else %}
|
|
670
|
+
# No proxy in front, so the browser talks to the published ports.
|
|
671
|
+
# Set PUBLIC_HOST to this host's address — the localhost default only
|
|
672
|
+
# works when the browser runs on the Docker host itself.
|
|
673
|
+
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://${PUBLIC_HOST:-localhost}:{{ cookiecutter.backend_port }}}
|
|
674
|
+
- NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL:-ws://${PUBLIC_HOST:-localhost}:{{ cookiecutter.backend_port }}}
|
|
675
|
+
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://${PUBLIC_HOST:-localhost}:{{ cookiecutter.frontend_port }}}
|
|
676
|
+
{%- endif %}
|
|
677
|
+
- NEXT_PUBLIC_MAX_UPLOAD_SIZE_MB=${NEXT_PUBLIC_MAX_UPLOAD_SIZE_MB:-50}
|
|
678
|
+
{%- if cookiecutter.enable_oauth %}
|
|
679
|
+
- NEXT_PUBLIC_OAUTH_PROVIDERS={% if cookiecutter.enable_oauth_google %}google{% endif %}
|
|
680
|
+
{%- endif %}
|
|
681
|
+
- NEXT_PUBLIC_RAG_ENABLED={% if cookiecutter.enable_rag %}true{% else %}false{% endif %}
|
|
682
|
+
{%- if cookiecutter.enable_brand_from_config %}
|
|
683
|
+
- NEXT_PUBLIC_BRAND_COLOR=${NEXT_PUBLIC_BRAND_COLOR:-{{ cookiecutter.brand_color }}}
|
|
684
|
+
- NEXT_PUBLIC_BRAND_LOGO_URL=${NEXT_PUBLIC_BRAND_LOGO_URL:-}
|
|
685
|
+
{%- endif %}
|
|
658
686
|
container_name: {{ cookiecutter.project_slug }}_frontend
|
|
659
687
|
environment:
|
|
660
688
|
- NODE_ENV=production
|
|
689
|
+
# Server-side only: resolved inside the compose network by the Next server.
|
|
661
690
|
- BACKEND_URL=http://app:{{ cookiecutter.backend_port }}
|
|
662
|
-
|
|
691
|
+
# The auth cookies are Secure in production, and a browser discards a
|
|
692
|
+
# Secure cookie that arrives over plain HTTP — login then "works" while
|
|
693
|
+
# every request after it is 401. Serve this over TLS, or set
|
|
694
|
+
# COOKIE_SECURE=false if it really is http:// on a trusted network.
|
|
695
|
+
- COOKIE_SECURE=${COOKIE_SECURE:-true}
|
|
663
696
|
{%- if cookiecutter.include_traefik_labels %}
|
|
664
697
|
networks:
|
|
665
698
|
- traefik-public
|
|
@@ -32,10 +32,45 @@ curl http://localhost:{{ cookiecutter.backend_port }}/api/v1/health
|
|
|
32
32
|
{% if cookiecutter.use_frontend %}# Frontend: http://localhost:{{ cookiecutter.frontend_port }}{% endif %}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
{% if cookiecutter.use_frontend %}
|
|
36
|
+
### Serving from a host that isn't localhost
|
|
37
|
+
|
|
38
|
+
The defaults assume the browser runs on the Docker host. Opening the app from
|
|
39
|
+
another machine (a LAN IP, a staging box, a tunnel) needs two things set, and
|
|
40
|
+
both fail in a way that looks like something else:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# In the .env next to docker-compose.prod.yml
|
|
44
|
+
PUBLIC_HOST=10.0.0.5 # an address the BROWSER can reach
|
|
45
|
+
COOKIE_SECURE=false # ONLY if you serve over plain http:// (see below)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# NEXT_PUBLIC_* is inlined into the bundle, so this needs a real rebuild
|
|
50
|
+
docker compose -f docker-compose.prod.yml build --no-cache frontend
|
|
51
|
+
docker compose -f docker-compose.prod.yml up -d frontend
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
1. **`NEXT_PUBLIC_*` is baked in at build time.** Next.js inlines those values
|
|
55
|
+
into the JavaScript the browser downloads, so a runtime env var cannot change
|
|
56
|
+
them — `docker-compose.prod.yml` passes them as `build:` args instead. Miss
|
|
57
|
+
this and the chat socket dials `ws://localhost:{{ cookiecutter.backend_port }}` from
|
|
58
|
+
the visitor's own machine and the input stays disabled ("Offline").
|
|
59
|
+
2. **The auth cookies are `Secure` in production, and a browser silently drops a
|
|
60
|
+
`Secure` cookie that arrives over `http://`.** Login looks like it worked (the
|
|
61
|
+
user comes back in the response body) and then every request carrying no
|
|
62
|
+
cookie answers 401, `/api/auth/me` included, so the token never refreshes.
|
|
63
|
+
TLS is the real fix; `COOKIE_SECURE=false` is the escape hatch for a trusted
|
|
64
|
+
network.
|
|
65
|
+
|
|
66
|
+
Reaching the backend matters too: the chat socket is opened by the browser
|
|
67
|
+
directly against `NEXT_PUBLIC_WS_URL`, so either publish the backend port or
|
|
68
|
+
route `/api/v1/ws` through your proxy with the `Upgrade` headers set.
|
|
69
|
+
{% endif %}
|
|
35
70
|
### Reverse proxy
|
|
36
71
|
|
|
37
72
|
{%- if cookiecutter.use_nginx %}
|
|
38
|
-
Nginx config in `nginx
|
|
73
|
+
Nginx config in `nginx/nginx.conf` proxies `/` → frontend, `/` on `api.DOMAIN` → backend, and `/api/v1/ws` → backend WebSocket. Update `server_name` and the TLS cert paths there.
|
|
39
74
|
{%- elif cookiecutter.use_traefik %}
|
|
40
75
|
Traefik labels in `docker-compose.yml` route based on `Host()`. Set `DOMAIN` env var, then point your DNS at the host. ACME / Let's Encrypt configured via labels — uncomment in `docker-compose.yml` and set `ACME_EMAIL`.
|
|
41
76
|
{%- else %}
|
|
@@ -108,7 +143,9 @@ cd frontend
|
|
|
108
143
|
vercel
|
|
109
144
|
```
|
|
110
145
|
|
|
111
|
-
Set `BACKEND_URL
|
|
146
|
+
Set `BACKEND_URL`, `NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_WS_URL` (`wss://…`) and
|
|
147
|
+
`NEXT_PUBLIC_SITE_URL` in the Vercel dashboard, pointing at your backend host,
|
|
148
|
+
then redeploy — the `NEXT_PUBLIC_*` ones are only picked up by a fresh build.
|
|
112
149
|
{% else %}
|
|
113
150
|
Not applicable — this project doesn't generate a frontend.
|
|
114
151
|
{% endif %}
|
|
@@ -51,7 +51,7 @@ These are defined in `app/api/deps.py` and used throughout the route layer:
|
|
|
51
51
|
|
|
52
52
|
| Endpoint | Method | Admin | User | Notes |
|
|
53
53
|
|----------|--------|-------|------|-------|
|
|
54
|
-
| `/
|
|
54
|
+
| `/ws/agent` | WS | Y | Y | WebSocket chat with AI agent |
|
|
55
55
|
|
|
56
56
|
### Conversations
|
|
57
57
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# Backend API URL (server-side only - not exposed to browser)
|
|
2
2
|
BACKEND_URL=http://localhost:{{ cookiecutter.backend_port }}
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
|
|
4
|
+
# Send the auth cookies with the Secure flag. Unset follows NODE_ENV, so
|
|
5
|
+
# production means Secure — and a browser silently discards a Secure cookie
|
|
6
|
+
# served over plain http://, which logs you in and then 401s every request
|
|
7
|
+
# after it. Only set this to false for an http:// deployment on a trusted
|
|
8
|
+
# network; the real fix is to serve the app over TLS.
|
|
9
|
+
# COOKIE_SECURE=false
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
NEXT_PUBLIC_AUTH_ENABLED=false
|
|
13
|
-
{%- endif %}
|
|
14
|
-
{%- if cookiecutter.enable_oauth %}
|
|
11
|
+
# WebSocket URL for the chat stream. Read by the BROWSER, so it must be an
|
|
12
|
+
# address the browser can reach (not a Docker service name), and because
|
|
13
|
+
# NEXT_PUBLIC_* is inlined at build time it has to be set before `bun run
|
|
14
|
+
# build` — in Docker that means a build arg, see docker-compose.frontend.yml.
|
|
15
|
+
NEXT_PUBLIC_WS_URL=ws://localhost:{{ cookiecutter.backend_port }}
|
|
15
16
|
|
|
16
|
-
# Public API URL
|
|
17
|
+
# Public API URL, exposed to the browser (OAuth redirects, links to the API
|
|
18
|
+
# docs). Same build-time rule as NEXT_PUBLIC_WS_URL above.
|
|
17
19
|
NEXT_PUBLIC_API_URL=http://localhost:{{ cookiecutter.backend_port }}
|
|
18
|
-
{%- endif %}
|
|
19
20
|
{%- if cookiecutter.enable_rag %}
|
|
20
21
|
|
|
21
22
|
# RAG (Retrieval Augmented Generation)
|
|
@@ -36,6 +36,12 @@ ENV NEXT_PUBLIC_OAUTH_PROVIDERS=$NEXT_PUBLIC_OAUTH_PROVIDERS
|
|
|
36
36
|
{%- endif %}
|
|
37
37
|
ARG NEXT_PUBLIC_RAG_ENABLED=false
|
|
38
38
|
ENV NEXT_PUBLIC_RAG_ENABLED=$NEXT_PUBLIC_RAG_ENABLED
|
|
39
|
+
{%- if cookiecutter.enable_brand_from_config %}
|
|
40
|
+
ARG NEXT_PUBLIC_BRAND_COLOR={{ cookiecutter.brand_color }}
|
|
41
|
+
ENV NEXT_PUBLIC_BRAND_COLOR=$NEXT_PUBLIC_BRAND_COLOR
|
|
42
|
+
ARG NEXT_PUBLIC_BRAND_LOGO_URL=
|
|
43
|
+
ENV NEXT_PUBLIC_BRAND_LOGO_URL=$NEXT_PUBLIC_BRAND_LOGO_URL
|
|
44
|
+
{%- endif %}
|
|
39
45
|
|
|
40
46
|
# Build the application
|
|
41
47
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
@@ -21,18 +21,30 @@ Or run it in Docker from the project root: `make dev-frontend`.
|
|
|
21
21
|
|
|
22
22
|
Copy `.env.example` to `.env.local` and adjust as needed:
|
|
23
23
|
|
|
24
|
-
| Variable | Description |
|
|
25
|
-
|
|
26
|
-
| `BACKEND_URL` | Backend HTTP base URL
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
24
|
+
| Variable | Read by | Description |
|
|
25
|
+
|----------|---------|-------------|
|
|
26
|
+
| `BACKEND_URL` | server | Backend HTTP base URL used by the route handlers in `src/app/api/*` |
|
|
27
|
+
| `COOKIE_SECURE` | server | `Secure` flag on the auth cookies. Unset follows `NODE_ENV`; set `false` only for an `http://` deployment on a trusted network |
|
|
28
|
+
| `NEXT_PUBLIC_WS_URL` | browser | Backend WebSocket origin for the chat stream (e.g. `wss://api.example.com`) |
|
|
29
|
+
| `NEXT_PUBLIC_API_URL` | browser | Public API URL (OAuth redirects, links to the API docs) |
|
|
30
|
+
| `NEXT_PUBLIC_SITE_URL` | browser | Canonical site origin for SEO metadata, OG tags, `sitemap.xml` |
|
|
29
31
|
{%- if cookiecutter.enable_oauth %}
|
|
30
|
-
| `
|
|
32
|
+
| `NEXT_PUBLIC_OAUTH_PROVIDERS` | browser | Comma-separated providers shown on `/login` + `/register` |
|
|
31
33
|
{%- endif %}
|
|
32
34
|
{%- if cookiecutter.enable_rag %}
|
|
33
|
-
| `NEXT_PUBLIC_RAG_ENABLED` | Show knowledge-base / RAG UI |
|
|
35
|
+
| `NEXT_PUBLIC_RAG_ENABLED` | browser | Show knowledge-base / RAG UI |
|
|
34
36
|
{%- endif %}
|
|
35
37
|
|
|
38
|
+
Two rules that cause most of the deployment confusion:
|
|
39
|
+
|
|
40
|
+
- **`NEXT_PUBLIC_*` is inlined into the browser bundle at build time.** Setting one
|
|
41
|
+
at runtime does nothing — you have to set it before `bun run build`, which in
|
|
42
|
+
Docker means a `build:` arg (see `docker-compose.frontend.yml`) followed by a
|
|
43
|
+
rebuild. Everything else in the table is read at runtime.
|
|
44
|
+
- **`NEXT_PUBLIC_*` values must be reachable from the browser**, so never a Docker
|
|
45
|
+
service name. `BACKEND_URL` is the opposite: it is resolved inside the
|
|
46
|
+
container, so a service name is exactly right there.
|
|
47
|
+
|
|
36
48
|
## Scripts
|
|
37
49
|
|
|
38
50
|
```bash
|
|
@@ -71,5 +83,8 @@ Add a locale by extending `i18n.ts` and providing its message catalog.
|
|
|
71
83
|
npx vercel --prod
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
In the Vercel dashboard set `BACKEND_URL`,
|
|
75
|
-
`
|
|
86
|
+
In the Vercel dashboard set `BACKEND_URL=https://api.your-domain.com`,
|
|
87
|
+
`NEXT_PUBLIC_API_URL=https://api.your-domain.com`, `NEXT_PUBLIC_WS_URL=wss://api.your-domain.com`
|
|
88
|
+
and `NEXT_PUBLIC_SITE_URL=https://your-domain.com`, then redeploy — the
|
|
89
|
+
`NEXT_PUBLIC_*` ones only take effect on a fresh build. See the project root
|
|
90
|
+
`docs/deploy.md` for details.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { setAuthCookies } from "@/lib/auth-cookies";
|
|
2
3
|
import { backendFetch, BackendApiError } from "@/lib/server-api";
|
|
3
4
|
import type { LoginResponse } from "@/types";
|
|
4
5
|
|
|
@@ -31,20 +32,9 @@ export async function POST(request: NextRequest) {
|
|
|
31
32
|
message: "Login successful",
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
response
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sameSite: "lax",
|
|
38
|
-
maxAge: 60 * 15, // 15 minutes
|
|
39
|
-
path: "/",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
response.cookies.set("refresh_token", data.refresh_token, {
|
|
43
|
-
httpOnly: true,
|
|
44
|
-
secure: process.env.NODE_ENV === "production",
|
|
45
|
-
sameSite: "lax",
|
|
46
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
47
|
-
path: "/",
|
|
35
|
+
setAuthCookies(response, {
|
|
36
|
+
accessToken: data.access_token,
|
|
37
|
+
refreshToken: data.refresh_token,
|
|
48
38
|
});
|
|
49
39
|
|
|
50
40
|
return response;
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
|
|
3
|
+
import { clearAuthCookies } from "@/lib/auth-cookies";
|
|
3
4
|
import { backendFetch, BackendApiError } from "@/lib/server-api";
|
|
4
5
|
|
|
5
6
|
export async function POST(request: NextRequest) {
|
|
@@ -22,20 +23,7 @@ export async function POST(request: NextRequest) {
|
|
|
22
23
|
|
|
23
24
|
const response = NextResponse.json({ message: "Logged out successfully" });
|
|
24
25
|
|
|
25
|
-
response
|
|
26
|
-
httpOnly: true,
|
|
27
|
-
secure: process.env.NODE_ENV === "production",
|
|
28
|
-
sameSite: "lax",
|
|
29
|
-
maxAge: 0,
|
|
30
|
-
path: "/",
|
|
31
|
-
});
|
|
32
|
-
response.cookies.set("refresh_token", "", {
|
|
33
|
-
httpOnly: true,
|
|
34
|
-
secure: process.env.NODE_ENV === "production",
|
|
35
|
-
sameSite: "lax",
|
|
36
|
-
maxAge: 0,
|
|
37
|
-
path: "/",
|
|
38
|
-
});
|
|
26
|
+
clearAuthCookies(response);
|
|
39
27
|
|
|
40
28
|
return response;
|
|
41
29
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from "next/server";
|
|
2
2
|
|
|
3
|
+
import { setAuthCookies } from "@/lib/auth-cookies";
|
|
3
4
|
import { BackendApiError, backendFetch } from "@/lib/server-api";
|
|
4
5
|
|
|
5
6
|
interface TokenResponse {
|
|
@@ -26,20 +27,9 @@ export async function POST(request: NextRequest) {
|
|
|
26
27
|
message: "Sign-in successful",
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
secure: isProd,
|
|
33
|
-
sameSite: "lax",
|
|
34
|
-
maxAge: 60 * 15,
|
|
35
|
-
path: "/",
|
|
36
|
-
});
|
|
37
|
-
response.cookies.set("refresh_token", data.refresh_token, {
|
|
38
|
-
httpOnly: true,
|
|
39
|
-
secure: isProd,
|
|
40
|
-
sameSite: "lax",
|
|
41
|
-
maxAge: 60 * 60 * 24 * 7,
|
|
42
|
-
path: "/",
|
|
30
|
+
setAuthCookies(response, {
|
|
31
|
+
accessToken: data.access_token,
|
|
32
|
+
refreshToken: data.refresh_token,
|
|
43
33
|
});
|
|
44
34
|
return response;
|
|
45
35
|
} catch (error) {
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { clearAuthCookies, setAuthCookies } from "@/lib/auth-cookies";
|
|
2
3
|
import { backendFetch, BackendApiError } from "@/lib/server-api";
|
|
3
4
|
import type { User } from "@/types";
|
|
4
5
|
|
|
5
|
-
const ACCESS_MAXAGE = 60 * 15; // 15 min
|
|
6
|
-
const REFRESH_MAXAGE = 60 * 60 * 24 * 7; // 7 days
|
|
7
|
-
|
|
8
|
-
const cookieOpts = (maxAge: number) =>
|
|
9
|
-
({
|
|
10
|
-
httpOnly: true,
|
|
11
|
-
secure: process.env.NODE_ENV === "production",
|
|
12
|
-
sameSite: "lax" as const,
|
|
13
|
-
maxAge,
|
|
14
|
-
path: "/",
|
|
15
|
-
}) as const;
|
|
16
|
-
|
|
17
6
|
function fetchMe(token: string) {
|
|
18
7
|
return backendFetch<User>("/api/v1/auth/me", {
|
|
19
8
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -55,16 +44,15 @@ export async function GET(request: NextRequest) {
|
|
|
55
44
|
);
|
|
56
45
|
const data = await fetchMe(refreshed.access_token);
|
|
57
46
|
const response = NextResponse.json({ ...data, access_token: refreshed.access_token });
|
|
58
|
-
response
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
47
|
+
setAuthCookies(response, {
|
|
48
|
+
accessToken: refreshed.access_token,
|
|
49
|
+
refreshToken: refreshed.refresh_token,
|
|
50
|
+
});
|
|
62
51
|
return response;
|
|
63
52
|
} catch {
|
|
64
53
|
// Refresh failed → truly logged out. Clear cookies.
|
|
65
54
|
const response = NextResponse.json({ detail: "Not authenticated" }, { status: 401 });
|
|
66
|
-
response
|
|
67
|
-
response.cookies.set("refresh_token", "", cookieOpts(0));
|
|
55
|
+
clearAuthCookies(response);
|
|
68
56
|
return response;
|
|
69
57
|
}
|
|
70
58
|
}
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
|
|
3
|
+
import { setAuthCookies } from "@/lib/auth-cookies";
|
|
3
4
|
import { backendFetch, BackendApiError } from "@/lib/server-api";
|
|
4
5
|
|
|
5
6
|
interface OAuthCallbackBody {
|
|
@@ -24,20 +25,9 @@ export async function POST(request: NextRequest) {
|
|
|
24
25
|
message: "Sign-in successful",
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
secure: isProd,
|
|
31
|
-
sameSite: "lax",
|
|
32
|
-
maxAge: 60 * 15,
|
|
33
|
-
path: "/",
|
|
34
|
-
});
|
|
35
|
-
response.cookies.set("refresh_token", body.refresh_token, {
|
|
36
|
-
httpOnly: true,
|
|
37
|
-
secure: isProd,
|
|
38
|
-
sameSite: "lax",
|
|
39
|
-
maxAge: 60 * 60 * 24 * 7,
|
|
40
|
-
path: "/",
|
|
28
|
+
setAuthCookies(response, {
|
|
29
|
+
accessToken: body.access_token,
|
|
30
|
+
refreshToken: body.refresh_token,
|
|
41
31
|
});
|
|
42
32
|
return response;
|
|
43
33
|
} catch (error) {
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { clearAuthCookies, setAuthCookies } from "@/lib/auth-cookies";
|
|
2
3
|
import { backendFetch, BackendApiError } from "@/lib/server-api";
|
|
3
4
|
import type { RefreshTokenResponse } from "@/types";
|
|
4
5
|
|
|
@@ -20,44 +21,18 @@ export async function POST(request: NextRequest) {
|
|
|
20
21
|
message: "Token refreshed",
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
maxAge: 60 * 15, // 15 minutes
|
|
28
|
-
path: "/",
|
|
24
|
+
// The refresh token is only re-set when the backend rotated it.
|
|
25
|
+
setAuthCookies(response, {
|
|
26
|
+
accessToken: data.access_token,
|
|
27
|
+
refreshToken: data.refresh_token,
|
|
29
28
|
});
|
|
30
29
|
|
|
31
|
-
// Rotate refresh token if backend returns a new one
|
|
32
|
-
if (data.refresh_token) {
|
|
33
|
-
response.cookies.set("refresh_token", data.refresh_token, {
|
|
34
|
-
httpOnly: true,
|
|
35
|
-
secure: process.env.NODE_ENV === "production",
|
|
36
|
-
sameSite: "lax",
|
|
37
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
38
|
-
path: "/",
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
30
|
return response;
|
|
43
31
|
} catch (error) {
|
|
44
32
|
if (error instanceof BackendApiError) {
|
|
45
33
|
const response = NextResponse.json({ detail: "Session expired" }, { status: 401 });
|
|
46
34
|
|
|
47
|
-
response
|
|
48
|
-
httpOnly: true,
|
|
49
|
-
secure: process.env.NODE_ENV === "production",
|
|
50
|
-
sameSite: "lax",
|
|
51
|
-
maxAge: 0,
|
|
52
|
-
path: "/",
|
|
53
|
-
});
|
|
54
|
-
response.cookies.set("refresh_token", "", {
|
|
55
|
-
httpOnly: true,
|
|
56
|
-
secure: process.env.NODE_ENV === "production",
|
|
57
|
-
sameSite: "lax",
|
|
58
|
-
maxAge: 0,
|
|
59
|
-
path: "/",
|
|
60
|
-
});
|
|
35
|
+
clearAuthCookies(response);
|
|
61
36
|
|
|
62
37
|
return response;
|
|
63
38
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import {
|
|
4
|
+
ACCESS_TOKEN_MAX_AGE,
|
|
5
|
+
REFRESH_TOKEN_MAX_AGE,
|
|
6
|
+
clearAuthCookies,
|
|
7
|
+
resolveCookieSecure,
|
|
8
|
+
setAuthCookies,
|
|
9
|
+
} from "./auth-cookies";
|
|
10
|
+
|
|
11
|
+
describe("resolveCookieSecure", () => {
|
|
12
|
+
it("follows NODE_ENV when COOKIE_SECURE is unset", () => {
|
|
13
|
+
expect(resolveCookieSecure({ NODE_ENV: "production" })).toBe(true);
|
|
14
|
+
expect(resolveCookieSecure({ NODE_ENV: "development" })).toBe(false);
|
|
15
|
+
expect(resolveCookieSecure({})).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("lets COOKIE_SECURE override NODE_ENV in both directions", () => {
|
|
19
|
+
// The whole point of the flag: a production build served over plain HTTP
|
|
20
|
+
// has to be able to turn Secure off, or the browser drops the cookie.
|
|
21
|
+
expect(resolveCookieSecure({ COOKIE_SECURE: "false", NODE_ENV: "production" })).toBe(false);
|
|
22
|
+
expect(resolveCookieSecure({ COOKIE_SECURE: "true", NODE_ENV: "development" })).toBe(true);
|
|
23
|
+
expect(resolveCookieSecure({ COOKIE_SECURE: "FALSE", NODE_ENV: "production" })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("ignores a value it does not understand instead of guessing", () => {
|
|
27
|
+
// A typo must not silently downgrade production to insecure cookies.
|
|
28
|
+
expect(resolveCookieSecure({ COOKIE_SECURE: "0", NODE_ENV: "production" })).toBe(true);
|
|
29
|
+
expect(resolveCookieSecure({ COOKIE_SECURE: "", NODE_ENV: "production" })).toBe(true);
|
|
30
|
+
expect(resolveCookieSecure({ COOKIE_SECURE: "yes", NODE_ENV: "development" })).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("setAuthCookies", () => {
|
|
35
|
+
it("stores both tokens httpOnly, scoped to the whole site", () => {
|
|
36
|
+
const response = NextResponse.json({});
|
|
37
|
+
setAuthCookies(response, { accessToken: "access", refreshToken: "refresh" });
|
|
38
|
+
|
|
39
|
+
const access = response.cookies.get("access_token");
|
|
40
|
+
expect(access?.value).toBe("access");
|
|
41
|
+
expect(access?.httpOnly).toBe(true);
|
|
42
|
+
expect(access?.sameSite).toBe("lax");
|
|
43
|
+
expect(access?.path).toBe("/");
|
|
44
|
+
expect(access?.maxAge).toBe(ACCESS_TOKEN_MAX_AGE);
|
|
45
|
+
|
|
46
|
+
const refresh = response.cookies.get("refresh_token");
|
|
47
|
+
expect(refresh?.value).toBe("refresh");
|
|
48
|
+
expect(refresh?.maxAge).toBe(REFRESH_TOKEN_MAX_AGE);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("leaves the refresh cookie alone when the backend did not rotate it", () => {
|
|
52
|
+
const response = NextResponse.json({});
|
|
53
|
+
setAuthCookies(response, { accessToken: "access" });
|
|
54
|
+
|
|
55
|
+
expect(response.cookies.get("access_token")?.value).toBe("access");
|
|
56
|
+
expect(response.cookies.get("refresh_token")).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("clearAuthCookies", () => {
|
|
61
|
+
it("expires both cookies", () => {
|
|
62
|
+
const response = NextResponse.json({});
|
|
63
|
+
clearAuthCookies(response);
|
|
64
|
+
|
|
65
|
+
expect(response.cookies.get("access_token")?.maxAge).toBe(0);
|
|
66
|
+
expect(response.cookies.get("refresh_token")?.maxAge).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth cookie handling shared by every `/api/auth/*` route handler.
|
|
3
|
+
*
|
|
4
|
+
* The browser silently discards a `Secure` cookie sent from an insecure origin.
|
|
5
|
+
* A deployment served over plain HTTP — a LAN IP, a bare Docker host, a tunnel
|
|
6
|
+
* without TLS — therefore accepts the login response and then sends no cookie
|
|
7
|
+
* back on any request after it, so `/api/auth/me` answers 401 forever, the
|
|
8
|
+
* access token is never refreshed and the chat socket never authenticates.
|
|
9
|
+
*
|
|
10
|
+
* `COOKIE_SECURE` makes that flag explicit: unset follows `NODE_ENV`, `false`
|
|
11
|
+
* opts a trusted-network HTTP deployment out, `true` forces it on (e.g. a dev
|
|
12
|
+
* server behind an HTTPS proxy). Anything else falls back to the `NODE_ENV`
|
|
13
|
+
* default rather than guessing, so a typo cannot downgrade production.
|
|
14
|
+
*/
|
|
15
|
+
import type { NextResponse } from "next/server";
|
|
16
|
+
|
|
17
|
+
export const ACCESS_TOKEN_MAX_AGE = 60 * 15; // 15 minutes
|
|
18
|
+
export const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
|
19
|
+
|
|
20
|
+
/** Exported for the unit test — the module-level value is read once at startup. */
|
|
21
|
+
export function resolveCookieSecure(env: { COOKIE_SECURE?: string; NODE_ENV?: string }): boolean {
|
|
22
|
+
switch (env.COOKIE_SECURE?.toLowerCase()) {
|
|
23
|
+
case "false":
|
|
24
|
+
return false;
|
|
25
|
+
case "true":
|
|
26
|
+
return true;
|
|
27
|
+
default:
|
|
28
|
+
return env.NODE_ENV === "production";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const COOKIE_SECURE = resolveCookieSecure(process.env);
|
|
33
|
+
|
|
34
|
+
const cookieOptions = (maxAge: number) =>
|
|
35
|
+
({
|
|
36
|
+
httpOnly: true,
|
|
37
|
+
secure: COOKIE_SECURE,
|
|
38
|
+
sameSite: "lax",
|
|
39
|
+
maxAge,
|
|
40
|
+
path: "/",
|
|
41
|
+
}) as const;
|
|
42
|
+
|
|
43
|
+
interface AuthTokens {
|
|
44
|
+
accessToken: string;
|
|
45
|
+
/** Only set when the backend issued (or rotated) one. */
|
|
46
|
+
refreshToken?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Store the session tokens as httpOnly cookies on an outgoing response. */
|
|
50
|
+
export function setAuthCookies(response: NextResponse, { accessToken, refreshToken }: AuthTokens) {
|
|
51
|
+
response.cookies.set("access_token", accessToken, cookieOptions(ACCESS_TOKEN_MAX_AGE));
|
|
52
|
+
if (refreshToken) {
|
|
53
|
+
response.cookies.set("refresh_token", refreshToken, cookieOptions(REFRESH_TOKEN_MAX_AGE));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Expire both session cookies — logout, or a refresh token the backend rejected. */
|
|
58
|
+
export function clearAuthCookies(response: NextResponse) {
|
|
59
|
+
response.cookies.set("access_token", "", cookieOptions(0));
|
|
60
|
+
response.cookies.set("refresh_token", "", cookieOptions(0));
|
|
61
|
+
}
|
|
@@ -96,8 +96,11 @@ server {
|
|
|
96
96
|
proxy_set_header X-Forwarded-Host $host;
|
|
97
97
|
proxy_set_header X-Forwarded-Port $server_port;
|
|
98
98
|
|
|
99
|
-
# WebSocket support
|
|
100
|
-
|
|
99
|
+
# WebSocket support. The chat sockets are mounted under the versioned API
|
|
100
|
+
# prefix (/api/v1/ws/agent, /api/v1/ws/projects/...), so this prefix has to
|
|
101
|
+
# match it — anything falling through to `location /` below is proxied
|
|
102
|
+
# without the Upgrade headers and the handshake fails.
|
|
103
|
+
location /api/v1/ws {
|
|
101
104
|
proxy_pass http://backend;
|
|
102
105
|
proxy_http_version 1.1;
|
|
103
106
|
proxy_set_header Upgrade $http_upgrade;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|