Template
Update Convex with no payload to be just like convex with payload but without payload
This commit is contained in:
1 parent
13b8b36c4c
commit
fba73a92ce
130 files changed
+15637
-32018
No files matched your search
@@ -1 +1 @@
|
||||
[{"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/forgot-password/layout.tsx":"1","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/forgot-password/page.tsx":"2","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/profile/layout.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/profile/page.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/sign-in/layout.tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(auth)/sign-in/page.tsx":"6","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/global-error.tsx":"7","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/layout.tsx":"8","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/page.tsx":"9","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx":"10","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/features.tsx":"11","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/hero.tsx":"12","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx":"13","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx":"14","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx":"15","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx":"16","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/avatar-upload.tsx":"17","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/header.tsx":"18","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/index.tsx":"19","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx":"20","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx":"21","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/user-info.tsx":"22","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx":"23","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx":"24","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx":"25","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/index.tsx":"26","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/ConvexClientProvider.tsx":"27","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/index.tsx":"28","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts":"29","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation.ts":"30","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/metadata.ts":"31","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/proxy/ban-sus-ips.ts":"32","/home/gib/Documents/Code/convex-monorepo/apps/next/src/proxy.ts":"33"},{"size":298,"mtime":1768236967188,"results":"34","hashOfConfig":"35"},{"size":10607,"mtime":1768372347281,"results":"36","hashOfConfig":"35"},{"size":276,"mtime":1768236979857,"results":"37","hashOfConfig":"35"},{"size":1515,"mtime":1768372347312,"results":"38","hashOfConfig":"35"},{"size":274,"mtime":1768236849421,"results":"39","hashOfConfig":"35"},{"size":16567,"mtime":1774544486885,"results":"40","hashOfConfig":"35"},{"size":2410,"mtime":1774544486967,"results":"41","hashOfConfig":"35"},{"size":2042,"mtime":1774131581680,"results":"42","hashOfConfig":"35"},{"size":253,"mtime":1768372347406,"results":"43","hashOfConfig":"35"},{"size":1139,"mtime":1774544487103,"results":"44","hashOfConfig":"35"},{"size":2916,"mtime":1774544487137,"results":"45","hashOfConfig":"35"},{"size":4045,"mtime":1774544487187,"results":"46","hashOfConfig":"35"},{"size":141,"mtime":1768325738000,"results":"47","hashOfConfig":"35"},{"size":2697,"mtime":1774544487253,"results":"48","hashOfConfig":"35"},{"size":1081,"mtime":1768372347496,"results":"49","hashOfConfig":"35"},{"size":52,"mtime":1768188510036,"results":"50","hashOfConfig":"35"},{"size":6797,"mtime":1768372347532,"results":"51","hashOfConfig":"35"},{"size":384,"mtime":1774544487414,"results":"52","hashOfConfig":"35"},{"size":230,"mtime":1768238412936,"results":"53","hashOfConfig":"35"},{"size":5681,"mtime":1768372347569,"results":"54","hashOfConfig":"35"},{"size":1234,"mtime":1768372347581,"results":"55","hashOfConfig":"35"},{"size":4786,"mtime":1774544487590,"results":"56","hashOfConfig":"35"},{"size":4010,"mtime":1768372347611,"results":"57","hashOfConfig":"35"},{"size":2539,"mtime":1768372347626,"results":"58","hashOfConfig":"35"},{"size":499,"mtime":1768372347633,"results":"59","hashOfConfig":"35"},{"size":2470,"mtime":1774544487733,"results":"60","hashOfConfig":"35"},{"size":444,"mtime":1774544487759,"rLine truncated
|
||||
[{"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx":"1","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/features.tsx":"2","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/hero.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx":"6","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx":"7","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/avatar-upload.tsx":"8","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/header.tsx":"9","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/index.tsx":"10","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx":"11","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx":"12","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/user-info.tsx":"13","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx":"14","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx":"15","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx":"16","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/index.tsx":"17","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/ConvexClientProvider.tsx":"18","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/providers/index.tsx":"19","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts":"20","/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation.ts":"21","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/metadata.ts":"22","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/proxy/ban-sus-ips.ts":"23","/home/gib/Documents/Code/convex-monorepo/apps/next/src/proxy.ts":"24","/home/gib/Documents/Code/convex-monorepo/apps/next/payload-types.ts":"25","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/forgot-password/layout.tsx":"26","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/forgot-password/page.tsx":"27","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/profile/layout.tsx":"28","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/profile/page.tsx":"29","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/sign-in/layout.tsx":"30","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/(auth)/sign-in/page.tsx":"31","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/global-error.tsx":"32","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/layout.tsx":"33","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(frontend)/page.tsx":"34","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/[[...segments]]/not-found.tsx":"35","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/[[...segments]]/page.tsx":"36","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/admin/importMap.js":"37","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/[...slug]/route.ts":"38","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/graphql/route.ts":"39","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/api/graphql-playground/route.ts":"40","/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/(payload)/layout.tsx":"41","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/content.ts":"42","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/faq.tsx":"43","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/logo-cloud.tsx":"44","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/page-builder.tsx":"45","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/pricing.tsx":"46","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/stats.tsx":"47","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/testimonials.tsx":"48","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/navigation.tsx":"49","/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/payload/refresh-route-on-save.tsx":"50","/home/gib/Documents/Code/convex-monorepo/apps/next/src/lib/payload/get-landing-page-content.ts":"51","/home/gib/Documents/Code/coLine truncated
|
||||
@@ -1 +1 @@
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56"],{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},{"key":"67","value":"68"},{"key":"69","value":"70"},{"key":"71","value":"72"},{"key":"73","value":"74"},{"key":"75","value":"76"},{"key":"77","value":"78"},{"key":"79","value":"80"},{"key":"81","value":"82"},{"key":"83","value":"84"},{"key":"85","value":"86"},{"key":"87","value":"88"},{"key":"89","value":"90"},{"key":"91","value":"92"},{"key":"93","value":"94"},{"key":"95","value":"96"},{"key":"97","value":"98"},{"key":"99","value":"100"},{"key":"101","value":"102"},{"key":"103","value":"104"},{"key":"105","value":"106"},{"key":"107","value":"108"},{"key":"109","value":"110"},{"key":"111","value":"112"},{"key":"113","value":"114"},{"key":"115","value":"116"},{"key":"117","value":"118"},{"key":"119","value":"120"},{"key":"121","value":"122"},{"key":"123","value":"124"},{"key":"125","value":"126"},{"key":"127","value":"128"},{"key":"129","value":"130"},{"key":"131","value":"132"},{"key":"133","value":"134"},{"key":"135","value":"136"},{"key":"137","value":"138"},{"key":"139","value":"140"},{"key":"141","value":"142"},{"key":"143","value":"144"},{"key":"145","value":"146"},{"key":"147","value":"148"},{"key":"149","value":"150"},{"key":"151","value":"152"},{"key":"153","value":"154"},{"key":"155","value":"156"},{"key":"157","value":"158"},{"key":"159","value":"160"},{"key":"161","value":"162"},{"key":"163","value":"164"},{"key":"165","value":"166"},{"key":"167","value":"168"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-black.svg",{"size":948,"mtime":1717458243000,"hash":"169"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/index.tsx",{"size":499,"mtime":1768372347633,"hash":"170","data":"171"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/gibs-auth.tsx",{"size":1081,"mtime":1768372347496,"hash":"172","data":"173"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-wordmark-black.svg",{"size":3110,"mtime":1717458243000,"hash":"174"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/gitea/gitea.svg",{"size":1844,"mtime":1747415494000,"hash":"175"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/sentry.server.config.ts",{"size":184,"mtime":1774544487955,"hash":"176","data":"177"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/reset-password.tsx",{"size":5681,"mtime":1768372347569,"hash":"178","data":"179"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-color.svg",{"size":948,"mtime":1717458243000,"hash":"180"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/instrumentation-client.ts",{"size":982,"mtime":1774544487814,"hash":"181","data":"182"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/tech-stack.tsx",{"size":2697,"mtime":1774544487253,"hash":"183","data":"184"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/favicon-light.png",{"size":14406,"mtime":1717458300000,"hash":"185"},"/home/gib/Documents/Code/convex-monorepo/apps/next/.cache/tsbuildinfo.json",{"size":679713,"mtime":1774546246715,"hash":"186","data":"187"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/app/global-error.tsx",{"size":2410,"mtime":1774544486967,"hash":"188","data":"189"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/footer/index.tsx",{"size":4010,"mtime":1768372347611,"hash":"190","data":"191"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/header/controls/AvatarDropdown.tsx",{"size":2539,"mtime":1768372347626,"hash":"192","data":"193"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/cta.tsx",{"size":1139,"mtime":1774544487103,"hash":"194","data":"195"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/profile/sign-out.tsx",{"size":1234,"mtime":1768372347581,"hash":"196","data":"197"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/landing/index.tsx",{"size":141,"mtime":1768325738000,"hash":"198","data":"199"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-white.svg",{"size":942,"mtime":1717458243000,"hash":"200"},"/home/gib/Documents/Code/convex-monorepo/apps/next/src/components/layout/auth/buttons/index.tsx",{"size":52,"mtime":1768188510036,"hash":"201","data":"202"},"/home/gib/Documents/Code/convex-monorepo/apps/next/postcss.config.js",{"size":63,"mtime":1766225879000,"hash":"203","data":"204"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convexLine truncated
|
||||
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81","82","83","84","85","86","87","88","89","90","91","92","93","94","95","96","97","98","99","100","101","102","103","104","105","106","107","108","109","110","111","112","113","114","115","116","117","118","119","120","121","122","123","124","125","126","127","128","129","130","131","132","133","134","135","136","137","138","139","140","141","142","143","144","145","146","147"],{"key":"148","value":"149"},{"key":"150","value":"151"},{"key":"152","value":"153"},{"key":"154","value":"155"},{"key":"156","value":"157"},{"key":"158","value":"159"},{"key":"160","value":"161"},{"key":"162","value":"163"},{"key":"164","value":"165"},{"key":"166","value":"167"},{"key":"168","value":"169"},{"key":"170","value":"171"},{"key":"172","value":"173"},{"key":"174","value":"175"},{"key":"176","value":"177"},{"key":"178","value":"179"},{"key":"180","value":"181"},{"key":"182","value":"183"},{"key":"184","value":"185"},{"key":"186","value":"187"},{"key":"188","value":"189"},{"key":"190","value":"191"},{"key":"192","value":"193"},{"key":"194","value":"195"},{"key":"196","value":"197"},{"key":"198","value":"199"},{"key":"200","value":"201"},{"key":"202","value":"203"},{"key":"204","value":"205"},{"key":"206","value":"207"},{"key":"208","value":"209"},{"key":"210","value":"211"},{"key":"212","value":"213"},{"key":"214","value":"215"},{"key":"216","value":"217"},{"key":"218","value":"219"},{"key":"220","value":"221"},{"key":"222","value":"223"},{"key":"224","value":"225"},{"key":"226","value":"227"},{"key":"228","value":"229"},{"key":"230","value":"231"},{"key":"232","value":"233"},{"key":"234","value":"235"},{"key":"236","value":"237"},{"key":"238","value":"239"},{"key":"240","value":"241"},{"key":"242","value":"243"},{"key":"244","value":"245"},{"key":"246","value":"247"},{"key":"248","value":"249"},{"key":"250","value":"251"},{"key":"252","value":"253"},{"key":"254","value":"255"},{"key":"256","value":"257"},{"key":"258","value":"259"},{"key":"260","value":"261"},{"key":"262","value":"263"},{"key":"264","value":"265"},{"key":"266","value":"267"},{"key":"268","value":"269"},{"key":"270","value":"271"},{"key":"272","value":"273"},{"key":"274","value":"275"},{"key":"276","value":"277"},{"key":"278","value":"279"},{"key":"280","value":"281"},{"key":"282","value":"283"},{"key":"284","value":"285"},{"key":"286","value":"287"},{"key":"288","value":"289"},{"key":"290","value":"291"},{"key":"292","value":"293"},{"key":"294","value":"295"},{"key":"296","value":"297"},{"key":"298","value":"299"},{"key":"300","value":"301"},{"key":"302","value":"303"},{"key":"304","value":"305"},{"key":"306","value":"307"},{"key":"308","value":"309"},{"key":"310","value":"311"},{"key":"312","value":"313"},{"key":"314","value":"315"},{"key":"316","value":"317"},{"key":"318","value":"319"},{"key":"320","value":"321"},{"key":"322","value":"323"},{"key":"324","value":"325"},{"key":"326","value":"327"},{"key":"328","value":"329"},{"key":"330","value":"331"},{"key":"332","value":"333"},{"key":"334","value":"335"},{"key":"336","value":"337"},{"key":"338","value":"339"},{"key":"340","value":"341"},{"key":"342","value":"343"},{"key":"344","value":"345"},{"key":"346","value":"347"},{"key":"348","value":"349"},{"key":"350","value":"351"},{"key":"352","value":"353"},{"key":"354","value":"355"},{"key":"356","value":"357"},{"key":"358","value":"359"},{"key":"360","value":"361"},{"key":"362","value":"363"},{"key":"364","value":"365"},{"key":"366","value":"367"},{"key":"368","value":"369"},{"key":"370","value":"371"},{"key":"372","value":"373"},{"key":"374","value":"375"},{"key":"376","value":"377"},{"key":"378","value":"379"},{"key":"380","value":"381"},{"key":"382","value":"383"},{"key":"384","value":"385"},{"key":"386","value":"387"},{"key":"388","value":"389"},{"key":"390","value":"391"},{"key":"392","value":"393"},{"key":"394","value":"395"},{"key":"396","value":"397"},{"key":"398","value":"399"},{"key":"400","value":"401"},{"key":"402","value":"403"},{"key":"404","value":"405"},{"key":"406","value":"407"},{"key":"408","value":"409"},{"key":"410","value":"411"},{"key":"412","value":"413"},{"key":"414","value":"415"},{"key":"416","value":"417"},{"key":"418","value":"419"},{"key":"420","value":"421"},{"key":"422","value":"423"},{"key":"424","value":"425"},{"key":"426","value":"427"},{"key":"428","value":"429"},{"key":"430","value":"431"},{"key":"432","value":"433"},{"key":"434","value":"435"},{"key":"436","value":"437"},{"key":"438","value":"439"},{"key":"440","value":"441"},"/home/gib/Documents/Code/convex-monorepo/apps/next/public/misc/convex/convex-symbol-black.svg",{"size":94Line truncated
|
||||
+1
-18303
File diff suppressed because it is too large.
Load diff
+15
-6
@@ -5,24 +5,28 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "bun with-env next build",
|
||||
"build:env": "bun with-env next build",
|
||||
"build:docker": "next build --webpack",
|
||||
"clean": "git clean -xdf .cache .next .turbo node_modules",
|
||||
"dev": "bun with-env next dev --turbo",
|
||||
"dev:tunnel": "bun with-env next dev --turbo",
|
||||
"dev:web": "bun with-env next dev --webpack",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
||||
"start": "bun with-env next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
"test:unit": "NODE_ENV=test vitest run --project unit",
|
||||
"test:integration": "NODE_ENV=test vitest run --project integration",
|
||||
"test:component": "NODE_ENV=test vitest run --project component",
|
||||
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
|
||||
},
|
||||
"dependencies": {
|
||||
"@convex-dev/auth": "catalog:convex",
|
||||
"@gib/backend": "workspace:*",
|
||||
"@gib/ui": "workspace:*",
|
||||
"@sentry/nextjs": "^10.43.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.10",
|
||||
"@sentry/nextjs": "^10.46.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.11",
|
||||
"convex": "catalog:convex",
|
||||
"next": "^16.1.7",
|
||||
"next": "^16.2.1",
|
||||
"next-plausible": "^3.12.5",
|
||||
"react": "catalog:react19",
|
||||
"react-dom": "catalog:react19",
|
||||
@@ -35,14 +39,19 @@
|
||||
"@gib/prettier-config": "workspace:*",
|
||||
"@gib/tailwind-config": "workspace:*",
|
||||
"@gib/tsconfig": "workspace:*",
|
||||
"@gib/vitest-config": "workspace:*",
|
||||
"@testing-library/react": "catalog:test",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:react19",
|
||||
"@types/react-dom": "catalog:react19",
|
||||
"baseline-browser-mapping": "^2.10.11",
|
||||
"eslint": "catalog:",
|
||||
"jsdom": "catalog:test",
|
||||
"prettier": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:test"
|
||||
},
|
||||
"prettier": "@gib/prettier-config"
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { isAuthenticatedNextjs } from '@convex-dev/auth/nextjs/server';
|
||||
|
||||
export const generateMetadata = (): Metadata => {
|
||||
return {
|
||||
title: 'Profile',
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ProfileLayout = ({
|
||||
const ProfileLayout = async ({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) => {
|
||||
if (!(await isAuthenticatedNextjs())) redirect('/sign-in');
|
||||
return <>{children}</>;
|
||||
};
|
||||
export default ProfileLayout;
|
||||
@@ -159,7 +159,7 @@ const SignIn = () => {
|
||||
};
|
||||
|
||||
const handleVerifyEmail = async (
|
||||
values: z.infer<typeof verifyEmailFormSchema>,
|
||||
_values: z.infer<typeof verifyEmailFormSchema>,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('code', code);
|
||||
@@ -194,7 +194,7 @@ const SignIn = () => {
|
||||
<FormField
|
||||
control={verifyEmailForm.control}
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
render={({ field: _field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-xl'>Code</FormLabel>
|
||||
<FormControl>
|
||||
|
||||
+10
-10
@@ -1,12 +1,12 @@
|
||||
import { CTA, Features, Hero, TechStack } from '@/components/landing';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className='flex min-h-screen flex-col'>
|
||||
<Hero />
|
||||
<Features />
|
||||
<TechStack />
|
||||
<CTA />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
const Home = () => (
|
||||
<main className='flex min-h-screen flex-col'>
|
||||
<Hero />
|
||||
<Features />
|
||||
<TechStack />
|
||||
<CTA />
|
||||
</main>
|
||||
);
|
||||
|
||||
export default Home;
|
||||
@@ -2,7 +2,7 @@
|
||||
@import 'tw-animate-css';
|
||||
@import '@gib/tailwind-config/theme';
|
||||
|
||||
@source '../../../../packages/ui/src/*.{ts,tsx}';
|
||||
@source '../../../../../packages/ui/src/*.{ts,tsx}';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
@custom-variant light (&:where(.light, .light *));
|
||||
@@ -23,7 +23,11 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
html {
|
||||
@apply bg-background;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export const CTA = () => (
|
||||
everything pre-configured.
|
||||
</p>
|
||||
|
||||
{/* Quick Start Command */}
|
||||
<div className='mt-12'>
|
||||
<p className='text-muted-foreground mb-3 text-sm font-medium'>
|
||||
Quick Start
|
||||
|
||||
@@ -60,7 +60,6 @@ const features = [
|
||||
export const Features = () => (
|
||||
<section id='features' className='container mx-auto px-4 py-24'>
|
||||
<div className='mx-auto max-w-6xl'>
|
||||
{/* Section Header */}
|
||||
<div className='mb-16 text-center'>
|
||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
||||
Everything You Need to Ship Fast
|
||||
@@ -71,7 +70,6 @@ export const Features = () => (
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className='grid gap-6 md:grid-cols-2 lg:grid-cols-3'>
|
||||
{features.map((feature) => (
|
||||
<Card key={feature.title} className='border-border/40'>
|
||||
|
||||
@@ -9,16 +9,16 @@ const kanitSans = Kanit({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
});
|
||||
|
||||
const highlights = ['TypeScript', 'Self-Hosted', 'Real-time', 'Auth Included'];
|
||||
|
||||
export const Hero = () => (
|
||||
<section className='container mx-auto px-4 py-24 md:py-32 lg:py-40'>
|
||||
<div className='mx-auto flex max-w-5xl flex-col items-center gap-8 text-center'>
|
||||
{/* Badge */}
|
||||
<div className='border-border/40 bg-muted/50 inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium'>
|
||||
<span className='mr-2'>🚀</span>
|
||||
<span>Production-ready monorepo template</span>
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className='from-foreground to-foreground/70 bg-linear-to-br bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-5xl md:text-6xl lg:text-7xl'>
|
||||
Build Full-Stack Apps with{' '}
|
||||
<span
|
||||
@@ -28,14 +28,12 @@ export const Hero = () => (
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className='text-muted-foreground max-w-2xl text-lg md:text-xl'>
|
||||
A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web
|
||||
and mobile apps faster with shared code, type-safe backend, and complete
|
||||
control over your infrastructure.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className='flex flex-col gap-3 sm:flex-row'>
|
||||
<Button size='lg' variant='outline' asChild>
|
||||
<Link
|
||||
@@ -54,72 +52,25 @@ export const Hero = () => (
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Features Quick List */}
|
||||
<div className='text-muted-foreground mt-8 flex flex-wrap items-center justify-center gap-6 text-sm'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<svg
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>TypeScript</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<svg
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>Self-Hosted</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<svg
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>Real-time</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<svg
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>Auth Included</span>
|
||||
</div>
|
||||
{highlights.map((highlight) => (
|
||||
<div key={highlight} className='flex items-center gap-2'>
|
||||
<svg
|
||||
className='h-5 w-5 text-green-500'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
<span>{highlight}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -29,7 +29,7 @@ const techStack = [
|
||||
technologies: [
|
||||
{ name: 'Turborepo', description: 'High-performance build system' },
|
||||
{ name: 'TypeScript', description: 'Type-safe development' },
|
||||
{ name: 'Bun', description: 'Fast package manager & runtime' },
|
||||
{ name: 'Bun', description: 'Fast package manager and runtime' },
|
||||
{ name: 'ESLint + Prettier', description: 'Code quality tools' },
|
||||
{ name: 'Docker', description: 'Containerized deployment' },
|
||||
],
|
||||
@@ -40,7 +40,6 @@ export const TechStack = () => (
|
||||
<section id='tech-stack' className='border-border/40 bg-muted/30 border-t'>
|
||||
<div className='container mx-auto px-4 py-24'>
|
||||
<div className='mx-auto max-w-6xl'>
|
||||
{/* Section Header */}
|
||||
<div className='mb-16 text-center'>
|
||||
<h2 className='mb-4 text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl'>
|
||||
Modern Tech Stack
|
||||
@@ -51,7 +50,6 @@ export const TechStack = () => (
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tech Stack Grid */}
|
||||
<div className='grid gap-12 md:grid-cols-3'>
|
||||
{techStack.map((stack) => (
|
||||
<div key={stack.category}>
|
||||
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
Input,
|
||||
} from '@gib/ui';
|
||||
|
||||
interface AvatarUploadProps {
|
||||
type AvatarUploadProps = {
|
||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||
}
|
||||
};
|
||||
|
||||
const dataUrlToBlob = async (
|
||||
dataUrl: string,
|
||||
|
||||
@@ -1,17 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Kanit } from 'next/font/google';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Coffee, Server, Wrench } from 'lucide-react';
|
||||
import { useConvexAuth, useQuery } from 'convex/react';
|
||||
import { Coffee, Server, User, Wrench } from 'lucide-react';
|
||||
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
|
||||
import type { NavItem } from './navigation';
|
||||
import { Controls } from './controls';
|
||||
import { DesktopNavigation, MobileNavigation } from './navigation';
|
||||
|
||||
const kanitSans = Kanit({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600', '700'],
|
||||
});
|
||||
|
||||
export default function Header(headerProps: ComponentProps<'header'>) {
|
||||
const Header = (headerProps: ComponentProps<'header'>) => {
|
||||
const { isAuthenticated } = useConvexAuth();
|
||||
const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
href: '/#features',
|
||||
icon: Wrench,
|
||||
label: 'Features',
|
||||
},
|
||||
{
|
||||
href: '/#tech-stack',
|
||||
icon: Server,
|
||||
label: 'Stack',
|
||||
},
|
||||
{
|
||||
href: 'https://git.gbrown.org/gib/convex-monorepo',
|
||||
icon: Coffee,
|
||||
label: 'Repository',
|
||||
external: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (user?.isAdmin) {
|
||||
navItems.push({
|
||||
href: '/admin',
|
||||
icon: User,
|
||||
label: 'Admin',
|
||||
external: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className='border-border/40 bg-background/95 supports-backdrop-filter:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur'
|
||||
@@ -28,45 +65,24 @@ export default function Header(headerProps: ComponentProps<'header'>) {
|
||||
alt='Convex Monorepo'
|
||||
width={50}
|
||||
height={50}
|
||||
className='w-15 invert dark:invert-0'
|
||||
className='w-10 invert lg:w-15 dark:invert-0'
|
||||
/>
|
||||
<span
|
||||
className={`mb-3 hidden font-extrabold lg:inline lg:text-5xl ${kanitSans.className}`}
|
||||
className={`hidden font-extrabold sm:mb-1 sm:text-lg md:inline lg:mb-3 lg:text-4xl xl:text-5xl ${kanitSans.className}`}
|
||||
>
|
||||
convex monorepo
|
||||
convex-monorepo
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className='hidden items-center gap-6 text-base font-medium md:flex'>
|
||||
<Link
|
||||
href='/#features'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Wrench width={18} height={18} />
|
||||
Features
|
||||
</Link>
|
||||
<Link
|
||||
href='/#tech-stack'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Server width={18} height={18} />
|
||||
Stack
|
||||
</Link>
|
||||
<Link
|
||||
href='https://git.gbrown.org/gib/convex-monorepo'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Coffee width={20} height={20} />
|
||||
Repository
|
||||
</Link>
|
||||
</nav>
|
||||
<DesktopNavigation items={navItems} />
|
||||
|
||||
{/* Controls (Theme + Auth) */}
|
||||
<Controls />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Controls />
|
||||
<MobileNavigation items={navItems} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { ExternalLink, Menu } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@gib/ui';
|
||||
|
||||
export type NavItem = {
|
||||
href: string;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
type NavigationProps = {
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
const DesktopNavigation = ({ items }: NavigationProps) => {
|
||||
return (
|
||||
<nav className='hidden items-center gap-4 text-xs font-medium sm:flex md:gap-6 lg:text-base'>
|
||||
{items.map(({ href, icon: Icon, label, external }) => (
|
||||
<Link
|
||||
key={label}
|
||||
href={href}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopener noreferrer' : undefined}
|
||||
className='text-foreground/60 hover:text-foreground flex items-center gap-2 transition-colors'
|
||||
>
|
||||
<Icon width={18} height={18} />
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavigation = ({ items }: NavigationProps) => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='icon-sm'
|
||||
className='sm:hidden'
|
||||
aria-label='Open navigation menu'
|
||||
>
|
||||
<Menu className='size-4.5' />
|
||||
<span className='sr-only'>Open navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='right' className='w-[min(88vw,22rem)] px-0'>
|
||||
<SheetHeader className='border-border/60 from-background to-muted/40 border-b bg-linear-to-br from-35% px-5 py-5 text-left'>
|
||||
<SheetTitle className='text-left text-lg'>Navigation</SheetTitle>
|
||||
<SheetDescription className='text-left'>
|
||||
Quick access to the links that collapse out of the header.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className='flex flex-col gap-3 px-4 py-5'>
|
||||
{items.map(({ href, icon: Icon, label, external }) => (
|
||||
<SheetClose asChild key={label}>
|
||||
<Link
|
||||
href={href}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopener noreferrer' : undefined}
|
||||
className='bg-card hover:bg-muted/70 border-border/60 text-card-foreground flex items-center justify-between rounded-2xl border px-4 py-3 transition-colors'
|
||||
>
|
||||
<span className='flex items-center gap-3'>
|
||||
<span className='bg-muted text-foreground flex h-9 w-9 items-center justify-center rounded-xl'>
|
||||
<Icon className='size-4.5' />
|
||||
</span>
|
||||
<span className='text-sm font-medium'>{label}</span>
|
||||
</span>
|
||||
{external ? (
|
||||
<ExternalLink className='text-muted-foreground size-4' />
|
||||
) : null}
|
||||
</Link>
|
||||
</SheetClose>
|
||||
))}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export { DesktopNavigation, MobileNavigation };
|
||||
@@ -19,7 +19,7 @@ Sentry.init({
|
||||
tracesSampleRate: 1,
|
||||
enableLogs: true,
|
||||
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
|
||||
replaysSessionSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.5,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
debug: false,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ import { NextResponse } from 'next/server';
|
||||
// In-memory stores for tracking IPs (use Redis in production)
|
||||
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||
const bannedIPs = new Set<string>();
|
||||
// Map of ip -> ban expiry timestamp. Avoids setTimeout closures leaking on hot reload.
|
||||
const bannedIPs = new Map<string, number>();
|
||||
|
||||
// Suspicious patterns that indicate malicious activity
|
||||
const MALICIOUS_PATTERNS = [
|
||||
@@ -72,6 +73,36 @@ const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
|
||||
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
|
||||
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
|
||||
|
||||
let lastCleanup = Date.now();
|
||||
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Lazily purge stale entries so Maps don't grow without bound.
|
||||
// Called on every request but only iterates Maps every CLEANUP_INTERVAL.
|
||||
const cleanupStaleMaps = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastCleanup < CLEANUP_INTERVAL) return;
|
||||
lastCleanup = now;
|
||||
for (const [ip, data] of ipAttempts.entries()) {
|
||||
if (now - data.lastAttempt > RATE_LIMIT_WINDOW) ipAttempts.delete(ip);
|
||||
}
|
||||
for (const [ip, data] of ip404Attempts.entries()) {
|
||||
if (now - data.lastAttempt > RATE_404_WINDOW) ip404Attempts.delete(ip);
|
||||
}
|
||||
for (const [ip, expiry] of bannedIPs.entries()) {
|
||||
if (now > expiry) bannedIPs.delete(ip);
|
||||
}
|
||||
};
|
||||
|
||||
const isIPBanned = (ip: string): boolean => {
|
||||
const expiry = bannedIPs.get(ip);
|
||||
if (expiry === undefined) return false;
|
||||
if (Date.now() > expiry) {
|
||||
bannedIPs.delete(ip);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const getClientIP = (request: NextRequest): string => {
|
||||
const forwarded = request.headers.get('x-forwarded-for');
|
||||
const realIP = request.headers.get('x-real-ip');
|
||||
@@ -104,13 +135,8 @@ const updateIPAttempts = (ip: string): boolean => {
|
||||
attempts.lastAttempt = now;
|
||||
|
||||
if (attempts.count > MAX_ATTEMPTS) {
|
||||
bannedIPs.add(ip);
|
||||
bannedIPs.set(ip, Date.now() + BAN_DURATION);
|
||||
ipAttempts.delete(ip);
|
||||
|
||||
setTimeout(() => {
|
||||
bannedIPs.delete(ip);
|
||||
}, BAN_DURATION);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -130,17 +156,13 @@ const update404Attempts = (ip: string): boolean => {
|
||||
attempts.lastAttempt = now;
|
||||
|
||||
if (attempts.count > MAX_404_ATTEMPTS) {
|
||||
bannedIPs.add(ip);
|
||||
bannedIPs.set(ip, Date.now() + BAN_DURATION);
|
||||
ip404Attempts.delete(ip);
|
||||
|
||||
console.log(
|
||||
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
bannedIPs.delete(ip);
|
||||
}, BAN_DURATION);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -148,12 +170,14 @@ const update404Attempts = (ip: string): boolean => {
|
||||
};
|
||||
|
||||
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
||||
cleanupStaleMaps();
|
||||
|
||||
const { pathname } = request.nextUrl;
|
||||
const method = request.method;
|
||||
const ip = getClientIP(request);
|
||||
|
||||
// Check if IP is already banned
|
||||
if (bannedIPs.has(ip)) {
|
||||
if (isIPBanned(ip)) {
|
||||
return new NextResponse('Access denied.', { status: 403 });
|
||||
}
|
||||
|
||||
@@ -183,7 +207,7 @@ export const handle404Response = (
|
||||
): NextResponse | null => {
|
||||
const ip = getClientIP(request);
|
||||
|
||||
if (bannedIPs.has(ip)) {
|
||||
if (isIPBanned(ip)) {
|
||||
return new NextResponse('Access denied.', { status: 403 });
|
||||
}
|
||||
|
||||
|
||||
+21
-5
@@ -1,19 +1,36 @@
|
||||
import { convexAuthNextjsMiddleware } from '@convex-dev/auth/nextjs/server';
|
||||
|
||||
export default convexAuthNextjsMiddleware();
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
'/((?!.*\\..*|_next).*)',
|
||||
'/',
|
||||
'/(api)(.*)',
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
import { banSuspiciousIPs } from '@/lib/proxy/ban-sus-ips';
|
||||
import {
|
||||
convexAuthNextjsMiddleware,
|
||||
createRouteMatcher,
|
||||
nextjsMiddlewareRedirect,
|
||||
} from '@convex-dev/auth/nextjs/server';
|
||||
|
||||
const isSignInPage = createRouteMatcher(['/sign-in']);
|
||||
const isProtectedRoute = createRouteMatcher(['/profile']);
|
||||
const isProtectedRoute = createRouteMatcher([
|
||||
'/profile(.*)',
|
||||
'/dashboard(.*)',
|
||||
'/admin-panel(.*)',
|
||||
]);
|
||||
|
||||
export default convexAuthNextjsMiddleware(
|
||||
async (request, { convexAuth }) => {
|
||||
const banResponse = banSuspiciousIPs(request);
|
||||
if (banResponse) return banResponse;
|
||||
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
|
||||
return nextjsMiddlewareRedirect(request, '/');
|
||||
return nextjsMiddlewareRedirect(request, '/profile');
|
||||
}
|
||||
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
|
||||
return nextjsMiddlewareRedirect(request, '/sign-in');
|
||||
@@ -23,8 +40,6 @@ export default convexAuthNextjsMiddleware(
|
||||
);
|
||||
|
||||
export const config = {
|
||||
// The following matcher runs middleware on all routes
|
||||
// except static assets.
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
'/((?!.*\\..*|_next).*)',
|
||||
@@ -32,3 +47,4 @@ export const config = {
|
||||
'/(api)(.*)',
|
||||
],
|
||||
};
|
||||
**/
|
||||
@@ -0,0 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const Greeting = ({ name }: { name: string }) => <p>Hello {name}</p>;
|
||||
|
||||
describe('component test harness', () => {
|
||||
it('renders through jsdom and Testing Library', () => {
|
||||
render(<Greeting name='world' />);
|
||||
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('integration test harness', () => {
|
||||
it('provides modern Node globals', () => {
|
||||
expect(typeof globalThis.structuredClone).toBe('function');
|
||||
});
|
||||
});
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('unit test harness', () => {
|
||||
it('executes isolated assertions', () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { jsdomProject, nodeProject } from '@gib/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
|
||||
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
|
||||
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
|
||||
],
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user