* auth

* Add default user

* setup auth

* add auth to all endpoints

* Add url logging

* Add tomato.gg

* Add analytics

* Add correct url

* fix auth

* log url

* change base url

* replace api

* wip

* Test

* test changes

* bump

* Add trusted origin

* f

* i almost give up

* stop using middleware

* Fix auth

* Fix build
This commit is contained in:
Bill Yang 2025-02-15 15:55:28 -05:00 committed by GitHub
parent 0871ed0ef7
commit 0dc058749d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1515 additions and 128 deletions

398
client/package-lock.json generated
View file

@ -14,9 +14,10 @@
"@phosphor-icons/react": "^2.1.7", "@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-select": "^2.1.5", "@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.64.2", "@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2", "@tanstack/react-query-devtools": "^5.64.2",
@ -25,6 +26,7 @@
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/pg": "^8.11.11", "@types/pg": "^8.11.11",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"better-auth": "^1.1.16",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"countries-list": "^3.1.1", "countries-list": "^3.1.1",
@ -88,6 +90,19 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@better-auth/utils": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.3.tgz",
"integrity": "sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==",
"dependencies": {
"uncrypto": "^0.1.3"
}
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.12.tgz",
"integrity": "sha512-B3bfloI/2UBQWIATRN6qmlORrvx3Mp0kkNjmXLv0b+DtbtR+pP4/I5kQA/rDUv+OReLywCCldf6co4LdDmh8JA=="
},
"node_modules/@clickhouse/client": { "node_modules/@clickhouse/client": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.10.1.tgz", "resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.10.1.tgz",
@ -932,6 +947,11 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
}, },
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="
},
"node_modules/@img/sharp-darwin-arm64": { "node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5", "version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@ -1339,6 +1359,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@levischuck/tiny-cbor": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.1.6", "version": "15.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz",
@ -1622,6 +1647,25 @@
"react": ">= 16.14.0 < 19.0.0" "react": ">= 16.14.0 < 19.0.0"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz",
"integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1657,6 +1701,59 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@peculiar/asn1-android": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.15.tgz",
"integrity": "sha512-8U2TIj59cRlSXTX2d0mzUKP7whfWGFMzTeC3qPgAbccXFrPNZLaDhpNEdG5U2QZ/tBv/IHlCJ8s+KYXpJeop6w==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz",
"integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz",
"integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz",
"integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==",
"dependencies": {
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz",
"integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@phosphor-icons/react": { "node_modules/@phosphor-icons/react": {
"version": "2.1.7", "version": "2.1.7",
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz", "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz",
@ -1736,6 +1833,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": { "node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
@ -1799,6 +1913,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": { "node_modules/@radix-ui/react-direction": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
@ -1922,6 +2053,50 @@
} }
} }
}, },
"node_modules/@radix-ui/react-label": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz",
"integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu": { "node_modules/@radix-ui/react-menu": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.5.tgz",
@ -1961,6 +2136,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": { "node_modules/@radix-ui/react-popover": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.5.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.5.tgz",
@ -1997,6 +2189,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
@ -2096,6 +2305,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
@ -2168,7 +2394,7 @@
} }
} }
}, },
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
@ -2185,6 +2411,23 @@
} }
} }
}, },
"node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
@ -2342,23 +2585,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
@ -2594,6 +2820,28 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/@simplewebauthn/browser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz",
"integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="
},
"node_modules/@simplewebauthn/server": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.1.tgz",
"integrity": "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==",
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
"@peculiar/asn1-android": "^2.3.10",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/counter": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@ -2925,12 +3173,55 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/asn1js": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
"dependencies": {
"pvtsutils": "^1.3.2",
"pvutils": "^1.1.3",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/better-auth": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.1.16.tgz",
"integrity": "sha512-Xc5pxafKZw4QVU8WYfkV2z4Hd8KCXXbphrgOpe2gA/EfanysLBhE1G/F7cEi5e0bW2pGR+vw6gf0ARHA7VFihg==",
"dependencies": {
"@better-auth/utils": "0.2.3",
"@better-fetch/fetch": "1.1.12",
"@noble/ciphers": "^0.6.0",
"@noble/hashes": "^1.6.1",
"@simplewebauthn/browser": "^13.0.0",
"@simplewebauthn/server": "^13.0.0",
"better-call": "0.3.3",
"defu": "^6.1.4",
"jose": "^5.9.6",
"kysely": "^0.27.4",
"nanostores": "^0.11.3",
"zod": "^3.24.1"
}
},
"node_modules/better-call": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-0.3.3.tgz",
"integrity": "sha512-N4lDVm0NGmFfDJ0XMQ4O83Zm/3dPlvIQdxvwvgSLSkjFX5PM4GUYSVAuxNzXN27QZMHDkrJTWUqxBrm4tPC3eA==",
"dependencies": {
"@better-fetch/fetch": "^1.1.4",
"rou3": "^0.5.1",
"uncrypto": "^0.1.3",
"zod": "^3.24.1"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -3431,6 +3722,11 @@
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
}, },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/delaunator": { "node_modules/delaunator": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
@ -3958,11 +4254,27 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/jose": {
"version": "5.9.6",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz",
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
}, },
"node_modules/kysely": {
"version": "0.27.5",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.5.tgz",
"integrity": "sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ldrs": { "node_modules/ldrs": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldrs/-/ldrs-1.0.2.tgz", "resolved": "https://registry.npmjs.org/ldrs/-/ldrs-1.0.2.tgz",
@ -4109,6 +4421,20 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/nanostores": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.3.tgz",
"integrity": "sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/next": { "node_modules/next": {
"version": "15.1.6", "version": "15.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz",
@ -4595,6 +4921,22 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -4892,6 +5234,11 @@
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
}, },
"node_modules/rou3": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz",
"integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -5354,6 +5701,11 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@ -5569,6 +5921,14 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zustand": { "node_modules/zustand": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",

View file

@ -15,9 +15,10 @@
"@phosphor-icons/react": "^2.1.7", "@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-select": "^2.1.5", "@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.64.2", "@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2", "@tanstack/react-query-devtools": "^5.64.2",
@ -26,6 +27,7 @@
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/pg": "^8.11.11", "@types/pg": "^8.11.11",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"better-auth": "^1.1.16",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"countries-list": "^3.1.1", "countries-list": "^3.1.1",

View file

@ -33,9 +33,7 @@ export function DateSelector() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>{getLabel(time)}</DropdownMenuTrigger>
<Button variant="default">{getLabel(time)}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>

View file

@ -12,6 +12,7 @@ import {
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { DateSelector } from "./DateSelector"; import { DateSelector } from "./DateSelector";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { authedFetch } from "../../../hooks/utils";
const canGoForward = (time: Time) => { const canGoForward = (time: Time) => {
const currentDay = DateTime.now().startOf("day"); const currentDay = DateTime.now().startOf("day");
@ -42,9 +43,7 @@ export function Header() {
const { data } = useQuery<{ count: number }>({ const { data } = useQuery<{ count: number }>({
queryKey: ["active-sessions"], queryKey: ["active-sessions"],
queryFn: () => queryFn: () =>
fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/live-user-count`).then( authedFetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/live-user-count`),
(res) => res.json()
),
}); });
const { time } = useTimeSelection(); const { time } = useTimeSelection();

View file

@ -1,3 +1,5 @@
"use client";
import { Duration } from "luxon"; import { Duration } from "luxon";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Skeleton } from "../../../components/ui/skeleton"; import { Skeleton } from "../../../components/ui/skeleton";

View file

@ -1,22 +1,40 @@
"use client";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import QueryProvider from "@/providers/QueryProvider"; import QueryProvider from "@/providers/QueryProvider";
import { ThemeProvider } from "@/providers/ThemeProvider"; import { ThemeProvider } from "@/providers/ThemeProvider";
import { TopBar } from "@/components/TopBar"; import { TopBar } from "@/components/TopBar";
import { authClient } from "../lib/auth";
import { redirect } from "next/navigation";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { userStore } from "../lib/useStore";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { const metadata: Metadata = {
title: "Frogstats Analytics", title: "Frogstats Analytics",
description: "Analytics dashboard for your web applications", description: "Analytics dashboard for your web applications",
}; };
const publicRoutes = ["/login"];
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const { user, isPending } = userStore();
const pathname = usePathname();
useEffect(() => {
if (!isPending && !user && !publicRoutes.includes(pathname)) {
redirect("/login");
}
}, [isPending, user, pathname]);
return ( return (
<html lang="en" className="h-full dark" suppressHydrationWarning> <html lang="en" className="h-full dark" suppressHydrationWarning>
<body <body

View file

@ -0,0 +1,98 @@
"use client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authClient } from "../../lib/auth";
import { userStore } from "../../lib/useStore";
export default function Page() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const { data, error } = await authClient.signIn.username({
username,
password,
});
if (data?.user) {
userStore.setState({
user: data.user,
});
router.push("/");
}
} catch (error) {
console.error("Login error:", error);
alert("Failed to login. Please try again.");
} finally {
setIsLoading(false);
}
};
return (
<div className={cn("flex flex-col gap-6 items-center")}>
<Card className="w-[500px]">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
type="username"
placeholder="username"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="#"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Logging in..." : "Login"}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View file

@ -1,4 +1,13 @@
"use client";
import { Button } from "@/components/ui/button";
import { authClient } from "../../lib/auth";
import { useRouter } from "next/navigation";
export default function SettingsPage() { export default function SettingsPage() {
const session = authClient.useSession();
const router = useRouter();
return ( return (
<div className="container max-w-6xl py-6"> <div className="container max-w-6xl py-6">
<div className="space-y-6"> <div className="space-y-6">
@ -8,16 +17,14 @@ export default function SettingsPage() {
Manage your analytics preferences and configurations. Manage your analytics preferences and configurations.
</p> </p>
</div> </div>
<div className="border rounded-lg p-4"> <Button
<div className="space-y-4"> onClick={async () => {
<div> await authClient.signOut();
<h4 className="text-sm font-medium">Coming Soon</h4> router.push("/login");
<p className="text-sm text-muted-foreground"> }}
Settings configuration options will be available here. >
</p> Signout
</div> </Button>
</div>
</div>
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,64 @@
"use client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function Page() {
return (
<div className={cn("flex flex-col gap-6")}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
type="username"
placeholder="username"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="#"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Login
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{""}
<a href="/signup" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,68 @@
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function LoginForm({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="#"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Login
</Button>
<Button variant="outline" className="w-full">
Login with Google
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{""}
<a href="#" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}

View file

@ -1,3 +1,5 @@
"use client";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useMeasure } from "@uidotdev/usehooks"; import { useMeasure } from "@uidotdev/usehooks";
@ -27,7 +29,7 @@ const CardLoader = React.forwardRef<
<div ref={ref2} className="mt-[-15px] absolute top-0 left-0 w-full"> <div ref={ref2} className="mt-[-15px] absolute top-0 left-0 w-full">
{/* @ts-ignore */} {/* @ts-ignore */}
<l-zoomies <l-zoomies
size={width} size={1000}
stroke="3" stroke="3"
bg-opacity="0.1" bg-opacity="0.1"
speed="1.4" speed="1.4"

View file

@ -5,10 +5,23 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react"; import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "./button";
const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; const DropdownMenuTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger> &
ButtonProps
>(({ className, variant, size, ...props }, ref) => (
<DropdownMenuPrimitive.Trigger
ref={ref}
// className={cn(className)}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
));
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
const DropdownMenuGroup = DropdownMenuPrimitive.Group; const DropdownMenuGroup = DropdownMenuPrimitive.Group;

View file

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-neutral-200 bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-neutral-800 dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View file

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -4,7 +4,7 @@ import {
UseQueryResult, UseQueryResult,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { useTimeSelection } from "../lib/timeSelectionStore"; import { useTimeSelection } from "../lib/timeSelectionStore";
import { getStartAndEndDate } from "./utils"; import { authedFetch, getStartAndEndDate } from "./utils";
export type APIResponse<T> = { export type APIResponse<T> = {
data: T; data: T;
@ -25,9 +25,9 @@ export function useGenericQuery<T>(
queryKey: [endpoint, timeToUse], queryKey: [endpoint, timeToUse],
queryFn: () => { queryFn: () => {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return fetch( return authedFetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/${endpoint}?startDate=${startDate}&endDate=${endDate}&timezone=${timezone}` `${process.env.NEXT_PUBLIC_BACKEND_URL}/${endpoint}?startDate=${startDate}&endDate=${endDate}&timezone=${timezone}`
).then((res) => res.json()); );
}, },
staleTime: Infinity, staleTime: Infinity,
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
@ -111,9 +111,9 @@ export function useGetPageviews(
queryKey: ["pageviews", timeToUse, bucket], queryKey: ["pageviews", timeToUse, bucket],
queryFn: () => { queryFn: () => {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return fetch( return authedFetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/pageviews?startDate=${startDate}&endDate=${endDate}&timezone=${timezone}&bucket=${bucket}` `${process.env.NEXT_PUBLIC_BACKEND_URL}/pageviews?startDate=${startDate}&endDate=${endDate}&timezone=${timezone}&bucket=${bucket}`
).then((res) => res.json()); );
}, },
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
staleTime: Infinity, staleTime: Infinity,

View file

@ -15,3 +15,13 @@ export function getStartAndEndDate(time: Time) {
} }
return { startDate: time.day, endDate: time.day }; return { startDate: time.day, endDate: time.day };
} }
export async function authedFetch(url: string) {
return fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
}

11
client/src/lib/auth.ts Normal file
View file

@ -0,0 +1,11 @@
import { createAuthClient } from "better-auth/react";
import { usernameClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
// baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL?.replace("/api", ""), // the base url of your auth server
plugins: [usernameClient()],
fetchOptions: {
credentials: "include",
},
});

View file

@ -0,0 +1,22 @@
import { User } from "better-auth";
import { create } from "zustand";
import { authClient } from "./auth";
export const userStore = create<{
user: User | null;
isPending: boolean;
setSession: (user: User) => void;
setIsPending: (isPending: boolean) => void;
}>((set) => ({
user: null,
isPending: true,
setSession: (user) => set({ user }),
setIsPending: (isPending) => set({ isPending }),
}));
authClient.getSession().then(({ data: session }) => {
userStore.setState({
user: session?.user,
isPending: false,
});
});

View file

@ -41,6 +41,7 @@ services:
- POSTGRES_DB=analytics - POSTGRES_DB=analytics
- POSTGRES_USER=frog - POSTGRES_USER=frog
- POSTGRES_PASSWORD=admin - POSTGRES_PASSWORD=admin
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
depends_on: depends_on:
- clickhouse - clickhouse
- postgres - postgres
@ -59,9 +60,13 @@ services:
- "3002:3002" - "3002:3002"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- CLICKHOUSE_HOST=http://clickhouse:8123 - POSTGRES_HOST=postgres
- CLICKHOUSE_DB=analytics - POSTGRES_PORT=5432
- POSTGRES_DB=analytics
- POSTGRES_USER=frog
- POSTGRES_PASSWORD=admin
- NEXT_PUBLIC_BACKEND_URL=${BASE_URL} - NEXT_PUBLIC_BACKEND_URL=${BASE_URL}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
depends_on: depends_on:
- backend - backend

View file

@ -0,0 +1,7 @@
create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" boolean not null, "image" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
create table "session" ("id" text not null primary key, "expiresAt" timestamp not null, "token" text not null unique, "createdAt" timestamp not null, "updatedAt" timestamp not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id"));
create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" timestamp, "refreshTokenExpiresAt" timestamp, "scope" text, "password" text, "createdAt" timestamp not null, "updatedAt" timestamp not null);
create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" timestamp not null, "createdAt" timestamp, "updatedAt" timestamp)

465
server/package-lock.json generated
View file

@ -12,12 +12,17 @@
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",
"@fastify/one-line-logger": "^1.4.0", "@fastify/one-line-logger": "^1.4.0",
"@fastify/static": "^8.0.4", "@fastify/static": "^8.0.4",
"@types/pg": "^8.11.11",
"better-auth": "^1.1.16",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"fastify": "^5.1.0", "fastify": "^5.1.0",
"fastify-better-auth": "^1.0.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pg": "^8.13.1",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"ua-parser-js": "^2.0.0" "ua-parser-js": "^2.0.0",
"undici": "^7.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
@ -27,6 +32,19 @@
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }
}, },
"node_modules/@better-auth/utils": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.3.tgz",
"integrity": "sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==",
"dependencies": {
"uncrypto": "^0.1.3"
}
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.12.tgz",
"integrity": "sha512-B3bfloI/2UBQWIATRN6qmlORrvx3Mp0kkNjmXLv0b+DtbtR+pP4/I5kQA/rDUv+OReLywCCldf6co4LdDmh8JA=="
},
"node_modules/@clickhouse/client": { "node_modules/@clickhouse/client": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.10.1.tgz", "resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.10.1.tgz",
@ -217,6 +235,11 @@
"glob": "^11.0.0" "glob": "^11.0.0"
} }
}, },
"node_modules/@hexagon/base64": {
"version": "1.1.28",
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -258,6 +281,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@levischuck/tiny-cbor": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="
},
"node_modules/@lukeed/ms": { "node_modules/@lukeed/ms": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
@ -266,6 +294,100 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz",
"integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@peculiar/asn1-android": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.15.tgz",
"integrity": "sha512-8U2TIj59cRlSXTX2d0mzUKP7whfWGFMzTeC3qPgAbccXFrPNZLaDhpNEdG5U2QZ/tBv/IHlCJ8s+KYXpJeop6w==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz",
"integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz",
"integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"@peculiar/asn1-x509": "^2.3.15",
"asn1js": "^3.0.5",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz",
"integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==",
"dependencies": {
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.3.15",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz",
"integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.15",
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@simplewebauthn/browser": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz",
"integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="
},
"node_modules/@simplewebauthn/server": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.1.tgz",
"integrity": "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==",
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
"@peculiar/asn1-android": "^2.3.10",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-rsa": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@ -300,7 +422,6 @@
"version": "20.10.0", "version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -311,6 +432,68 @@
"integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
"dev": true "dev": true
}, },
"node_modules/@types/pg": {
"version": "8.11.11",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz",
"integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^4.0.1"
}
},
"node_modules/@types/pg/node_modules/pg-types": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
"dependencies": {
"pg-int8": "1.0.1",
"pg-numeric": "1.0.2",
"postgres-array": "~3.0.1",
"postgres-bytea": "~3.0.0",
"postgres-date": "~2.1.0",
"postgres-interval": "^3.0.0",
"postgres-range": "^1.1.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/pg/node_modules/postgres-array": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pg/node_modules/postgres-bytea": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"dependencies": {
"obuf": "~1.1.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/pg/node_modules/postgres-date": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
"engines": {
"node": ">=12"
}
},
"node_modules/@types/pg/node_modules/postgres-interval": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
"engines": {
"node": ">=12"
}
},
"node_modules/@types/strip-bom": { "node_modules/@types/strip-bom": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
@ -435,6 +618,19 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true "dev": true
}, },
"node_modules/asn1js": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
"integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==",
"dependencies": {
"pvtsutils": "^1.3.2",
"pvutils": "^1.1.3",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/atomic-sleep": { "node_modules/atomic-sleep": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@ -476,6 +672,36 @@
} }
] ]
}, },
"node_modules/better-auth": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.1.16.tgz",
"integrity": "sha512-Xc5pxafKZw4QVU8WYfkV2z4Hd8KCXXbphrgOpe2gA/EfanysLBhE1G/F7cEi5e0bW2pGR+vw6gf0ARHA7VFihg==",
"dependencies": {
"@better-auth/utils": "0.2.3",
"@better-fetch/fetch": "1.1.12",
"@noble/ciphers": "^0.6.0",
"@noble/hashes": "^1.6.1",
"@simplewebauthn/browser": "^13.0.0",
"@simplewebauthn/server": "^13.0.0",
"better-call": "0.3.3",
"defu": "^6.1.4",
"jose": "^5.9.6",
"kysely": "^0.27.4",
"nanostores": "^0.11.3",
"zod": "^3.24.1"
}
},
"node_modules/better-call": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/better-call/-/better-call-0.3.3.tgz",
"integrity": "sha512-N4lDVm0NGmFfDJ0XMQ4O83Zm/3dPlvIQdxvwvgSLSkjFX5PM4GUYSVAuxNzXN27QZMHDkrJTWUqxBrm4tPC3eA==",
"dependencies": {
"@better-fetch/fetch": "^1.1.4",
"rou3": "^0.5.1",
"uncrypto": "^0.1.3",
"zod": "^3.24.1"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -634,6 +860,11 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -843,6 +1074,21 @@
"toad-cache": "^3.7.0" "toad-cache": "^3.7.0"
} }
}, },
"node_modules/fastify-better-auth": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fastify-better-auth/-/fastify-better-auth-1.0.0.tgz",
"integrity": "sha512-3sSPlcwOVp9tZAQaniu7LeAX8U237jBjHoPs5cAVMBRCdSHRveb20dGf762mtFSxUygMYciAd431sRpRuJFc1Q==",
"dependencies": {
"fastify-plugin": "^5.0.1"
},
"engines": {
"node": ">= 22"
},
"peerDependencies": {
"better-auth": "1.x",
"fastify": "5.x"
}
},
"node_modules/fastify-plugin": { "node_modules/fastify-plugin": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz",
@ -1137,6 +1383,14 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/jose": {
"version": "5.9.6",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz",
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/joycon": { "node_modules/joycon": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@ -1168,6 +1422,14 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}, },
"node_modules/kysely": {
"version": "0.27.5",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.5.tgz",
"integrity": "sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/light-my-request": { "node_modules/light-my-request": {
"version": "6.5.1", "version": "6.5.1",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.5.1.tgz", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.5.1.tgz",
@ -1271,6 +1533,20 @@
"obliterator": "^2.0.1" "obliterator": "^2.0.1"
} }
}, },
"node_modules/nanostores": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.3.tgz",
"integrity": "sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/node-cron": { "node_modules/node-cron": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
@ -1296,6 +1572,11 @@
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
"integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==" "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="
}, },
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
},
"node_modules/on-exit-leak-free": { "node_modules/on-exit-leak-free": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
@ -1355,6 +1636,95 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/pg": {
"version": "8.13.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.0",
"pg-protocol": "^1.7.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-numeric": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
"engines": {
"node": ">=4"
}
},
"node_modules/pg-pool": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -1453,6 +1823,46 @@
"url": "https://github.com/sponsors/porsager" "url": "https://github.com/sponsors/porsager"
} }
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-range": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="
},
"node_modules/process": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -1485,6 +1895,22 @@
"once": "^1.3.1" "once": "^1.3.1"
} }
}, },
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz",
"integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/quick-format-unescaped": { "node_modules/quick-format-unescaped": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
@ -1631,6 +2057,11 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/rou3": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz",
"integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -2045,6 +2476,11 @@
"strip-json-comments": "^2.0.0" "strip-json-comments": "^2.0.0"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
@ -2107,11 +2543,23 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="
},
"node_modules/undici": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.3.0.tgz",
"integrity": "sha512-Qy96NND4Dou5jKoSJ2gm8ax8AJM/Ey9o9mz7KN1bb9GP+G0l20Zw8afxTnY2f4b7hmhn/z8aC2kfArVQlAhFBw==",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"dev": true
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
@ -2234,7 +2682,6 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.4" "node": ">=0.4"
} }
@ -2247,6 +2694,14 @@
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View file

@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Self-hosted analytics backend using ClickHouse", "description": "Self-hosted analytics backend using ClickHouse",
"main": "dist/index.js", "main": "dist/index.js",
"type": "module",
"scripts": { "scripts": {
"dev": "tsc && node dist/index.js", "dev": "tsc && node dist/index.js",
"build": "tsc", "build": "tsc",
@ -13,12 +14,17 @@
"@fastify/cors": "^10.0.2", "@fastify/cors": "^10.0.2",
"@fastify/one-line-logger": "^1.4.0", "@fastify/one-line-logger": "^1.4.0",
"@fastify/static": "^8.0.4", "@fastify/static": "^8.0.4",
"@types/pg": "^8.11.11",
"better-auth": "^1.1.16",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"fastify": "^5.1.0", "fastify": "^5.1.0",
"fastify-better-auth": "^1.0.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pg": "^8.13.1",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"ua-parser-js": "^2.0.0" "ua-parser-js": "^2.0.0",
"undici": "^7.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",

View file

@ -1,7 +1,7 @@
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { TrackingPayload } from "../types"; import { TrackingPayload } from "../types.js";
import { getDeviceType } from "../utils"; import { getDeviceType } from "../utils.js";
type TotalPayload = TrackingPayload & { type TotalPayload = TrackingPayload & {
userId: string; userId: string;

View file

@ -1,12 +1,12 @@
import { FastifyRequest } from "fastify"; import { FastifyRequest } from "fastify";
import { TrackingPayload } from "../types"; import { TrackingPayload } from "../types.js";
import { getUserId, getDeviceType, getIpAddress } from "../utils"; import { getUserId, getDeviceType, getIpAddress } from "../utils.js";
import crypto from "crypto"; import crypto from "crypto";
import { sql } from "../db/postgres/postgres"; import { sql } from "../db/postgres/postgres.js";
import UAParser, { UAParser as userAgentParser } from "ua-parser-js"; import UAParser, { UAParser as userAgentParser } from "ua-parser-js";
import { Pageview } from "../db/clickhouse/types"; import { Pageview } from "../db/clickhouse/types.js";
import { pageviewQueue } from "./pageviewQueue"; import { pageviewQueue } from "./pageviewQueue.js";
type TotalPayload = TrackingPayload & { type TotalPayload = TrackingPayload & {
userId: string; userId: string;

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
type GetBrowsersResponse = { type GetBrowsersResponse = {
browser: string; browser: string;

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
type GetCountriesResponse = { type GetCountriesResponse = {
country: string; country: string;

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
type GetDevicesResponse = { type GetDevicesResponse = {
device_type: string; device_type: string;

View file

@ -1,4 +1,4 @@
import { sql } from "../db/postgres/postgres"; import { sql } from "../db/postgres/postgres.js";
export const getLiveUsercount = async () => { export const getLiveUsercount = async () => {
const result = await sql`SELECT COUNT(*) FROM active_sessions`; const result = await sql`SELECT COUNT(*) FROM active_sessions`;

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
type GetOperatingSystemsResponse = { type GetOperatingSystemsResponse = {
operating_system: string; operating_system: string;

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
type GetOverviewResponse = { type GetOverviewResponse = {
sessions: number; sessions: number;

View file

@ -1,6 +1,6 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
const TimeBucketToFn = { const TimeBucketToFn = {
hour: "toStartOfHour", hour: "toStartOfHour",

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
type GetPagesResponse = { type GetPagesResponse = {
pathname: string; pathname: string;

View file

@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify"; import { FastifyReply, FastifyRequest } from "fastify";
import { getTimeStatement, processResults } from "./utils"; import { getTimeStatement, processResults } from "./utils.js";
import clickhouse from "../db/clickhouse/clickhouse"; import clickhouse from "../db/clickhouse/clickhouse.js";
import { GenericRequest } from "./types"; import { GenericRequest } from "./types.js";
type GetReferrersResponse = { type GetReferrersResponse = {
referrer: string; referrer: string;

View file

@ -1,5 +1,5 @@
import { createClient } from "@clickhouse/client"; import { createClient } from "@clickhouse/client";
import { Session } from "../postgres/types"; import { Session } from "../postgres/types.js";
export const clickhouse = createClient({ export const clickhouse = createClient({
host: process.env.CLICKHOUSE_HOST, host: process.env.CLICKHOUSE_HOST,

View file

@ -1,6 +1,7 @@
import postgres from "postgres"; import postgres from "postgres";
import { Session } from "./types"; import { Session } from "./types.js";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { auth } from "../../lib/auth.js";
dotenv.config(); dotenv.config();
@ -14,25 +15,99 @@ export const sql = postgres({
export async function initializePostgres() { export async function initializePostgres() {
try { try {
await sql<Session[]>` // Phase 1: Create tables with no dependencies
CREATE TABLE IF NOT EXISTS active_sessions ( await Promise.all([
session_id TEXT PRIMARY KEY, sql`
user_id TEXT, CREATE TABLE IF NOT EXISTS "user" (
hostname TEXT, "id" text not null primary key,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "name" text not null,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "email" text not null unique,
pageviews INT DEFAULT 0, "emailVerified" boolean not null,
entry_page TEXT, "image" text,
exit_page TEXT, "createdAt" timestamp not null,
device_type TEXT, "updatedAt" timestamp not null
screen_width INT, );
screen_height INT, `,
browser TEXT,
operating_system TEXT, sql`
language TEXT, CREATE TABLE IF NOT EXISTS "verification" (
referrer TEXT "id" text not null primary key,
); "identifier" text not null,
`; "value" text not null,
"expiresAt" timestamp not null,
"createdAt" timestamp,
"updatedAt" timestamp
);
`,
sql<Session[]>`
CREATE TABLE IF NOT EXISTS active_sessions (
session_id TEXT PRIMARY KEY,
user_id TEXT,
hostname TEXT,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
pageviews INT DEFAULT 0,
entry_page TEXT,
exit_page TEXT,
device_type TEXT,
screen_width INT,
screen_height INT,
browser TEXT,
operating_system TEXT,
language TEXT,
referrer TEXT
);
`,
]);
// Phase 2: Create tables with foreign key dependencies
await Promise.all([
sql`
CREATE TABLE IF NOT EXISTS "session" (
"id" text not null primary key,
"expiresAt" timestamp not null,
"token" text not null unique,
"createdAt" timestamp not null,
"updatedAt" timestamp not null,
"ipAddress" text,
"userAgent" text,
"userId" text not null references "user" ("id")
);
`,
sql`
CREATE TABLE IF NOT EXISTS "account" (
"id" text not null primary key,
"accountId" text not null,
"providerId" text not null,
"userId" text not null references "user" ("id"),
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamp,
"refreshTokenExpiresAt" timestamp,
"scope" text,
"password" text,
"createdAt" timestamp not null,
"updatedAt" timestamp not null
);
`,
]);
const user =
await sql`SELECT count(*) FROM "user" WHERE username = 'admin'`;
if (user.length === 0) {
auth.api.signUpEmail({
body: {
email: "test@test.com",
username: "admin",
name: "admin",
password: "admin123",
},
});
}
console.log("Tables created successfully."); console.log("Tables created successfully.");
} catch (err) { } catch (err) {
console.error("Error creating tables:", err); console.error("Error creating tables:", err);

View file

@ -1,6 +1,6 @@
import { insertSessions } from "../clickhouse/clickhouse"; import { insertSessions } from "../clickhouse/clickhouse.js";
import { sql } from "./postgres"; import { sql } from "./postgres.js";
import { Session } from "./types"; import { Session } from "./types.js";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
// function convertPostgresToClickhouse(postgresTimestamp: string): string { // function convertPostgresToClickhouse(postgresTimestamp: string): string {

View file

@ -1,22 +1,32 @@
import cors from "@fastify/cors"; import cors from "@fastify/cors";
import fastifyStatic from "@fastify/static"; import fastifyStatic from "@fastify/static";
import Fastify, { FastifyReply, FastifyRequest } from "fastify"; import Fastify, { FastifyReply, FastifyRequest } from "fastify";
import path from "path"; import FastifyBetterAuth from "fastify-better-auth";
import { trackPageView } from "./actions/trackPageView";
import { initializeClickhouse } from "./db/clickhouse/clickhouse";
import { TrackingPayload } from "./types";
import { initializePostgres } from "./db/postgres/postgres";
import cron from "node-cron"; import cron from "node-cron";
import { cleanupOldSessions } from "./db/postgres/session-cleanup"; import { dirname, join } from "path";
import { getLiveUsercount } from "./api/getLiveUsercount"; import { fileURLToPath } from "url";
import { getCountries } from "./api/getCountries"; import { Headers, HeadersInit } from "undici"; // Ensure Undici is used for Headers
import { getOperatingSystems } from "./api/getOperatingSystems"; import { trackPageView } from "./actions/trackPageView.js";
import { getBrowsers } from "./api/getBrowsers"; import { getBrowsers } from "./api/getBrowsers.js";
import { getDevices } from "./api/getDevices"; import { getCountries } from "./api/getCountries.js";
import { getReferrers } from "./api/getReferrers"; import { getDevices } from "./api/getDevices.js";
import { getPages } from "./api/getPages"; import { getLiveUsercount } from "./api/getLiveUsercount.js";
import { getPageViews } from "./api/getPageViews"; import { getOperatingSystems } from "./api/getOperatingSystems.js";
import { getOverview } from "./api/getOverview"; import { getOverview } from "./api/getOverview.js";
import { getPages } from "./api/getPages.js";
import { getPageViews } from "./api/getPageViews.js";
import { getReferrers } from "./api/getReferrers.js";
import { initializeClickhouse } from "./db/clickhouse/clickhouse.js";
import { initializePostgres } from "./db/postgres/postgres.js";
import { cleanupOldSessions } from "./db/postgres/session-cleanup.js";
import { auth } from "./lib/auth.js";
import { TrackingPayload } from "./types.js";
import { toNodeHandler } from "better-auth/node";
import { mapHeaders } from "./lib/betterAuth.js";
// ESM replacement for __dirname:
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const server = Fastify({ const server = Fastify({
logger: { logger: {
@ -30,13 +40,77 @@ const server = Fastify({
// Register CORS // Register CORS
server.register(cors, { server.register(cors, {
origin: true, // In production, you should specify your frontend domain origin: [
"http://localhost:3002",
"https://tracking.tomato.gg",
"https://tomato.gg",
], // In production, you should specify your frontend domain
credentials: true,
}); });
// Serve static files // Serve static files
server.register(fastifyStatic, { server.register(fastifyStatic, {
root: path.join(__dirname, "../public"), root: join(__dirname, "../public"),
prefix: "/", // optional: default '/' prefix: "/", // or whatever prefix you need
});
server.register(
async (fastify, options) => {
await fastify.register((fastify) => {
const authHandler = toNodeHandler(options.auth);
fastify.addContentTypeParser(
"application/json",
/* c8 ignore next 3 */
(_request, _payload, done) => {
done(null, null);
}
);
fastify.all("/api/auth/*", async (request, reply: any) => {
reply.raw.setHeaders(mapHeaders(reply.getHeaders()));
await authHandler(request.raw, reply.raw);
});
fastify.all("/auth/*", async (request, reply: any) => {
reply.raw.setHeaders(mapHeaders(reply.getHeaders()));
await authHandler(request.raw, reply.raw);
});
});
},
{ auth }
);
server.addHook("onRequest", async (request, reply) => {
const { url } = request.raw;
// Bypass auth for health check and tracking
if (
url?.startsWith("/health") ||
url?.startsWith("/track/pageview") ||
url?.startsWith("/analytics") ||
url?.startsWith("/auth") ||
url?.startsWith("/api/auth")
) {
return;
}
try {
// Convert Fastify headers object into Fetch-compatible Headers
const headers = new Headers(request.headers as HeadersInit);
// Get session from BetterAuth
const session = await auth.api.getSession({ headers });
if (!session) {
return reply.status(401).send({ error: "Unauthorized" });
}
// Attach session user info to request
request.user = session.user;
} catch (err) {
console.error("Auth Error:", err);
return reply.status(500).send({ error: "Auth check failed" });
}
}); });
// Health check endpoint // Health check endpoint
@ -95,3 +169,9 @@ const start = async () => {
}; };
start(); start();
declare module "fastify" {
interface FastifyRequest {
user?: any; // Or define a more specific user type
}
}

27
server/src/lib/auth.ts Normal file
View file

@ -0,0 +1,27 @@
import { betterAuth } from "better-auth";
import pg from "pg";
import { username } from "better-auth/plugins";
import dotenv from "dotenv";
dotenv.config();
export const auth = betterAuth({
basePath: "/auth",
database: new pg.Pool({
host: process.env.POSTGRES_HOST || "postgres",
port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
}),
emailAndPassword: {
enabled: true,
},
plugins: [username()],
trustedOrigins: [
"http://localhost:3002",
"http://localhost:3001",
"https://tracking.tomato.gg",
"https://tomato.gg",
],
});

View file

@ -0,0 +1,10 @@
export function mapHeaders(headers: any) {
const entries = Object.entries(headers);
const map = new Map();
for (const [headerKey, headerValue] of entries) {
if (headerValue != null) {
map.set(headerKey, headerValue);
}
}
return map;
}

View file

@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"module": "commonjs", "module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": [ "lib": [
"ES2020" "ES2020"
], ],
@ -11,7 +12,6 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src",
"moduleResolution": "node",
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": [ "include": [