mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Persist users and orgs (#29)
This commit is contained in:
parent
82b0e45668
commit
a933a42de5
33 changed files with 2022 additions and 208 deletions
23
.github/workflows/CI.yml
vendored
23
.github/workflows/CI.yml
vendored
|
|
@ -38,6 +38,22 @@ jobs:
|
|||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15.4
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: roo_code_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -69,8 +85,15 @@ jobs:
|
|||
- name: Run type checker
|
||||
run: pnpm check-types
|
||||
|
||||
- name: Setup database schema
|
||||
run: pnpm db:test:push
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:password@localhost:5432/roo_code_test
|
||||
|
||||
- name: Run unit tests
|
||||
run: pnpm test # -- --coverage
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:password@localhost:5432/roo_code_test
|
||||
|
||||
# - name: Upload coverage reports to Codecov
|
||||
# uses: codecov/codecov-action@v4
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ const nextConfig: NextConfig = {
|
|||
eslint: { dirs: ['.'] },
|
||||
poweredByHeader: false,
|
||||
reactStrictMode: true,
|
||||
// If node middleware were stable & enabled, then `syncAuth` could be
|
||||
// implemented in the middleware instead of the layout.
|
||||
// experimental: {
|
||||
// nodeMiddleware: true,
|
||||
// },
|
||||
};
|
||||
|
||||
export default withSentryConfig(bundleAnalyzer(withNextIntl(nextConfig)), {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"check-types": "tsc --noEmit --pretty",
|
||||
"test": "vitest run",
|
||||
"test": "dotenvx run -f .env.test -f .env -- vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
|
|
@ -13,7 +13,12 @@
|
|||
"clean": "rimraf .next out coverage",
|
||||
"db:generate": "dotenvx run -f .env.development -- drizzle-kit generate",
|
||||
"db:migrate": "dotenvx run -f .env.development -- drizzle-kit migrate",
|
||||
"db:push": "dotenvx run -f .env.development -- drizzle-kit push",
|
||||
"db:studio": "dotenvx run -f .env.development -- drizzle-kit studio",
|
||||
"db:test:generate": "dotenvx run -f .env.test -- drizzle-kit generate",
|
||||
"db:test:migrate": "dotenvx run -f .env.test -- drizzle-kit migrate",
|
||||
"db:test:push": "dotenvx run -f .env.test -- drizzle-kit push",
|
||||
"db:test:studio": "dotenvx run -f .env.test -- drizzle-kit studio",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"storybook:serve": "http-server storybook-static --port 6006 --silent",
|
||||
|
|
@ -56,6 +61,7 @@
|
|||
"next": "^15.3.2",
|
||||
"next-intl": "^4.1.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"next-typesafe-url": "^5.1.7",
|
||||
"pg": "^8.13.0",
|
||||
"pino": "^9.5.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
|
|
|
|||
190
pnpm-lock.yaml
generated
190
pnpm-lock.yaml
generated
|
|
@ -110,6 +110,9 @@ importers:
|
|||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next-typesafe-url:
|
||||
specifier: ^5.1.7
|
||||
version: 5.1.7(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(zod@3.24.4)
|
||||
pg:
|
||||
specifier: ^8.13.0
|
||||
version: 8.15.6
|
||||
|
|
@ -3614,6 +3617,9 @@ packages:
|
|||
'@types/mdx@2.0.13':
|
||||
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
|
||||
|
||||
'@types/minimist@1.2.5':
|
||||
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
|
|
@ -3623,6 +3629,9 @@ packages:
|
|||
'@types/node@22.15.17':
|
||||
resolution: {integrity: sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==}
|
||||
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
'@types/parse-json@4.0.2':
|
||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||
|
||||
|
|
@ -4012,6 +4021,10 @@ packages:
|
|||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
arrify@1.0.1:
|
||||
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
asn1.js@4.10.1:
|
||||
resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==}
|
||||
|
||||
|
|
@ -4240,6 +4253,10 @@ packages:
|
|||
camel-case@4.1.2:
|
||||
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
|
||||
|
||||
camelcase-keys@6.2.2:
|
||||
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4615,6 +4632,10 @@ packages:
|
|||
supports-color:
|
||||
optional: true
|
||||
|
||||
decamelize-keys@1.1.1:
|
||||
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -5495,6 +5516,10 @@ packages:
|
|||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
hard-rejection@2.1.0:
|
||||
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -5557,6 +5582,10 @@ packages:
|
|||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
html-encoding-sniffer@3.0.0:
|
||||
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -5803,6 +5832,10 @@ packages:
|
|||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
is-plain-obj@1.1.0:
|
||||
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -6190,6 +6223,10 @@ packages:
|
|||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
kind-of@6.0.3:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
kleur@3.0.3:
|
||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -6354,6 +6391,10 @@ packages:
|
|||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lucide-react@0.509.0:
|
||||
resolution: {integrity: sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==}
|
||||
peerDependencies:
|
||||
|
|
@ -6387,6 +6428,10 @@ packages:
|
|||
makeerror@1.0.12:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
|
||||
map-obj@1.0.1:
|
||||
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
map-obj@4.3.0:
|
||||
resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -6422,6 +6467,10 @@ packages:
|
|||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
meow@9.0.0:
|
||||
resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
merge-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -6495,6 +6544,10 @@ packages:
|
|||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimist-options@4.1.0:
|
||||
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
|
|
@ -6575,6 +6628,14 @@ packages:
|
|||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next-typesafe-url@5.1.7:
|
||||
resolution: {integrity: sha512-RJRhRygT1WGpq6w52CoN0iXMhSj87ap/4VOpDiNhFSmp9TBYMXf0HOf0ygY6L/puDxKw8J1FqaIOb2+78DfuGQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
next: ^13.4.12 || ^14.0.0 || ^15.0.0
|
||||
react: ^18.2.0 || ^19.0.0
|
||||
zod: ^3.21.4
|
||||
|
||||
next@15.3.2:
|
||||
resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
|
|
@ -6633,6 +6694,10 @@ packages:
|
|||
normalize-package-data@2.5.0:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
|
||||
normalize-package-data@3.0.3:
|
||||
resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -7200,6 +7265,10 @@ packages:
|
|||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
quick-lru@4.0.1:
|
||||
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
|
|
@ -7297,10 +7366,18 @@ packages:
|
|||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
read-pkg-up@7.0.1:
|
||||
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
read-pkg@3.0.0:
|
||||
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
read-pkg@5.2.0:
|
||||
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
|
|
@ -8125,6 +8202,10 @@ packages:
|
|||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
trim-newlines@3.0.1:
|
||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ts-api-utils@2.1.0:
|
||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
||||
engines: {node: '>=18.12'}
|
||||
|
|
@ -8235,6 +8316,10 @@ packages:
|
|||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
type-fest@0.18.1:
|
||||
resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@0.20.2:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -8243,6 +8328,10 @@ packages:
|
|||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@0.6.0:
|
||||
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
type-fest@0.7.1:
|
||||
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -8718,6 +8807,9 @@ packages:
|
|||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -8735,6 +8827,10 @@ packages:
|
|||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs-parser@20.2.9:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -12391,6 +12487,8 @@ snapshots:
|
|||
|
||||
'@types/mdx@2.0.13': {}
|
||||
|
||||
'@types/minimist@1.2.5': {}
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
optional: true
|
||||
|
||||
|
|
@ -12402,6 +12500,8 @@ snapshots:
|
|||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/parse-json@4.0.2': {}
|
||||
|
||||
'@types/pg-pool@2.0.6':
|
||||
|
|
@ -12905,6 +13005,8 @@ snapshots:
|
|||
get-intrinsic: 1.3.0
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
arrify@1.0.1: {}
|
||||
|
||||
asn1.js@4.10.1:
|
||||
dependencies:
|
||||
bn.js: 4.12.2
|
||||
|
|
@ -13210,6 +13312,12 @@ snapshots:
|
|||
pascal-case: 3.1.2
|
||||
tslib: 2.8.1
|
||||
|
||||
camelcase-keys@6.2.2:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
map-obj: 4.3.0
|
||||
quick-lru: 4.0.1
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
|
@ -13593,6 +13701,11 @@ snapshots:
|
|||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decamelize-keys@1.1.1:
|
||||
dependencies:
|
||||
decamelize: 1.2.0
|
||||
map-obj: 1.0.1
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decimal.js@10.5.0: {}
|
||||
|
|
@ -14578,6 +14691,8 @@ snapshots:
|
|||
dependencies:
|
||||
duplexer: 0.1.2
|
||||
|
||||
hard-rejection@2.1.0: {}
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@3.0.0: {}
|
||||
|
|
@ -14637,6 +14752,10 @@ snapshots:
|
|||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
html-encoding-sniffer@3.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 2.0.0
|
||||
|
|
@ -14916,6 +15035,8 @@ snapshots:
|
|||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-plain-obj@1.1.0: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
|
@ -15544,6 +15665,8 @@ snapshots:
|
|||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
kind-of@6.0.3: {}
|
||||
|
||||
kleur@3.0.3: {}
|
||||
|
||||
lazy-ass@1.6.0: {}
|
||||
|
|
@ -15693,6 +15816,10 @@ snapshots:
|
|||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lru-cache@6.0.0:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
lucide-react@0.509.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
|
@ -15728,6 +15855,8 @@ snapshots:
|
|||
dependencies:
|
||||
tmpl: 1.0.5
|
||||
|
||||
map-obj@1.0.1: {}
|
||||
|
||||
map-obj@4.3.0: {}
|
||||
|
||||
map-or-similar@1.5.0: {}
|
||||
|
|
@ -15756,6 +15885,21 @@ snapshots:
|
|||
|
||||
memorystream@0.3.1: {}
|
||||
|
||||
meow@9.0.0:
|
||||
dependencies:
|
||||
'@types/minimist': 1.2.5
|
||||
camelcase-keys: 6.2.2
|
||||
decamelize: 1.2.0
|
||||
decamelize-keys: 1.1.1
|
||||
hard-rejection: 2.1.0
|
||||
minimist-options: 4.1.0
|
||||
normalize-package-data: 3.0.3
|
||||
read-pkg-up: 7.0.1
|
||||
redent: 3.0.0
|
||||
trim-newlines: 3.0.1
|
||||
type-fest: 0.18.1
|
||||
yargs-parser: 20.2.9
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
|
@ -15812,6 +15956,12 @@ snapshots:
|
|||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minimist-options@4.1.0:
|
||||
dependencies:
|
||||
arrify: 1.0.1
|
||||
is-plain-obj: 1.1.0
|
||||
kind-of: 6.0.3
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@4.2.8: {}
|
||||
|
|
@ -15872,6 +16022,14 @@ snapshots:
|
|||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
next-typesafe-url@5.1.7(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(zod@3.24.4):
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
meow: 9.0.0
|
||||
next: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
zod: 3.24.4
|
||||
|
||||
next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@next/env': 15.3.2
|
||||
|
|
@ -15956,6 +16114,13 @@ snapshots:
|
|||
semver: 5.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
normalize-package-data@3.0.3:
|
||||
dependencies:
|
||||
hosted-git-info: 4.1.0
|
||||
is-core-module: 2.16.1
|
||||
semver: 7.7.1
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
|
@ -16563,6 +16728,8 @@ snapshots:
|
|||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
quick-lru@4.0.1: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
|
@ -16672,12 +16839,25 @@ snapshots:
|
|||
|
||||
react@19.1.0: {}
|
||||
|
||||
read-pkg-up@7.0.1:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
read-pkg: 5.2.0
|
||||
type-fest: 0.8.1
|
||||
|
||||
read-pkg@3.0.0:
|
||||
dependencies:
|
||||
load-json-file: 4.0.0
|
||||
normalize-package-data: 2.5.0
|
||||
path-type: 3.0.0
|
||||
|
||||
read-pkg@5.2.0:
|
||||
dependencies:
|
||||
'@types/normalize-package-data': 2.4.4
|
||||
normalize-package-data: 2.5.0
|
||||
parse-json: 5.2.0
|
||||
type-fest: 0.6.0
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
|
|
@ -17646,6 +17826,8 @@ snapshots:
|
|||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
trim-newlines@3.0.1: {}
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
|
|
@ -17742,10 +17924,14 @@ snapshots:
|
|||
|
||||
type-detect@4.0.8: {}
|
||||
|
||||
type-fest@0.18.1: {}
|
||||
|
||||
type-fest@0.20.2: {}
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
type-fest@0.6.0: {}
|
||||
|
||||
type-fest@0.7.1: {}
|
||||
|
||||
type-fest@0.8.1: {}
|
||||
|
|
@ -18288,6 +18474,8 @@ snapshots:
|
|||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yaml@1.10.2: {}
|
||||
|
|
@ -18299,6 +18487,8 @@ snapshots:
|
|||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs-parser@20.2.9: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@15.4.1:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const getUsage = async ({
|
|||
}: {
|
||||
orgId?: string | null;
|
||||
timePeriod: TimePeriod;
|
||||
}): Promise<UsageRecord> => {
|
||||
}): Promise<UsageRecord | undefined> => {
|
||||
if (!orgId) {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,17 @@
|
|||
import { eq, gte, and, desc } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { auditLogs } from '@/db/schema';
|
||||
import { auditLogs, type AuditLog } from '@/db/schema';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import { AuditLog } from '@/db/schema';
|
||||
|
||||
/**
|
||||
* getAuditLogs
|
||||
*
|
||||
* Server action to retrieve audit logs for an organization
|
||||
*/
|
||||
export const getAuditLogs = async ({
|
||||
orgId,
|
||||
limit = 100,
|
||||
nRecentDays,
|
||||
timePeriod,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
limit?: number;
|
||||
nRecentDays?: number;
|
||||
timePeriod?: number;
|
||||
}): Promise<AuditLog[]> => {
|
||||
if (!orgId) {
|
||||
return [];
|
||||
|
|
@ -29,27 +23,27 @@ export const getAuditLogs = async ({
|
|||
if (isNaN(limit) || limit <= 0) {
|
||||
throw new Error('Limit must be a positive number');
|
||||
}
|
||||
if (nRecentDays === undefined) {
|
||||
|
||||
if (timePeriod === undefined) {
|
||||
return await db
|
||||
.select()
|
||||
.from(auditLogs)
|
||||
.where(eq(auditLogs.organizationId, orgId))
|
||||
.where(eq(auditLogs.orgId, orgId))
|
||||
.orderBy(desc(auditLogs.createdAt))
|
||||
.limit(limit);
|
||||
} else {
|
||||
if (isNaN(nRecentDays) || nRecentDays <= 0) {
|
||||
throw new Error('nRecentDays must be a positive number');
|
||||
if (isNaN(timePeriod) || timePeriod <= 0) {
|
||||
throw new Error('timePeriod must be a positive number');
|
||||
}
|
||||
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - nRecentDays);
|
||||
cutoff.setDate(cutoff.getDate() - timePeriod);
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(auditLogs)
|
||||
.where(
|
||||
and(
|
||||
eq(auditLogs.organizationId, orgId),
|
||||
gte(auditLogs.createdAt, cutoff),
|
||||
),
|
||||
and(eq(auditLogs.orgId, orgId), gte(auditLogs.createdAt, cutoff)),
|
||||
)
|
||||
.orderBy(desc(auditLogs.createdAt))
|
||||
.limit(limit);
|
||||
|
|
@ -59,6 +53,7 @@ export const getAuditLogs = async ({
|
|||
event: 'audit_log_fetch_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { z } from 'zod';
|
|||
|
||||
import { AuditLogTargetType } from '@/db/schema';
|
||||
import { createAuditLog } from '@/lib/server/auditLogs';
|
||||
|
||||
import {
|
||||
handleError,
|
||||
isAuthSuccess,
|
||||
|
|
@ -48,24 +49,24 @@ export async function updateDefaultParameters(
|
|||
): Promise<ApiResponse> {
|
||||
try {
|
||||
const authResult = await validateAuth();
|
||||
if (!isAuthSuccess(authResult)) return authResult;
|
||||
const { userId, orgId } = authResult;
|
||||
|
||||
if (!isAuthSuccess(authResult)) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const result = defaultParametersSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid request data',
|
||||
};
|
||||
return { success: false, error: 'Invalid request data' };
|
||||
}
|
||||
|
||||
const validatedData = result.data;
|
||||
|
||||
// TODO: persist data to the database
|
||||
|
||||
// TODO: audit log description should contain more granular information on what changed
|
||||
// TODO: Audit log description should contain more granular information on
|
||||
// what changed.
|
||||
await createAuditLog({
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
userId: authResult.userId,
|
||||
orgId: authResult.orgId,
|
||||
targetType: AuditLogTargetType.DEFAULT_PARAMETERS,
|
||||
targetId: 'default-parameters',
|
||||
newValue: validatedData,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
'use server';
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { db } from '@/db';
|
||||
import { organizationSettings } from '@/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { type OrganizationSettings } from '@/schemas';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { orgSettings, type OrgSettings } from '@/db/schema';
|
||||
|
||||
/**
|
||||
* Get organization settings for the current organization
|
||||
*/
|
||||
export async function getOrganizationSettings(): Promise<
|
||||
OrganizationSettings | undefined
|
||||
OrgSettings | undefined
|
||||
> {
|
||||
const { userId, orgId } = await auth();
|
||||
|
||||
|
|
@ -24,13 +21,9 @@ export async function getOrganizationSettings(): Promise<
|
|||
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(organizationSettings)
|
||||
.where(eq(organizationSettings.organizationId, orgId))
|
||||
.from(orgSettings)
|
||||
.where(eq(orgSettings.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (settings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return settings[0];
|
||||
return settings.length === 0 ? undefined : settings[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,30 +33,30 @@ type AllowAllProvidersRequest = z.infer<typeof allowAllProvidersSchema>;
|
|||
type ProviderToggleRequest = z.infer<typeof providerToggleSchema>;
|
||||
type ModelToggleRequest = z.infer<typeof modelToggleSchema>;
|
||||
|
||||
// Note: I haven't thought much about this API; this is a temporary stub to get audit logs working.
|
||||
// Note: I haven't thought much about this API; this is a temporary stub to get
|
||||
// audit logs working.
|
||||
// Feel free to refactor when implementing actual data writes.
|
||||
export async function updateAllowAllProviders(
|
||||
data: AllowAllProvidersRequest,
|
||||
): Promise<ApiResponse> {
|
||||
try {
|
||||
const authResult = await validateAuth();
|
||||
if (!isAuthSuccess(authResult)) return authResult;
|
||||
const { userId, orgId } = authResult;
|
||||
|
||||
if (!isAuthSuccess(authResult)) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const result = allowAllProvidersSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid request data',
|
||||
};
|
||||
return { success: false, error: 'Invalid request data' };
|
||||
}
|
||||
|
||||
const validatedData = result.data;
|
||||
|
||||
// TODO: persist data to the database
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
userId: authResult.userId,
|
||||
orgId: authResult.orgId,
|
||||
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
|
||||
targetId: 'allow-all-providers',
|
||||
newValue: { allowAllProviders: validatedData.allowAllProviders },
|
||||
|
|
@ -72,30 +72,30 @@ export async function updateAllowAllProviders(
|
|||
}
|
||||
}
|
||||
|
||||
// Note: I haven't thought much about this API; this is a temporary stub to get audit logs working.
|
||||
// Note: I haven't thought much about this API; this is a temporary stub to get
|
||||
// audit logs working.
|
||||
// Feel free to refactor when implementing actual data writes.
|
||||
export async function updateProviderStatus(
|
||||
data: ProviderToggleRequest,
|
||||
): Promise<ApiResponse> {
|
||||
try {
|
||||
const authResult = await validateAuth();
|
||||
if (!isAuthSuccess(authResult)) return authResult;
|
||||
const { userId, orgId } = authResult;
|
||||
|
||||
if (!isAuthSuccess(authResult)) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const result = providerToggleSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid request data',
|
||||
};
|
||||
return { success: false, error: 'Invalid request data' };
|
||||
}
|
||||
|
||||
const validatedData = result.data;
|
||||
|
||||
// TODO: persist data to the database
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
userId: authResult.userId,
|
||||
orgId: authResult.orgId,
|
||||
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
|
||||
targetId: validatedData.providerId,
|
||||
newValue: { enabled: validatedData.enabled },
|
||||
|
|
@ -111,40 +111,37 @@ export async function updateProviderStatus(
|
|||
}
|
||||
}
|
||||
|
||||
// Note: I haven't thought much about this API; this is a temporary stub to get audit logs working.
|
||||
// Note: I haven't thought much about this API; this is a temporary stub to get
|
||||
// audit logs working.
|
||||
// Feel free to refactor when implementing actual data writes.
|
||||
export async function updateModelStatus(
|
||||
data: ModelToggleRequest,
|
||||
): Promise<ApiResponse> {
|
||||
try {
|
||||
const authResult = await validateAuth();
|
||||
if (!isAuthSuccess(authResult)) return authResult;
|
||||
const { userId, orgId } = authResult;
|
||||
|
||||
if (!isAuthSuccess(authResult)) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const result = modelToggleSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid request data',
|
||||
};
|
||||
return { success: false, error: 'Invalid request data' };
|
||||
}
|
||||
|
||||
const validatedData = result.data;
|
||||
|
||||
// TODO: persist data to the database
|
||||
|
||||
await createAuditLog({
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
userId: authResult.userId,
|
||||
orgId: authResult.orgId,
|
||||
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
|
||||
targetId: `${validatedData.providerId}:${validatedData.modelId}`,
|
||||
newValue: { enabled: validatedData.enabled },
|
||||
description: `${validatedData.enabled ? 'Enabled' : 'Disabled'} model ${validatedData.modelId} for provider ${validatedData.providerId}`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Model status updated successfully',
|
||||
};
|
||||
return { success: true, message: 'Model status updated successfully' };
|
||||
} catch (error) {
|
||||
return handleError(error, 'model_toggle');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { AuditLog } from '@/db/schema';
|
||||
|
|
@ -19,21 +19,18 @@ import {
|
|||
import { AuditLogEntry, AuditLogDrawer } from '@/components/audit-logs';
|
||||
|
||||
export const AuditLogs = () => {
|
||||
const { organization } = useOrganization();
|
||||
const { orgId } = useAuth();
|
||||
const limit = 20;
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
|
||||
const { data: logs = [], isPending } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, timePeriod],
|
||||
queryFn: () =>
|
||||
getAuditLogs({
|
||||
orgId: organization?.id,
|
||||
limit: 20,
|
||||
nRecentDays: timePeriod,
|
||||
}),
|
||||
enabled: !!organization?.id,
|
||||
queryKey: ['auditLogs', orgId, limit, timePeriod],
|
||||
queryFn: () => getAuditLogs({ orgId, limit, timePeriod }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { UsageCard } from '@/components/usage/UsageCard';
|
||||
import { AuditLogCard } from '@/components/audit-logs/AuditLogCard';
|
||||
import { UsageCard } from '@/components/usage';
|
||||
import { AuditLogCard } from '@/components/audit-logs';
|
||||
|
||||
export const Dashboard = () => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import type { Metadata } from 'next';
|
|||
import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getLocale, getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import { getClerkLocale } from '@/i18n/locale';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { syncAuth } from '@/lib/server/sync';
|
||||
import { Toaster } from '@/components/ui';
|
||||
import {
|
||||
ThemeProvider,
|
||||
|
|
@ -40,8 +41,6 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||
};
|
||||
}
|
||||
|
||||
const geist = Geist({ subsets: ['latin'] });
|
||||
|
||||
const fontSans = Geist({ variable: '--font-sans', subsets: ['latin'] });
|
||||
const fontMono = Geist_Mono({ variable: '--font-mono', subsets: ['latin'] });
|
||||
|
||||
|
|
@ -53,12 +52,10 @@ export default async function RootLayout({
|
|||
const locale = await getLocale();
|
||||
setRequestLocale(locale);
|
||||
|
||||
syncAuth(await auth());
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={cn(geist.className)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
|
||||
suppressHydrationWarning
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
|
||||
|
|
@ -23,17 +23,19 @@ import { AuditLogEntry } from '@/components/audit-logs';
|
|||
import { AuditLogDrawer } from './AuditLogDrawer';
|
||||
|
||||
export function AuditLogCard() {
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const path = usePathname();
|
||||
const { orgId } = useAuth();
|
||||
const limit = 5;
|
||||
const timePeriod = undefined;
|
||||
|
||||
const { data: logs = [], isPending } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, 5],
|
||||
queryFn: () => getAuditLogs({ orgId: organization?.id, limit: 5 }),
|
||||
enabled: !!organization?.id,
|
||||
queryKey: ['auditLogs', orgId, limit, timePeriod],
|
||||
queryFn: () => getAuditLogs({ orgId, limit, timePeriod }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ export const Metric = ({ label, value, isPending }: MetricProps) => (
|
|||
</div>
|
||||
) : typeof value === 'number' ? (
|
||||
formatNumber(value)
|
||||
) : value ? (
|
||||
) : typeof value !== 'undefined' ? (
|
||||
value
|
||||
) : null}
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const UsageCard = () => {
|
|||
const { data: usage = {}, isPending } = useQuery({
|
||||
queryKey: ['usage', orgId, timePeriod],
|
||||
queryFn: () => getUsage({ orgId, timePeriod }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
1
src/components/usage/index.ts
Normal file
1
src/components/usage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { UsageCard } from './UsageCard';
|
||||
|
|
@ -1,5 +1,29 @@
|
|||
import { Env } from '../lib/server/env';
|
||||
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
export const db = drizzle(Env.DATABASE_URL!);
|
||||
import { Env } from '@/lib/server/env';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
let testDb: ReturnType<typeof drizzle> | undefined = undefined;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
if (
|
||||
!Env.DATABASE_URL?.includes('test') ||
|
||||
!Env.DATABASE_URL?.includes('localhost')
|
||||
) {
|
||||
throw new Error('DATABASE_URL is not a test database');
|
||||
}
|
||||
|
||||
testDb = drizzle(pool);
|
||||
}
|
||||
|
||||
const db = process.env.NODE_ENV === 'test' ? testDb! : drizzle(pool);
|
||||
|
||||
const disconnect = async () => {
|
||||
await pool.end();
|
||||
};
|
||||
|
||||
export { db, testDb, disconnect };
|
||||
|
|
|
|||
43
src/db/migrations/0002_normal_mephistopheles.sql
Normal file
43
src/db/migrations/0002_normal_mephistopheles.sql
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
CREATE TABLE "organizations" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"image_url" text NOT NULL,
|
||||
"entity" jsonb NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"organization_id" text,
|
||||
"organization_role" text,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"image_url" text NOT NULL,
|
||||
"entity" jsonb NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "id" SET DATA TYPE uuid;--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "id" SET DEFAULT gen_random_uuid();--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "created_at" SET DATA TYPE timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "organization_settings" ALTER COLUMN "version" SET DEFAULT 1;--> statement-breakpoint
|
||||
ALTER TABLE "organization_settings" ALTER COLUMN "allow_list" SET DEFAULT '{"allowAll":true,"providers":{}}'::jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "organizations_slug_idx" ON "organizations" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE INDEX "organizations_created_at_idx" ON "organizations" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE INDEX "users_organization_id_idx" ON "users" USING btree ("organization_id");--> statement-breakpoint
|
||||
CREATE INDEX "users_organization_role_idx" ON "users" USING btree ("organization_id","organization_role");--> statement-breakpoint
|
||||
CREATE INDEX "users_email_idx" ON "users" USING btree ("email");--> statement-breakpoint
|
||||
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "organization_settings" ADD CONSTRAINT "organization_settings_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "audit_logs_organization_id_idx" ON "audit_logs" USING btree ("organization_id");--> statement-breakpoint
|
||||
CREATE INDEX "audit_logs_target_idx" ON "audit_logs" USING btree ("target_type","target_id");--> statement-breakpoint
|
||||
CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE INDEX "organization_settings_created_at_idx" ON "organization_settings" USING btree ("created_at");
|
||||
2
src/db/migrations/0003_strange_wild_pack.sql
Normal file
2
src/db/migrations/0003_strange_wild_pack.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "organizations" ADD COLUMN "last_sync_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "users" ADD COLUMN "last_sync_at" timestamp DEFAULT now() NOT NULL;
|
||||
482
src/db/migrations/meta/0002_snapshot.json
Normal file
482
src/db/migrations/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
{
|
||||
"id": "c88e310b-6684-4083-a251-4e794bcefbbd",
|
||||
"prevId": "a7b8c9d0-e1f2-3456-7890-a1b2c3d4e5f6",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.audit_logs": {
|
||||
"name": "audit_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_type": {
|
||||
"name": "target_type",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"new_value": {
|
||||
"name": "new_value",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"audit_logs_user_id_idx": {
|
||||
"name": "audit_logs_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"audit_logs_organization_id_idx": {
|
||||
"name": "audit_logs_organization_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"audit_logs_target_idx": {
|
||||
"name": "audit_logs_target_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "target_type",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "target_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"audit_logs_created_at_idx": {
|
||||
"name": "audit_logs_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"audit_logs_user_id_users_id_fk": {
|
||||
"name": "audit_logs_user_id_users_id_fk",
|
||||
"tableFrom": "audit_logs",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"audit_logs_organization_id_organizations_id_fk": {
|
||||
"name": "audit_logs_organization_id_organizations_id_fk",
|
||||
"tableFrom": "audit_logs",
|
||||
"tableTo": "organizations",
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.organization_settings": {
|
||||
"name": "organization_settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"default_settings": {
|
||||
"name": "default_settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"allow_list": {
|
||||
"name": "allow_list",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{\"allowAll\":true,\"providers\":{}}'::jsonb"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"organization_settings_created_at_idx": {
|
||||
"name": "organization_settings_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"organization_settings_organization_id_organizations_id_fk": {
|
||||
"name": "organization_settings_organization_id_organizations_id_fk",
|
||||
"tableFrom": "organization_settings",
|
||||
"tableTo": "organizations",
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.organizations": {
|
||||
"name": "organizations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity": {
|
||||
"name": "entity",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"organizations_slug_idx": {
|
||||
"name": "organizations_slug_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"organizations_created_at_idx": {
|
||||
"name": "organizations_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"organization_role": {
|
||||
"name": "organization_role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity": {
|
||||
"name": "entity",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_organization_id_idx": {
|
||||
"name": "users_organization_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_organization_role_idx": {
|
||||
"name": "users_organization_role_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "organization_role",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_email_idx": {
|
||||
"name": "users_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_created_at_idx": {
|
||||
"name": "users_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"users_organization_id_organizations_id_fk": {
|
||||
"name": "users_organization_id_organizations_id_fk",
|
||||
"tableFrom": "users",
|
||||
"tableTo": "organizations",
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
496
src/db/migrations/meta/0003_snapshot.json
Normal file
496
src/db/migrations/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
{
|
||||
"id": "bef500d1-5175-4068-af51-1f565355f1ac",
|
||||
"prevId": "c88e310b-6684-4083-a251-4e794bcefbbd",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.audit_logs": {
|
||||
"name": "audit_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_type": {
|
||||
"name": "target_type",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"new_value": {
|
||||
"name": "new_value",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"audit_logs_user_id_idx": {
|
||||
"name": "audit_logs_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"audit_logs_organization_id_idx": {
|
||||
"name": "audit_logs_organization_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"audit_logs_target_idx": {
|
||||
"name": "audit_logs_target_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "target_type",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "target_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"audit_logs_created_at_idx": {
|
||||
"name": "audit_logs_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"audit_logs_user_id_users_id_fk": {
|
||||
"name": "audit_logs_user_id_users_id_fk",
|
||||
"tableFrom": "audit_logs",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"audit_logs_organization_id_organizations_id_fk": {
|
||||
"name": "audit_logs_organization_id_organizations_id_fk",
|
||||
"tableFrom": "audit_logs",
|
||||
"tableTo": "organizations",
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.organization_settings": {
|
||||
"name": "organization_settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"default_settings": {
|
||||
"name": "default_settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"allow_list": {
|
||||
"name": "allow_list",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{\"allowAll\":true,\"providers\":{}}'::jsonb"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"organization_settings_created_at_idx": {
|
||||
"name": "organization_settings_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"organization_settings_organization_id_organizations_id_fk": {
|
||||
"name": "organization_settings_organization_id_organizations_id_fk",
|
||||
"tableFrom": "organization_settings",
|
||||
"tableTo": "organizations",
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.organizations": {
|
||||
"name": "organizations",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity": {
|
||||
"name": "entity",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_sync_at": {
|
||||
"name": "last_sync_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"organizations_slug_idx": {
|
||||
"name": "organizations_slug_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"organizations_created_at_idx": {
|
||||
"name": "organizations_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"organization_role": {
|
||||
"name": "organization_role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"entity": {
|
||||
"name": "entity",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_sync_at": {
|
||||
"name": "last_sync_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_organization_id_idx": {
|
||||
"name": "users_organization_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_organization_role_idx": {
|
||||
"name": "users_organization_role_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "organization_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "organization_role",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_email_idx": {
|
||||
"name": "users_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_created_at_idx": {
|
||||
"name": "users_created_at_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "created_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"users_organization_id_organizations_id_fk": {
|
||||
"name": "users_organization_id_organizations_id_fk",
|
||||
"tableFrom": "users",
|
||||
"tableTo": "organizations",
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,20 @@
|
|||
"when": 1747347900000,
|
||||
"tag": "0001_add_organization_settings",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1747537218300,
|
||||
"tag": "0002_normal_mephistopheles",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1747551227749,
|
||||
"tag": "0003_strange_wild_pack",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
195
src/db/schema.ts
195
src/db/schema.ts
|
|
@ -1,8 +1,3 @@
|
|||
import {
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
OrganizationDefaultSettings,
|
||||
type OrganizationAllowList,
|
||||
} from '@/schemas';
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
|
|
@ -10,20 +5,163 @@ import {
|
|||
integer,
|
||||
jsonb,
|
||||
uuid,
|
||||
index,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
export const auditLogs = pgTable('audit_logs', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
targetType: integer('target_type').notNull(), // AuditLogTargetType
|
||||
targetId: text('target_id').notNull(),
|
||||
newValue: jsonb('new_value').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
description: text('description').notNull(),
|
||||
});
|
||||
import {
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
type OrganizationDefaultSettings,
|
||||
type OrganizationAllowList,
|
||||
} from '@/schemas';
|
||||
|
||||
/**
|
||||
* users
|
||||
*/
|
||||
|
||||
export const users = pgTable(
|
||||
'users',
|
||||
{
|
||||
id: text('id').notNull().primaryKey(), // Assigned by Clerk.
|
||||
orgId: text('organization_id').references(() => orgs.id),
|
||||
orgRole: text('organization_role'),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull(),
|
||||
imageUrl: text('image_url').notNull(),
|
||||
entity: jsonb('entity').notNull(),
|
||||
lastSyncAt: timestamp('last_sync_at').notNull().defaultNow(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('users_organization_id_idx').on(table.orgId),
|
||||
index('users_organization_role_idx').on(table.orgId, table.orgRole),
|
||||
index('users_email_idx').on(table.email),
|
||||
index('users_created_at_idx').on(table.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const userRelations = relations(users, ({ one }) => ({
|
||||
org: one(orgs, {
|
||||
fields: [users.orgId],
|
||||
references: [orgs.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
export type CreateUser = Omit<typeof users.$inferInsert, 'id' | 'createdAt'>;
|
||||
|
||||
/**
|
||||
* organizations
|
||||
*/
|
||||
|
||||
export const orgs = pgTable(
|
||||
'organizations',
|
||||
{
|
||||
id: text('id').notNull().primaryKey(), // Assigned by Clerk.
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').notNull(),
|
||||
imageUrl: text('image_url').notNull(),
|
||||
entity: jsonb('entity').notNull(),
|
||||
lastSyncAt: timestamp('last_sync_at').notNull().defaultNow(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('organizations_slug_idx').on(table.slug),
|
||||
index('organizations_created_at_idx').on(table.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const orgsRelations = relations(orgs, ({ many, one }) => ({
|
||||
users: many(users),
|
||||
auditLogs: many(auditLogs),
|
||||
orgSettings: one(orgSettings, {
|
||||
fields: [orgs.id],
|
||||
references: [orgSettings.orgId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type Org = typeof orgs.$inferSelect;
|
||||
|
||||
export type CreateOrg = Omit<typeof orgs.$inferInsert, 'id' | 'createdAt'>;
|
||||
|
||||
/**
|
||||
* organization_settings
|
||||
*/
|
||||
|
||||
export const orgSettings = pgTable(
|
||||
'organization_settings',
|
||||
{
|
||||
orgId: text('organization_id')
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.references(() => orgs.id), // Assigned by Clerk.
|
||||
version: integer('version').notNull().default(1),
|
||||
defaultSettings: jsonb('default_settings')
|
||||
.notNull()
|
||||
.$type<OrganizationDefaultSettings>()
|
||||
.default({}),
|
||||
allowList: jsonb('allow_list')
|
||||
.notNull()
|
||||
.$type<OrganizationAllowList>()
|
||||
.default(ORGANIZATION_ALLOW_ALL),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('organization_settings_created_at_idx').on(table.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const orgSettingsRelations = relations(orgSettings, ({ one }) => ({
|
||||
org: one(orgs, {
|
||||
fields: [orgSettings.orgId],
|
||||
references: [orgs.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type OrgSettings = typeof orgSettings.$inferSelect;
|
||||
|
||||
/**
|
||||
* audit_logs
|
||||
*/
|
||||
|
||||
export const auditLogs = pgTable(
|
||||
'audit_logs',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
orgId: text('organization_id')
|
||||
.notNull()
|
||||
.references(() => orgs.id),
|
||||
targetType: integer('target_type').notNull(), // AuditLogTargetType
|
||||
targetId: text('target_id').notNull(),
|
||||
newValue: jsonb('new_value').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
description: text('description').notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('audit_logs_user_id_idx').on(table.userId),
|
||||
index('audit_logs_organization_id_idx').on(table.orgId),
|
||||
index('audit_logs_target_idx').on(table.targetType, table.targetId),
|
||||
index('audit_logs_created_at_idx').on(table.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const auditLogsRelations = relations(auditLogs, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [auditLogs.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
org: one(orgs, {
|
||||
fields: [auditLogs.orgId],
|
||||
references: [orgs.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export enum AuditLogTargetType {
|
||||
PROVIDER_WHITELIST = 1,
|
||||
|
|
@ -35,20 +173,9 @@ export type AuditLog = typeof auditLogs.$inferSelect & {
|
|||
targetType: AuditLogTargetType;
|
||||
};
|
||||
|
||||
export const organizationSettings = pgTable('organization_settings', {
|
||||
// Organization ID (from Clerk)
|
||||
organizationId: text('organization_id').notNull().primaryKey(),
|
||||
|
||||
// Version number, incremented on updates
|
||||
version: integer('version').notNull().default(1),
|
||||
defaultSettings: jsonb('default_settings')
|
||||
.notNull()
|
||||
.$type<OrganizationDefaultSettings>()
|
||||
.default({}),
|
||||
allowList: jsonb('allow_list')
|
||||
.notNull()
|
||||
.$type<OrganizationAllowList>()
|
||||
.default(ORGANIZATION_ALLOW_ALL),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
export type CreateAuditLog = Omit<
|
||||
typeof auditLogs.$inferInsert,
|
||||
'id' | 'createdAt'
|
||||
> & {
|
||||
targetType: AuditLogTargetType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// npx vitest run src/lib/locale.test.ts
|
||||
// pnpm test src/i18n/__tests__/locale.test.ts
|
||||
|
||||
import { enUS, frFR } from '@clerk/localizations';
|
||||
|
||||
import { locales, isLocale, Locales, getClerkLocale, Locale } from './locale';
|
||||
import { locales, isLocale, Locales, getClerkLocale, Locale } from '../locale';
|
||||
|
||||
describe('locale module', () => {
|
||||
describe('locales', () => {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// npx vitest run src/lib/formatters.test.ts
|
||||
// pnpm test src/lib/__tests__/formatters.test.ts
|
||||
|
||||
import { formatNumber, formatCurrency } from './formatters';
|
||||
import { formatNumber, formatCurrency } from '../formatters';
|
||||
|
||||
describe('formatNumber', () => {
|
||||
it('should return empty string for undefined or null values', () => {
|
||||
147
src/lib/server/__tests__/syncCurrentUser.test.ts
Normal file
147
src/lib/server/__tests__/syncCurrentUser.test.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// pnpm test src/lib/server/__tests__/syncCurrentUser.test.ts
|
||||
|
||||
import { db } from '@/db';
|
||||
import { users, orgs } from '@/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import { syncCurrentUser } from '../sync';
|
||||
import { logger } from '../logger';
|
||||
|
||||
const testUserId = 'fake-user-id';
|
||||
const testOrgId = 'fake-org-id';
|
||||
|
||||
vi.mock('@clerk/nextjs/server', () => ({
|
||||
currentUser: vi.fn().mockResolvedValue({
|
||||
id: 'fake-user-id',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
fullName: 'Test User',
|
||||
emailAddresses: [{ emailAddress: 'test@example.com' }],
|
||||
imageUrl: 'https://example.com/image.jpg',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('syncCurrentUser', () => {
|
||||
beforeEach(async () => {
|
||||
await db.delete(users).where(eq(users.id, testUserId));
|
||||
await db.delete(orgs).where(eq(orgs.id, testOrgId));
|
||||
|
||||
await db.insert(orgs).values({
|
||||
id: testOrgId,
|
||||
name: 'Test Organization',
|
||||
slug: 'test-org',
|
||||
imageUrl: 'https://example.com/org-image.jpg',
|
||||
entity: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new user if one does not exist', async () => {
|
||||
await syncCurrentUser({ userId: testUserId });
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, testUserId));
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
const user = result[0];
|
||||
expect(user).toBeDefined();
|
||||
expect(user!.id).toBe(testUserId);
|
||||
expect(user!.name).toBe('Test User');
|
||||
expect(user!.email).toBe('test@example.com');
|
||||
expect(user!.imageUrl).toBe('https://example.com/image.jpg');
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: 'user_created',
|
||||
userId: testUserId,
|
||||
orgId: undefined,
|
||||
orgRole: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update an existing user if data has changed', async () => {
|
||||
await db.insert(users).values({
|
||||
id: testUserId,
|
||||
name: 'Old Name',
|
||||
email: 'old@example.com',
|
||||
imageUrl: 'https://example.com/old-image.jpg',
|
||||
entity: {},
|
||||
lastSyncAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // Set lastSyncAt to 2 hours ago
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
||||
});
|
||||
|
||||
await syncCurrentUser({ userId: testUserId });
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, testUserId));
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
const user = result[0];
|
||||
expect(user).toBeDefined();
|
||||
expect(user!.name).toBe('Test User');
|
||||
expect(user!.email).toBe('test@example.com');
|
||||
expect(user!.imageUrl).toBe('https://example.com/image.jpg');
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: 'user_updated',
|
||||
userId: testUserId,
|
||||
orgId: undefined,
|
||||
orgRole: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update user organization if orgId is provided', async () => {
|
||||
await db.insert(users).values({
|
||||
id: testUserId,
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
imageUrl: 'https://example.com/image.jpg',
|
||||
entity: {},
|
||||
lastSyncAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // Set lastSyncAt to 2 hours ago
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await syncCurrentUser({
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
orgRole: 'admin',
|
||||
});
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, testUserId));
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
const user = result[0];
|
||||
expect(user).toBeDefined();
|
||||
expect(user!.orgId).toBe(testOrgId);
|
||||
expect(user!.orgRole).toBe('admin');
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: 'user_updated',
|
||||
userId: testUserId,
|
||||
orgId: testOrgId,
|
||||
orgRole: 'admin',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
117
src/lib/server/__tests__/syncOrg.test.ts
Normal file
117
src/lib/server/__tests__/syncOrg.test.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// pnpm test src/lib/server/__tests__/syncOrg.test.ts
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { clerkClient } from '@clerk/nextjs/server';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { orgs } from '@/db/schema';
|
||||
|
||||
import { syncOrg } from '../sync';
|
||||
import { logger } from '../logger';
|
||||
|
||||
vi.mock('@clerk/nextjs/server', () => ({
|
||||
clerkClient: vi.fn().mockResolvedValue({
|
||||
organizations: {
|
||||
getOrganization: vi.fn().mockResolvedValue({
|
||||
id: 'test-org-id',
|
||||
name: 'Test Organization',
|
||||
slug: 'test-org',
|
||||
imageUrl: 'https://example.com/org-image.jpg',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('syncOrg', () => {
|
||||
it('should create a new organization if one does not exist', async () => {
|
||||
await syncOrg('test-org-id');
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.id, 'test-org-id'));
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
const org = result[0];
|
||||
expect(org).toBeDefined();
|
||||
expect(org!.id).toBe('test-org-id');
|
||||
expect(org!.name).toBe('Test Organization');
|
||||
expect(org!.slug).toBe('test-org');
|
||||
expect(org!.imageUrl).toBe('https://example.com/org-image.jpg');
|
||||
});
|
||||
|
||||
it('should update an existing organization if data has changed', async () => {
|
||||
await db.delete(orgs).where(eq(orgs.id, 'test-org-id'));
|
||||
|
||||
await db.insert(orgs).values({
|
||||
id: 'test-org-id',
|
||||
name: 'Old Name',
|
||||
slug: 'old-slug',
|
||||
imageUrl: 'https://example.com/old-image.jpg',
|
||||
entity: {},
|
||||
lastSyncAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // Set lastSyncAt to 2 hours ago
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await syncOrg('test-org-id');
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.id, 'test-org-id'));
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
const org = result[0];
|
||||
expect(org).toBeDefined();
|
||||
expect(org!.name).toBe('Test Organization');
|
||||
expect(org!.slug).toBe('test-org');
|
||||
expect(org!.imageUrl).toBe('https://example.com/org-image.jpg');
|
||||
});
|
||||
|
||||
it('should not update if organization data has not changed', async () => {
|
||||
await db.delete(orgs).where(eq(orgs.id, 'test-org-id'));
|
||||
|
||||
await db.insert(orgs).values({
|
||||
id: 'test-org-id',
|
||||
name: 'Test Organization',
|
||||
slug: 'test-org',
|
||||
imageUrl: 'https://example.com/org-image.jpg',
|
||||
entity: {},
|
||||
lastSyncAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // Set lastSyncAt to 2 hours ago
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
vi.mocked(logger.info).mockClear();
|
||||
|
||||
await syncOrg('test-org-id');
|
||||
|
||||
expect(logger.info).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ event: 'organization_updated' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
await db.delete(orgs).where(eq(orgs.id, 'test-org-id'));
|
||||
|
||||
vi.mocked(clerkClient).mockRejectedValueOnce(new Error('Clerk API error'));
|
||||
|
||||
await syncOrg('test-org-id');
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: 'org_sync_error',
|
||||
orgId: 'test-org-id',
|
||||
error: 'Clerk API error',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,52 +1,17 @@
|
|||
import { db } from '@/db';
|
||||
import { auditLogs } from '@/db/schema';
|
||||
import { auditLogs, type CreateAuditLog } from '@/db/schema';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import type { z } from 'zod';
|
||||
import { createInsertSchema } from 'drizzle-zod';
|
||||
|
||||
type AuditLogCreateRequest = z.infer<
|
||||
ReturnType<typeof createInsertSchema<typeof auditLogs>>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Server-side function to create audit logs in the database
|
||||
* @param request The audit log data to create
|
||||
* @returns Object containing success status or error
|
||||
*/
|
||||
export async function createAuditLog(request: AuditLogCreateRequest): Promise<{
|
||||
success: boolean;
|
||||
error?: string | Record<string, unknown>;
|
||||
}> {
|
||||
export async function createAuditLog(values: CreateAuditLog) {
|
||||
try {
|
||||
await db.insert(auditLogs).values({
|
||||
userId: request.userId,
|
||||
organizationId: request.organizationId,
|
||||
targetType: request.targetType,
|
||||
targetId: request.targetId,
|
||||
newValue: request.newValue,
|
||||
description: request.description,
|
||||
// id and createdAt will be set automatically by the database
|
||||
});
|
||||
|
||||
logger.info({
|
||||
event: 'audit_log_created',
|
||||
userId: request.userId,
|
||||
organizationId: request.organizationId,
|
||||
targetType: request.targetType,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
event: 'audit_log_creation_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'An unexpected error occurred',
|
||||
};
|
||||
await db.insert(auditLogs).values(values);
|
||||
const { userId, orgId, targetType } = values;
|
||||
logger.info({ userId, orgId, targetType });
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
const error =
|
||||
e instanceof Error ? e.message : 'An unexpected error occurred';
|
||||
logger.error({ event: 'audit_log_creation_error', error });
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
152
src/lib/server/sync.ts
Normal file
152
src/lib/server/sync.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { clerkClient, currentUser } from '@clerk/nextjs/server';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { users, type CreateUser, orgs, type CreateOrg } from '@/db/schema';
|
||||
|
||||
import { logger } from './logger';
|
||||
|
||||
export async function syncCurrentUser({
|
||||
userId,
|
||||
orgId,
|
||||
orgRole,
|
||||
}: {
|
||||
userId: string;
|
||||
orgId?: string | null;
|
||||
orgRole?: string | null;
|
||||
}) {
|
||||
try {
|
||||
const existingUsers = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
const existingUser =
|
||||
existingUsers.length > 0 ? existingUsers[0] : undefined;
|
||||
|
||||
if (
|
||||
existingUser &&
|
||||
existingUser.lastSyncAt.getTime() > Date.now() - 60_000
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = await currentUser();
|
||||
|
||||
if (!entity) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
const newUser: CreateUser = {
|
||||
orgId,
|
||||
orgRole,
|
||||
name: entity.fullName || entity.username || '',
|
||||
email: entity.emailAddresses[0]?.emailAddress || '',
|
||||
imageUrl: entity.imageUrl,
|
||||
entity: entity,
|
||||
};
|
||||
|
||||
if (!existingUser) {
|
||||
await db.insert(users).values({ id: userId, ...newUser });
|
||||
logger.info({ event: 'user_created', userId, orgId, orgRole });
|
||||
} else {
|
||||
const isDirty =
|
||||
existingUser.orgId !== orgId ||
|
||||
existingUser.orgRole !== newUser.orgRole ||
|
||||
existingUser.name !== newUser.name ||
|
||||
existingUser.email !== newUser.email ||
|
||||
existingUser.imageUrl !== newUser.imageUrl ||
|
||||
existingUser.updatedAt.getTime() < Date.now() - 3_600_000;
|
||||
|
||||
if (isDirty) {
|
||||
await db
|
||||
.update(users)
|
||||
.set({ ...newUser, lastSyncAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(users.id, userId));
|
||||
logger.info({ event: 'user_updated', userId, orgId, orgRole });
|
||||
} else {
|
||||
await db
|
||||
.update(users)
|
||||
.set({ lastSyncAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(users.id, userId));
|
||||
logger.info({ event: 'user_sync', userId, orgId, orgRole });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e.message : 'Unknown error';
|
||||
logger.error({ event: 'user_sync_error', error });
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncOrg(orgId: string) {
|
||||
try {
|
||||
const existingOrgs = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.id, orgId))
|
||||
.limit(1);
|
||||
|
||||
const existingOrg = existingOrgs.length > 0 ? existingOrgs[0] : undefined;
|
||||
|
||||
if (existingOrg && existingOrg.lastSyncAt.getTime() > Date.now() - 60_000) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await clerkClient();
|
||||
|
||||
const entity = await client.organizations.getOrganization({
|
||||
organizationId: orgId,
|
||||
});
|
||||
|
||||
const { name, slug, imageUrl } = entity;
|
||||
const newOrg: CreateOrg = { name, slug, imageUrl, entity };
|
||||
|
||||
if (!existingOrg) {
|
||||
await db.insert(orgs).values({ id: orgId, ...newOrg });
|
||||
logger.info({ event: 'org_created', orgId });
|
||||
} else {
|
||||
const isDirty =
|
||||
existingOrg.name !== name ||
|
||||
existingOrg.slug !== slug ||
|
||||
existingOrg.imageUrl !== imageUrl ||
|
||||
existingOrg.updatedAt.getTime() < Date.now() - 3_600_000;
|
||||
|
||||
if (isDirty) {
|
||||
await db
|
||||
.update(orgs)
|
||||
.set({ ...newOrg, lastSyncAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(orgs.id, orgId));
|
||||
|
||||
logger.info({ event: 'org_updated', orgId });
|
||||
} else {
|
||||
await db
|
||||
.update(orgs)
|
||||
.set({ lastSyncAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(orgs.id, orgId));
|
||||
logger.info({ event: 'org_sync', orgId });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e.message : 'Unknown error';
|
||||
logger.error({ event: 'org_sync_error', orgId, error });
|
||||
}
|
||||
}
|
||||
|
||||
type SyncAuth = {
|
||||
userId: string | null;
|
||||
orgId?: string | null;
|
||||
orgRole?: string | null;
|
||||
};
|
||||
|
||||
export async function syncAuth({ userId, orgId, orgRole }: SyncAuth) {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncCurrentUser({ userId, orgId, orgRole });
|
||||
|
||||
if (orgId) {
|
||||
syncOrg(orgId);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,7 @@ const isUnprotectedRoute = createRouteMatcher([
|
|||
export default clerkMiddleware(
|
||||
async (auth, req) => {
|
||||
if (!isUnprotectedRoute(req)) {
|
||||
console.log(`clerkMiddleware: PROTECTED -> ${req.nextUrl.pathname}`);
|
||||
await auth.protect();
|
||||
} else {
|
||||
console.log(`clerkMiddleware: UNPROTECTED -> ${req.nextUrl.pathname}`);
|
||||
}
|
||||
},
|
||||
{ debug: false },
|
||||
|
|
|
|||
36
vitest-global-setup.ts
Normal file
36
vitest-global-setup.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { sql } from 'drizzle-orm';
|
||||
|
||||
import { Env } from '@/lib/server/env';
|
||||
import { testDb, disconnect } from '@/db';
|
||||
|
||||
async function resetTestDatabase() {
|
||||
const db = testDb!;
|
||||
|
||||
try {
|
||||
const tables = await db.execute<{ table_name: string }>(sql`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE';
|
||||
`);
|
||||
|
||||
const tableNames = (tables.rows || []).map(({ table_name }) => table_name);
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
await db.execute(sql`TRUNCATE TABLE "${sql.raw(tableName)}" CASCADE;`);
|
||||
}
|
||||
|
||||
console.log(`[${Env.DATABASE_URL}] TRUNCATE ${tableNames.join(', ')}`);
|
||||
} catch (error) {
|
||||
console.error('Error resetting database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function () {
|
||||
await resetTestDatabase();
|
||||
|
||||
return async () => {
|
||||
await disconnect();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
import failOnConsole from 'vitest-fail-on-console';
|
||||
|
||||
failOnConsole({
|
||||
|
|
@ -9,3 +8,5 @@ failOnConsole({
|
|||
shouldFailOnLog: true,
|
||||
shouldFailOnWarn: true,
|
||||
});
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { defineConfig } from 'vitest/config';
|
|||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
test: {
|
||||
globals: true, // This is needed by @testing-library to be cleaned up after each test
|
||||
globals: true, // This is needed by @testing-library to be cleaned up after each test.
|
||||
include: ['src/**/*.test.{js,jsx,ts,tsx}'],
|
||||
coverage: {
|
||||
include: ['src/**/*'],
|
||||
exclude: ['src/**/*.stories.{js,jsx,ts,tsx}', '**/*.d.ts'],
|
||||
},
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest-setup.ts'],
|
||||
environment: 'node', // jsdom
|
||||
setupFiles: './vitest-setup.ts',
|
||||
globalSetup: './vitest-global-setup.ts',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue