Adding APIs, schemas for base roles etc.

This commit is contained in:
Bradley Shellnut 2024-12-26 16:46:28 -08:00
parent 12d8384fa4
commit be1a0aecfe
41 changed files with 1441 additions and 94 deletions

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
22

View file

@ -14,10 +14,10 @@ export default defineConfig({
dialect: 'postgresql', dialect: 'postgresql',
casing: 'snake_case', casing: 'snake_case',
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL! url: process.env.DATABASE_URL ?? '',
}, },
migrations: { migrations: {
table: 'migrations', table: 'migrations',
schema: 'public' schema: 'public',
} },
}); });

View file

@ -22,6 +22,9 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@chromatic-com/storybook": "^3.2.3", "@chromatic-com/storybook": "^3.2.3",
"@dicebear/collection": "^9.2.2",
"@dicebear/converter": "^9.2.2",
"@dicebear/core": "^9.2.2",
"@faker-js/faker": "^9.3.0", "@faker-js/faker": "^9.3.0",
"@playwright/test": "^1.45.3", "@playwright/test": "^1.45.3",
"@storybook/addon-essentials": "^8.4.7", "@storybook/addon-essentials": "^8.4.7",
@ -35,6 +38,8 @@
"@sveltejs/enhanced-img": "^0.4.4", "@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.9.0", "@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tanstack/svelte-query": "^5.62.9",
"@tanstack/svelte-query-devtools": "^5.62.9",
"@types/cookie": "^1.0.0", "@types/cookie": "^1.0.0",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
@ -48,7 +53,6 @@
"storybook": "^8.4.7", "storybook": "^8.4.7",
"svelte": "^5.16.0", "svelte": "^5.16.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"svelte-headless-table": "^0.18.3",
"svelte-meta-tags": "^4.0.4", "svelte-meta-tags": "^4.0.4",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2", "svelte-sequential-preprocessor": "^2.0.2",

View file

@ -123,6 +123,15 @@ importers:
'@chromatic-com/storybook': '@chromatic-com/storybook':
specifier: ^3.2.3 specifier: ^3.2.3
version: 3.2.3(react@18.3.1)(storybook@8.4.7) version: 3.2.3(react@18.3.1)(storybook@8.4.7)
'@dicebear/collection':
specifier: ^9.2.2
version: 9.2.2(@dicebear/core@9.2.2)
'@dicebear/converter':
specifier: ^9.2.2
version: 9.2.2
'@dicebear/core':
specifier: ^9.2.2
version: 9.2.2
'@faker-js/faker': '@faker-js/faker':
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@ -162,6 +171,12 @@ importers:
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(svelte@5.16.0)(vite@6.0.6(@types/node@22.10.2)(jiti@1.21.7)(yaml@2.6.1)) version: 5.0.3(svelte@5.16.0)(vite@6.0.6(@types/node@22.10.2)(jiti@1.21.7)(yaml@2.6.1))
'@tanstack/svelte-query':
specifier: ^5.62.9
version: 5.62.9(svelte@5.16.0)
'@tanstack/svelte-query-devtools':
specifier: ^5.62.9
version: 5.62.9(@tanstack/svelte-query@5.62.9(svelte@5.16.0))(svelte@5.16.0)
'@types/cookie': '@types/cookie':
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -201,9 +216,6 @@ importers:
svelte-check: svelte-check:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.1.1(picomatch@4.0.2)(svelte@5.16.0)(typescript@5.7.2) version: 4.1.1(picomatch@4.0.2)(svelte@5.16.0)(typescript@5.7.2)
svelte-headless-table:
specifier: ^0.18.3
version: 0.18.3(svelte@5.16.0)
svelte-meta-tags: svelte-meta-tags:
specifier: ^4.0.4 specifier: ^4.0.4
version: 4.0.4(svelte@5.16.0)(typescript@5.7.2) version: 4.0.4(svelte@5.16.0)(typescript@5.7.2)
@ -342,6 +354,200 @@ packages:
peerDependencies: peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
'@dicebear/adventurer-neutral@9.2.2':
resolution: {integrity: sha512-XVAjhUWjav6luTZ7txz8zVJU/H0DiUy4uU1Z7IO5MDO6kWvum+If1+0OUgEWYZwM+RDI7rt2CgVP910DyZGd1w==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/adventurer@9.2.2':
resolution: {integrity: sha512-WjBXCP9EXbUul2zC3BS2/R3/4diw1uh/lU4jTEnujK1mhqwIwanFboIMzQsasNNL/xf+m3OHN7MUNJfHZ1fLZA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/avataaars-neutral@9.2.2':
resolution: {integrity: sha512-pRj16P27dFDBI3LtdiHUDwIXIGndHAbZf5AxaMkn6/+0X93mVQ/btVJDXyW0G96WCsyC88wKAWr6/KJotPxU6Q==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/avataaars@9.2.2':
resolution: {integrity: sha512-WqJPQEt0OhBybTpI0TqU1uD1pSk9M2+VPIwvBye/dXo46b+0jHGpftmxjQwk6tX8z0+mRko8pwV5n+cWht1/+w==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/big-ears-neutral@9.2.2':
resolution: {integrity: sha512-IPHt8fi3dv9cyfBJBZ4s8T+PhFCrQvOCf91iRHBT3iOLNPdyZpI5GNLmGiV0XMAvIDP5NvA5+f6wdoBLhYhbDA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/big-ears@9.2.2':
resolution: {integrity: sha512-hz4UXdPq4qqZpu0YVvlqM4RDFhk5i0WgPcuwj/MOLlgTjuj63uHUhCQSk6ZiW1DQOs12qpwUBMGWVHxBRBas9g==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/big-smile@9.2.2':
resolution: {integrity: sha512-D4td0GL8or1nTNnXvZqkEXlzyqzGPWs3znOnm1HIohtFTeIwXm72Ob2lNDsaQJSJvXmVlwaQQ0CCTvyCl8Stjw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/bottts-neutral@9.2.2':
resolution: {integrity: sha512-lSgpqmSJtlnyxVuUgNdBwyzuA0O9xa5zRJtz7x2KyWbicXir5iYdX0MVMCkp1EDvlcxm9rGJsclktugOyakTlw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/bottts@9.2.2':
resolution: {integrity: sha512-wugFkzw8JNWV1nftq/Wp/vmQsLAXDxrMtRK3AoMODuUpSVoP3EHRUfKS043xggOsQFvoj0HZ7kadmhn0AMLf5A==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/collection@9.2.2':
resolution: {integrity: sha512-vZAmXhPWCK3sf8Fj9/QflFC6XOLroJOT5K1HdnzHaPboEvffUQideGCrrEamnJtlH0iF0ZDXh8gqmwy2fu+yHA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/converter@9.2.2':
resolution: {integrity: sha512-704MOowKUOGI88tOJIYFSkd2HXbkNtPdjMrPTx7j+7gAqImCBZ298KVUk0TCo36VCUfN0EtgngUuBZUxyPTadQ==}
engines: {node: '>=18.0.0'}
'@dicebear/core@9.2.2':
resolution: {integrity: sha512-ROhgHG249dPtcXgBHcqPEsDeAPRPRD/9d+tZCjLYyueO+cXDlIA8dUlxpwIVcOuZFvCyW6RJtqo8BhNAi16pIQ==}
engines: {node: '>=18.0.0'}
'@dicebear/croodles-neutral@9.2.2':
resolution: {integrity: sha512-/4mNirxoQ+z1kHXnpDRbJ1JV1ZgXogeTeNp0MaFYxocCgHfJ7ckNM23EE1I7akoo9pqPxrKlaeNzGAjKHdS9vA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/croodles@9.2.2':
resolution: {integrity: sha512-OzvAXQWsOgMwL3Sl+lBxCubqSOWoBJpC78c4TKnNTS21rR63TtXUyVdLLzgKVN4YHRnvMgtPf8F/W9YAgIDK4w==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/dylan@9.2.2':
resolution: {integrity: sha512-s7e3XliC1YXP+Wykj+j5kwdOWFRXFzYHYk/PB4oZ1F3sJandXiG0HS4chaNu4EoP0yZgKyFMUVTGZx+o6tMaYg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/fun-emoji@9.2.2':
resolution: {integrity: sha512-M+rYTpB3lfwz18f+/i+ggNwNWUoEj58SJqXJ1wr7Jh/4E5uL+NmJg9JGwYNaVtGbCFrKAjSaILNUWGQSFgMfog==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/glass@9.2.2':
resolution: {integrity: sha512-imCMxcg+XScHYtQq2MUv1lCzhQSCUglMlPSezKEpXhTxgbgUpmGlSGVkOfmX5EEc7SQowKkF1W/1gNk6CXvBaQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/icons@9.2.2':
resolution: {integrity: sha512-Tqq2OVCdS7J02DNw58xwlgLGl40sWEckbqXT3qRvIF63FfVq+wQZBGuhuiyAURcSgvsc3h2oQeYFi9iXh7HTOA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/identicon@9.2.2':
resolution: {integrity: sha512-POVKFulIrcuZf3rdAgxYaSm2XUg/TJg3tg9zq9150reEGPpzWR7ijyJ03dzAADPzS3DExfdYVT9+z3JKwwJnTQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/initials@9.2.2':
resolution: {integrity: sha512-/xNnsEmsstWjmF77htAOuwOMhFlP6eBVXgcgFlTl/CCH/Oc6H7t0vwX1he8KLQBBzjGpvJcvIAn4Wh9rE4D5/A==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/lorelei-neutral@9.2.2':
resolution: {integrity: sha512-Eys9Os6nt2Xll7Mvu66CfRR2YggTopWcmFcRZ9pPdohS96kT0MsLI2iTcfZXQ51K8hvT3IbwoGc86W8n0cDxAQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/lorelei@9.2.2':
resolution: {integrity: sha512-koXqVr/vcWUPo00VP5H6Czsit+uF1tmwd2NK7Q/e34/9Sd1f4QLLxHjjBNm/iNjCI1+UNTOvZ2Qqu0N5eo7Flw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/micah@9.2.2':
resolution: {integrity: sha512-NCajcJV5yw8uMKiACp694w1T/UyYme2CUEzyTzWHgWnQ+drAuCcH8gpAoLWd67viNdQB/MTpNlaelUgTjmI4AQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/miniavs@9.2.2':
resolution: {integrity: sha512-vvkWXttdw+KHF3j+9qcUFzK+P0nbNnImGjvN48wwkPIh2h08WWFq0MnoOls4IHwUJC4GXBjWtiyVoCxz6hhtOA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/notionists-neutral@9.2.2':
resolution: {integrity: sha512-AhOzk+lz6kB4uxGun8AJhV+W1nttnMlxmxd+5KbQ/txCIziYIaeD3il44wsAGegEpGFvAZyMYtR/jjfHcem3TA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/notionists@9.2.2':
resolution: {integrity: sha512-Z9orRaHoj7Y9Ap4wEu8XOrFACsG1KbbBQUPV1R50uh6AHwsyNrm4cS84ICoGLvxgLNHHOae3YCjd8aMu2z19zg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/open-peeps@9.2.2':
resolution: {integrity: sha512-6PeQDHYyjvKrGSl/gP+RE5dSYAQGKpcGnM65HorgyTIugZK7STo0W4hvEycedupZ3MCCEH8x/XyiChKM2sHXog==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/personas@9.2.2':
resolution: {integrity: sha512-705+ObNLC0w1fcgE/Utav+8bqO+Esu53TXegpX5j7trGEoIMf2bThqJGHuhknZ3+T2az3Wr89cGyOGlI0KLzLA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/pixel-art-neutral@9.2.2':
resolution: {integrity: sha512-CdUY77H6Aj7dKLW3hdkv7tu0XQJArUjaWoXihQxlhl3oVYplWaoyu9omYy5pl8HTqs8YgVTGljjMXYoFuK0JUw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/pixel-art@9.2.2':
resolution: {integrity: sha512-BvbFdrpzQl04+Y9UsWP63YGug+ENGC7GMG88qbEFWxb/IqRavGa4H3D0T4Zl2PSLiw7f2Ctv98bsCQZ1PtCznQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/rings@9.2.2':
resolution: {integrity: sha512-eD1J1k364Arny+UlvGrk12HP/XGG6WxPSm4BarFqdJGSV45XOZlwqoi7FlcMr9r9yvE/nGL8OizbwMYusEEdjw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/shapes@9.2.2':
resolution: {integrity: sha512-e741NNWBa7fg0BjomxXa0fFPME2XCIR0FA+VHdq9AD2taTGHEPsg5x1QJhCRdK6ww85yeu3V3ucpZXdSrHVw5Q==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/thumbs@9.2.2':
resolution: {integrity: sha512-FkPLDNu7n5kThLSk7lR/0cz/NkUqgGdZGfLZv6fLkGNGtv6W+e2vZaO7HCXVwIgJ+II+kImN41zVIZ6Jlll7pQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@drizzle-team/brocli@0.10.2': '@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
@ -1349,6 +1555,9 @@ packages:
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@photostructure/tz-lookup@11.0.0':
resolution: {integrity: sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -1365,6 +1574,82 @@ packages:
resolution: {integrity: sha512-B4iV6QxW//Fn17+qF1EMZRmoThIUJlCtcO85yoRDJnMyHeAthjz4ig9OTkfGGXKtQhcdPX0me75gU5K9J897+w==} resolution: {integrity: sha512-B4iV6QxW//Fn17+qF1EMZRmoThIUJlCtcO85yoRDJnMyHeAthjz4ig9OTkfGGXKtQhcdPX0me75gU5K9J897+w==}
engines: {node: '>=18.16.0'} engines: {node: '>=18.16.0'}
'@resvg/resvg-js-android-arm-eabi@2.6.2':
resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
'@resvg/resvg-js-android-arm64@2.6.2':
resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@resvg/resvg-js-darwin-arm64@2.6.2':
resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@resvg/resvg-js-darwin-x64@2.6.2':
resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@resvg/resvg-js@2.6.2':
resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==}
engines: {node: '>= 10'}
'@rollup/plugin-commonjs@28.0.2': '@rollup/plugin-commonjs@28.0.2':
resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==} resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==}
engines: {node: '>=16.0.0 || 14 >= 14.17'} engines: {node: '>=16.0.0 || 14 >= 14.17'}
@ -1761,6 +2046,23 @@ packages:
peerDependencies: peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20'
'@tanstack/query-core@5.62.9':
resolution: {integrity: sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==}
'@tanstack/query-devtools@5.62.9':
resolution: {integrity: sha512-b1NZzDLVf6laJsB1Cfm3ieuYzM+WqoO8qpm9v+3Etwd+Ph4zkhUMiT+wcWj5AhEPsXiRodKYiiW048VDNdBxNg==}
'@tanstack/svelte-query-devtools@5.62.9':
resolution: {integrity: sha512-baESJCUDBIJIRiwfodW/j0BU8c5uADk37A0UfadOU8cWFt9M67zawKltJmut7AoEXbwLWYqTO+3HU2XKCyJEWw==}
peerDependencies:
'@tanstack/svelte-query': ^5.62.9
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0
'@tanstack/svelte-query@5.62.9':
resolution: {integrity: sha512-2M/CpePioU4IRw1OOdA+/KwA+0swJAb3c0uipFOoWkuT11uC4tTe8UD/lYRQpJYFFn5hML7KYRxVOW0HH60XiA==}
peerDependencies:
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0
'@testing-library/dom@10.4.0': '@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1800,6 +2102,9 @@ packages:
'@types/jsonwebtoken@9.0.7': '@types/jsonwebtoken@9.0.7':
resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==}
'@types/luxon@3.4.2':
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
'@types/mdx@2.0.13': '@types/mdx@2.0.13':
resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
@ -2027,6 +2332,10 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
batch-cluster@13.0.0:
resolution: {integrity: sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==}
engines: {node: '>=14'}
before-after-hook@2.2.3: before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
@ -2695,6 +3004,17 @@ packages:
eventemitter3@5.0.1: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
exiftool-vendored.exe@13.0.0:
resolution: {integrity: sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==}
os: [win32]
exiftool-vendored.pl@13.0.1:
resolution: {integrity: sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==}
os: ['!win32']
exiftool-vendored@28.8.0:
resolution: {integrity: sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==}
expect-type@1.1.0: expect-type@1.1.0:
resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -2898,6 +3218,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
help-me@5.0.0: help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
@ -3188,6 +3512,10 @@ packages:
peerDependencies: peerDependencies:
svelte: ^3 || ^4 || ^5.0.0-next.42 svelte: ^3 || ^4 || ^5.0.0-next.42
luxon@3.5.0:
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
engines: {node: '>=12'}
lz-string@1.5.0: lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true hasBin: true
@ -4018,16 +4346,6 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0 svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0' typescript: '>=5.0.0'
svelte-headless-table@0.18.3:
resolution: {integrity: sha512-1zVnqXW0dvn6ZceYa94k+ziK+w5Dj9nlWYTQGXBv2JhM0resj9w7CWpclZK1TJwAALfEeH4InPBPO87L5fr+nQ==}
peerDependencies:
svelte: ^4.0.0
svelte-keyed@2.0.0:
resolution: {integrity: sha512-7TeEn+QbJC2OJrHiuM0T8vMBkms3DNpTE+Ir+NtnVBnBMA78aL4f1ft9t0Hn/pBbD/TnIXi4YfjFRAgtN+DZ5g==}
peerDependencies:
svelte: ^4.0.0
svelte-meta-tags@4.0.4: svelte-meta-tags@4.0.4:
resolution: {integrity: sha512-i0vgxGreM3lXTTxLSuPQLE1n56KAFACHWIXRj7fJCTpd/5D16O97Ha/OXDZS4Lsk+D347VEK4LeMoacsftbeKw==} resolution: {integrity: sha512-i0vgxGreM3lXTTxLSuPQLE1n56KAFACHWIXRj7fJCTpd/5D16O97Ha/OXDZS4Lsk+D347VEK4LeMoacsftbeKw==}
peerDependencies: peerDependencies:
@ -4112,11 +4430,6 @@ packages:
typescript: typescript:
optional: true optional: true
svelte-render@2.0.1:
resolution: {integrity: sha512-RpB0SurwXm4xhjvHHtjeqMmvd645FURb79GFOotScOSqnKK5vpqBgoBPGC0pp+E/eZgDSQ9rRAdn/+N4ys1mXQ==}
peerDependencies:
svelte: ^4.0.0
svelte-sequential-preprocessor@2.0.2: svelte-sequential-preprocessor@2.0.2:
resolution: {integrity: sha512-DIFm0kSNscVxtBmKkBiygAHB5otoqN1aVmJ3t57jZhJfCB7Np/lUSoTtSrvPFjmlBbMeOsb1VQ06cut1+rBYOg==} resolution: {integrity: sha512-DIFm0kSNscVxtBmKkBiygAHB5otoqN1aVmJ3t57jZhJfCB7Np/lUSoTtSrvPFjmlBbMeOsb1VQ06cut1+rBYOg==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -4126,11 +4439,6 @@ packages:
peerDependencies: peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1 svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
svelte-subscribe@2.0.1:
resolution: {integrity: sha512-eKXIjLxB4C7eQWPqKEdxcGfNXm2g/qJ67zmEZK/GigCZMfrTR3m7DPY93R6MX+5uoqM1FRYxl8LZ1oy4URWi2A==}
peerDependencies:
svelte: ^4.0.0
svelte-toolbelt@0.4.6: svelte-toolbelt@0.4.6:
resolution: {integrity: sha512-k8OUvXBUifHZcAlWeY/HLg/4J0v5m2iOfOhn8fDmjt4AP8ZluaDh9eBFus9lFiLX6O5l6vKqI1dKL5wy7090NQ==} resolution: {integrity: sha512-k8OUvXBUifHZcAlWeY/HLg/4J0v5m2iOfOhn8fDmjt4AP8ZluaDh9eBFus9lFiLX6O5l6vKqI1dKL5wy7090NQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'} engines: {node: '>=18', pnpm: '>=8.7.0'}
@ -4239,6 +4547,13 @@ packages:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
tmp@0.2.3:
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
engines: {node: '>=14.14'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -4656,6 +4971,171 @@ snapshots:
- '@chromatic-com/playwright' - '@chromatic-com/playwright'
- react - react
'@dicebear/adventurer-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/adventurer@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/avataaars-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/avataaars@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/big-ears-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/big-ears@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/big-smile@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/bottts-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/bottts@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/collection@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/adventurer': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/adventurer-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/avataaars': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/avataaars-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/big-ears': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/big-ears-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/big-smile': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/bottts': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/bottts-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/core': 9.2.2
'@dicebear/croodles': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/croodles-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/dylan': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/fun-emoji': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/glass': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/icons': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/identicon': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/initials': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/lorelei': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/lorelei-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/micah': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/miniavs': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/notionists': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/notionists-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/open-peeps': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/personas': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/pixel-art': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/pixel-art-neutral': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/rings': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/shapes': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/thumbs': 9.2.2(@dicebear/core@9.2.2)
'@dicebear/converter@9.2.2':
dependencies:
'@resvg/resvg-js': 2.6.2
exiftool-vendored: 28.8.0
sharp: 0.33.5
tmp-promise: 3.0.3
'@dicebear/core@9.2.2':
dependencies:
'@types/json-schema': 7.0.15
'@dicebear/croodles-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/croodles@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/dylan@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/fun-emoji@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/glass@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/icons@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/identicon@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/initials@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/lorelei-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/lorelei@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/micah@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/miniavs@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/notionists-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/notionists@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/open-peeps@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/personas@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/pixel-art-neutral@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/pixel-art@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/rings@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/shapes@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@dicebear/thumbs@9.2.2(@dicebear/core@9.2.2)':
dependencies:
'@dicebear/core': 9.2.2
'@drizzle-team/brocli@0.10.2': {} '@drizzle-team/brocli@0.10.2': {}
'@emnapi/runtime@1.3.1': '@emnapi/runtime@1.3.1':
@ -5505,6 +5985,8 @@ snapshots:
'@phc/format@1.0.0': {} '@phc/format@1.0.0': {}
'@photostructure/tz-lookup@11.0.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -5517,6 +5999,57 @@ snapshots:
'@poppinss/macroable@1.0.3': '@poppinss/macroable@1.0.3':
optional: true optional: true
'@resvg/resvg-js-android-arm-eabi@2.6.2':
optional: true
'@resvg/resvg-js-android-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-x64@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-musl@2.6.2':
optional: true
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
optional: true
'@resvg/resvg-js@2.6.2':
optionalDependencies:
'@resvg/resvg-js-android-arm-eabi': 2.6.2
'@resvg/resvg-js-android-arm64': 2.6.2
'@resvg/resvg-js-darwin-arm64': 2.6.2
'@resvg/resvg-js-darwin-x64': 2.6.2
'@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2
'@resvg/resvg-js-linux-arm64-gnu': 2.6.2
'@resvg/resvg-js-linux-arm64-musl': 2.6.2
'@resvg/resvg-js-linux-x64-gnu': 2.6.2
'@resvg/resvg-js-linux-x64-musl': 2.6.2
'@resvg/resvg-js-win32-arm64-msvc': 2.6.2
'@resvg/resvg-js-win32-ia32-msvc': 2.6.2
'@resvg/resvg-js-win32-x64-msvc': 2.6.2
'@rollup/plugin-commonjs@28.0.2(rollup@4.29.1)': '@rollup/plugin-commonjs@28.0.2(rollup@4.29.1)':
dependencies: dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.29.1) '@rollup/pluginutils': 5.1.4(rollup@4.29.1)
@ -5993,6 +6526,22 @@ snapshots:
postcss-selector-parser: 6.0.10 postcss-selector-parser: 6.0.10
tailwindcss: 3.4.17 tailwindcss: 3.4.17
'@tanstack/query-core@5.62.9': {}
'@tanstack/query-devtools@5.62.9': {}
'@tanstack/svelte-query-devtools@5.62.9(@tanstack/svelte-query@5.62.9(svelte@5.16.0))(svelte@5.16.0)':
dependencies:
'@tanstack/query-devtools': 5.62.9
'@tanstack/svelte-query': 5.62.9(svelte@5.16.0)
esm-env: 1.2.1
svelte: 5.16.0
'@tanstack/svelte-query@5.62.9(svelte@5.16.0)':
dependencies:
'@tanstack/query-core': 5.62.9
svelte: 5.16.0
'@testing-library/dom@10.4.0': '@testing-library/dom@10.4.0':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@ -6032,13 +6581,14 @@ snapshots:
'@types/estree@1.0.6': {} '@types/estree@1.0.6': {}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15': {}
optional: true
'@types/jsonwebtoken@9.0.7': '@types/jsonwebtoken@9.0.7':
dependencies: dependencies:
'@types/node': 22.10.2 '@types/node': 22.10.2
'@types/luxon@3.4.2': {}
'@types/mdx@2.0.13': {} '@types/mdx@2.0.13': {}
'@types/node@22.10.2': '@types/node@22.10.2':
@ -6287,6 +6837,8 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
batch-cluster@13.0.0: {}
before-after-hook@2.2.3: {} before-after-hook@2.2.3: {}
better-opn@3.0.2: better-opn@3.0.2:
@ -6922,6 +7474,23 @@ snapshots:
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
exiftool-vendored.exe@13.0.0:
optional: true
exiftool-vendored.pl@13.0.1:
optional: true
exiftool-vendored@28.8.0:
dependencies:
'@photostructure/tz-lookup': 11.0.0
'@types/luxon': 3.4.2
batch-cluster: 13.0.0
he: 1.2.0
luxon: 3.5.0
optionalDependencies:
exiftool-vendored.exe: 13.0.0
exiftool-vendored.pl: 13.0.1
expect-type@1.1.0: {} expect-type@1.1.0: {}
express-rate-limit@7.5.0(express@4.21.2): express-rate-limit@7.5.0(express@4.21.2):
@ -7147,6 +7716,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
he@1.2.0: {}
help-me@5.0.0: {} help-me@5.0.0: {}
hono-pino@0.7.0(hono@4.6.14)(pino@9.6.0): hono-pino@0.7.0(hono@4.6.14)(pino@9.6.0):
@ -7419,6 +7990,8 @@ snapshots:
dependencies: dependencies:
svelte: 5.16.0 svelte: 5.16.0
luxon@3.5.0: {}
lz-string@1.5.0: {} lz-string@1.5.0: {}
magic-string@0.30.17: magic-string@0.30.17:
@ -8270,17 +8843,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- picomatch - picomatch
svelte-headless-table@0.18.3(svelte@5.16.0):
dependencies:
svelte: 5.16.0
svelte-keyed: 2.0.0(svelte@5.16.0)
svelte-render: 2.0.1(svelte@5.16.0)
svelte-subscribe: 2.0.1(svelte@5.16.0)
svelte-keyed@2.0.0(svelte@5.16.0):
dependencies:
svelte: 5.16.0
svelte-meta-tags@4.0.4(svelte@5.16.0)(typescript@5.7.2): svelte-meta-tags@4.0.4(svelte@5.16.0)(typescript@5.7.2):
dependencies: dependencies:
schema-dts: 1.1.2(typescript@5.7.2) schema-dts: 1.1.2(typescript@5.7.2)
@ -8313,11 +8875,6 @@ snapshots:
postcss-load-config: 4.0.2(postcss@8.4.49) postcss-load-config: 4.0.2(postcss@8.4.49)
typescript: 5.7.2 typescript: 5.7.2
svelte-render@2.0.1(svelte@5.16.0):
dependencies:
svelte: 5.16.0
svelte-subscribe: 2.0.1(svelte@5.16.0)
svelte-sequential-preprocessor@2.0.2: svelte-sequential-preprocessor@2.0.2:
dependencies: dependencies:
svelte: 4.2.19 svelte: 4.2.19
@ -8327,10 +8884,6 @@ snapshots:
dependencies: dependencies:
svelte: 5.16.0 svelte: 5.16.0
svelte-subscribe@2.0.1(svelte@5.16.0):
dependencies:
svelte: 5.16.0
svelte-toolbelt@0.4.6(svelte@5.16.0): svelte-toolbelt@0.4.6(svelte@5.16.0):
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
@ -8502,6 +9055,12 @@ snapshots:
tinyspy@3.0.2: {} tinyspy@3.0.2: {}
tmp-promise@3.0.3:
dependencies:
tmp: 0.2.3
tmp@0.2.3: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0

View file

@ -0,0 +1 @@
eb7d81211e6faa5e843476a33150228f5db02e3c62748d6eff4e45b709691d18

View file

@ -4,5 +4,7 @@ import { sequence } from '@sveltejs/kit/hooks';
import { startServer } from '$lib/server/api'; import { startServer } from '$lib/server/api';
const handleParaglide: Handle = i18n.handle(); const handleParaglide: Handle = i18n.handle();
startServer(); startServer();
export const handle: Handle = sequence(handleParaglide); export const handle: Handle = sequence(handleParaglide);

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("p-6", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}>
{@render children?.()}
</p>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
level?: 1 | 2 | 3 | 4 | 5 | 6;
} = $props();
</script>
<div
role="heading"
aria-level={level}
bind:this={ref}
class={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,22 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
};

View file

@ -0,0 +1,28 @@
import Root from "./table.svelte";
import Body from "./table-body.svelte";
import Caption from "./table-caption.svelte";
import Cell from "./table-cell.svelte";
import Footer from "./table-footer.svelte";
import Head from "./table-head.svelte";
import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tbody bind:this={ref} class={cn("[&_tr:last-child]:border-0", className)} {...restProps}>
{@render children?.()}
</tbody>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<caption bind:this={ref} class={cn("text-muted-foreground mt-4 text-sm", className)} {...restProps}>
{@render children?.()}
</caption>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLTdAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTdAttributes> = $props();
</script>
<td
bind:this={ref}
class={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...restProps}
>
{@render children?.()}
</td>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tfoot bind:this={ref} class={cn("bg-muted/50 font-medium", className)} {...restProps}>
{@render children?.()}
</tfoot>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLThAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLThAttributes> = $props();
</script>
<th
bind:this={ref}
class={cn(
"text-muted-foreground h-12 px-4 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0",
className
)}
{...restProps}
>
{@render children?.()}
</th>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<thead bind:this={ref} class={cn("[&_tr]:border-b", className)} {...restProps}>
{@render children?.()}
</thead>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
</script>
<tr
bind:this={ref}
class={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...restProps}
>
{@render children?.()}
</tr>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import type { HTMLTableAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTableAttributes> = $props();
</script>
<div class="relative w-full overflow-auto">
<table bind:this={ref} class={cn("w-full caption-bottom text-sm", className)} {...restProps}>
{@render children?.()}
</table>
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts" module>
import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<HTMLImageElement> {
user: {
id: string;
avatar: string | null;
};
}
</script>
<script lang="ts">
import { createAvatar } from '@dicebear/core';
import { funEmoji } from '@dicebear/collection';
const { user, ...props }: Props = $props();
const avatar =
user.avatar ||
createAvatar(funEmoji, {
seed: user.id
}).toDataUri();
</script>
<img {...props} src={avatar} />

View file

@ -1 +1,3 @@
export * from '../../roles/tables/roles.table';
export * from '../../users/tables/user-roles.table';
export * from '../../users/tables/users.table'; export * from '../../users/tables/users.table';

View file

@ -0,0 +1,107 @@
import { requireFullAuth, requireTempAuth } from '$lib/server/api/common/middleware/require-auth.middleware';
import { Controller } from '$lib/server/api/common/types/controller';
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto';
import { TotpService } from '$lib/server/api/mfa/totp.service';
import { RecoveryCodesService } from '$lib/server/api/users/recovery-codes.service';
import { UsersService } from '$lib/server/api/users/users.service';
import { StatusCodes } from '$lib/utils/status-codes';
import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from '@needle-di/core';
import { decodeBase64 } from '@oslojs/encoding';
import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '../common/utils/cookies';
import { CredentialsType } from '../databases/postgres/tables';
import { createTwoFactorSchema } from '../dtos/create-totp.dto';
import { LoginRequestsService } from '../login/loginrequest.service';
@injectable()
export class MfaController extends Controller {
constructor(
private loginRequestService = inject(LoginRequestsService),
private recoveryCodesService = inject(RecoveryCodesService),
private totpService = inject(TotpService),
private usersService = inject(UsersService),
) {
super();
}
routes() {
return this.controller
.get('/totp', requireTempAuth, async (c) => {
const user = c.var.user;
c.var.logger.info(`The user ${user.id} is requesting TOTP credentials`);
const totpCredential = await this.totpService.findOneByUserId(user.id);
return c.json({ totpCredential });
})
.post('/totp', requireTempAuth, zValidator('json', createTwoFactorSchema), async (c) => {
const user = c.var.user;
const { key } = c.req.valid('json');
const totpCredential = await this.totpService.create(user.id, decodeBase64(key));
if (totpCredential) {
await this.usersService.updateUser(user.id, { mfa_enabled: true });
return c.json({ totpCredential });
}
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
})
.delete('/totp', requireFullAuth, async (c) => {
const user = c.var.user;
try {
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP);
await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id);
await this.usersService.updateUser(user.id, { mfa_enabled: false });
console.log('TOTP deleted');
return c.body(null, StatusCodes.NO_CONTENT);
} catch (e) {
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
.get('/totp/recoveryCodes', requireFullAuth, async (c) => {
const user = c.var.user;
// You can only view recovery codes once and that is on creation
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id);
if (existingCodes && existingCodes.length > 0) {
console.log('Recovery Codes found', existingCodes);
// Filter out codes that are not used and only return the code
const codes = existingCodes.filter((code) => !code.used).map((code) => code.code);
return c.json({ recoveryCodes: codes });
}
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id);
return c.json({ recoveryCodes });
})
.post('/totp/recoveryCodes', requireFullAuth, zValidator('json', verifyTotpDto), async (c) => {
try {
const user = c.var.user;
const { code } = c.req.valid('json');
c.var.logger.info(`Verifying code ${code} for user ${user.id}`);
const verified = await this.recoveryCodesService.verify(user.id, code);
if (verified) {
return c.json({}, StatusCodes.OK);
}
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
} catch (e) {
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
.post('/totp/verify', requireTempAuth, zValidator('json', verifyTotpDto), async (c) => {
try {
const user = c.var.user;
const { code } = c.req.valid('json');
c.var.logger.info(`Verifying code ${code} for user ${user.id}`);
const verified = await this.totpService.verify(user.id, code);
if (verified) {
await this.usersService.updateUser(user.id, { mfa_enabled: true });
const session = await this.loginRequestService.createUserSession(user.id, c.req, true, true);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
console.log('set cookie', sessionCookie);
setSessionCookie(c, sessionCookie);
return c.json({}, StatusCodes.OK);
}
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
} catch (e) {
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
});
}
}

View file

@ -0,0 +1,36 @@
import { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { id, timestamps } from '../../common/utils/drizzle';
import { usersTable } from '../../users/tables/users.table';
import { generateId } from '../../common/utils/crypto';
/* -------------------------------------------------------------------------- */
/* Table */
/* -------------------------------------------------------------------------- */
export const twoFactorTable = pgTable('two_factor', {
id: id()
.primaryKey()
.$defaultFn(() => generateId()),
secret: text().notNull(),
enabled: boolean().notNull().default(false),
user_id: id()
.notNull()
.references(() => usersTable.id)
.unique('two_factor_user_id_unique'),
...timestamps,
});
/* -------------------------------------------------------------------------- */
/* Relations */
/* -------------------------------------------------------------------------- */
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
user: one(usersTable, {
fields: [twoFactorTable.user_id],
references: [usersTable.id],
}),
}));
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export type TwoFactor = InferSelectModel<typeof twoFactorTable>;

View file

@ -0,0 +1,57 @@
import { inject, injectable } from '@needle-di/core';
import { decodeBase64, encodeBase64 } from '@oslojs/encoding';
import { generateTOTP, verifyTOTP } from '@oslojs/otp';
import { EncryptionService } from '../common/services/encryption.service';
import type { CredentialsType } from '../databases/postgres/tables';
import { CredentialsRepository } from '../users/credentials.repository';
@injectable()
export class TotpService {
constructor(
private credentialsRepository = inject(CredentialsRepository),
private encryptionService = inject(EncryptionService),
) {}
async findOneByUserId(userId: string) {
return this.credentialsRepository.findTOTPCredentialsByUserId(userId);
}
async findOneByUserIdOrThrow(userId: string) {
const credential = await this.findOneByUserId(userId);
if (!credential) {
throw new Error('TOTP credential not found');
}
return credential;
}
async create(userId: string, key: Uint8Array) {
try {
return await this.credentialsRepository.create({
user_id: userId,
secret_data: encodeBase64(this.encryptionService.encrypt(key)),
type: 'totp',
});
} catch (e) {
console.error(e);
return null;
}
}
async deleteOneByUserId(userId: string) {
return this.credentialsRepository.deleteByUserId(userId);
}
async deleteOneByUserIdAndType(userId: string, type: CredentialsType) {
return this.credentialsRepository.deleteByUserIdAndType(userId, type);
}
async verify(userId: string, code: string) {
const credential = await this.credentialsRepository.findTOTPCredentialsByUserId(userId);
console.log(`TOTP credential: ${JSON.stringify(credential)}`);
if (!credential) {
throw new Error('TOTP credential not found');
}
const secret = this.encryptionService.decrypt(decodeBase64(credential.secret_data));
return verifyTOTP(secret, 30, 6, code);
}
}

View file

@ -0,0 +1,38 @@
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text } from 'drizzle-orm/pg-core';
import { user_roles } from '../../users/tables/user-roles.table';
import { timestamps } from '../../common/utils/drizzle';
import { id } from '../../common/utils/drizzle';
import { generateId } from '../../common/utils/crypto';
export enum RoleName {
ADMIN = 'admin',
EDITOR = 'editor',
MODERATOR = 'moderator',
USER = 'user',
}
/* -------------------------------------------------------------------------- */
/* Table */
/* -------------------------------------------------------------------------- */
export const rolesTable = pgTable('roles', {
id: id()
.primaryKey()
.$defaultFn(() => generateId()),
name: text().unique().notNull(),
...timestamps,
});
/* -------------------------------------------------------------------------- */
/* Relations */
/* -------------------------------------------------------------------------- */
export const role_relations = relations(rolesTable, ({ many }) => ({
user_roles: many(user_roles),
}));
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export type Roles = InferSelectModel<typeof rolesTable>;

View file

@ -0,0 +1,29 @@
import type { InferSelectModel } from 'drizzle-orm';
import { pgTable, text } from 'drizzle-orm/pg-core';
import { id, timestamps } from '../../common/utils/drizzle';
import { usersTable } from './users.table';
import { generateId } from '../../common/utils/crypto';
/* -------------------------------------------------------------------------- */
/* Table */
/* -------------------------------------------------------------------------- */
export enum CredentialsType {
SECRET = 'secret',
PASSWORD = 'password',
TOTP = 'totp',
HOTP = 'hotp',
}
export const credentialsTable = pgTable('credentials', {
id: id()
.primaryKey()
.$defaultFn(() => generateId()),
user_id: id()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
type: text().notNull().default(CredentialsType.PASSWORD),
secret_data: text().notNull(),
...timestamps,
});
export type Credentials = InferSelectModel<typeof credentialsTable>;

View file

@ -0,0 +1,50 @@
import { type InferSelectModel, relations, getTableColumns } from 'drizzle-orm';
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { rolesTable } from '../../roles/tables/roles.table';
import { usersTable } from './users.table';
import { generateId } from '../../common/utils/crypto';
import { id, timestamps } from '../../common/utils/drizzle';
/* -------------------------------------------------------------------------- */
/* Table */
/* -------------------------------------------------------------------------- */
export const user_roles = pgTable('user_roles', {
id: id()
.primaryKey()
.$defaultFn(() => generateId()),
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
role_id: uuid()
.notNull()
.references(() => rolesTable.id, { onDelete: 'cascade' }),
primary: boolean().default(false),
...timestamps,
});
/* -------------------------------------------------------------------------- */
/* Relations */
/* -------------------------------------------------------------------------- */
export const user_role_relations = relations(user_roles, ({ one }) => ({
role: one(rolesTable, {
fields: [user_roles.role_id],
references: [rolesTable.id],
}),
user: one(usersTable, {
fields: [user_roles.user_id],
references: [usersTable.id],
}),
}));
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export type UserRoles = InferSelectModel<typeof user_roles>;
export type UserRolesWithRelations = UserRoles & {};
const userRolesColumns = getTableColumns(user_roles);
export const publicUserColumns = {
id: userRolesColumns.id,
...timestamps,
};

View file

@ -1,7 +1,8 @@
import { pgTable, text } from 'drizzle-orm/pg-core'; import { boolean, pgTable, text } from 'drizzle-orm/pg-core';
import { getTableColumns, type InferSelectModel, relations } from 'drizzle-orm'; import { getTableColumns, type InferSelectModel, relations } from 'drizzle-orm';
import { citext, id, timestamps } from '../../common/utils/drizzle'; import { citext, id, timestamps } from '../../common/utils/drizzle';
import { generateId } from '../../common/utils/crypto'; import { generateId } from '../../common/utils/crypto';
import { user_roles } from './user-roles.table';
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Table */ /* Table */
@ -10,15 +11,22 @@ export const usersTable = pgTable('users', {
id: id() id: id()
.primaryKey() .primaryKey()
.$defaultFn(() => generateId()), .$defaultFn(() => generateId()),
username: text().unique(),
email: citext().unique().notNull(), email: citext().unique().notNull(),
first_name: text(),
last_name: text(),
email_verified: boolean().default(false),
mfa_enabled: boolean().notNull().default(false),
avatar: text(), avatar: text(),
...timestamps ...timestamps,
}); });
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Relations */ /* Relations */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
export const usersRelations = relations(usersTable, () => ({})); export const userRelations = relations(usersTable, ({ many }) => ({
user_roles: many(user_roles),
}));
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Types */ /* Types */
@ -30,7 +38,10 @@ const userColumns = getTableColumns(usersTable);
export const publicUserColumns = { export const publicUserColumns = {
id: userColumns.id, id: userColumns.id,
username: userColumns.username,
email: userColumns.email, email: userColumns.email,
avatar: userColumns.avatar, avatar: userColumns.avatar,
...timestamps first_name: userColumns.first_name,
last_name: userColumns.last_name,
...timestamps,
}; };

View file

@ -0,0 +1,34 @@
import type { InferRequestType } from 'hono';
import { parseApiResponse } from '$lib/utils/api';
import type { Api, ApiMutation } from '$lib/utils/types';
import { TanstackQueryModule } from './query-module';
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
type RequestLogin = Api['iam']['login']['request']['$post'];
type VerifyLogin = Api['iam']['login']['verify']['$post'];
type Logout = Api['iam']['logout']['$post'];
/* -------------------------------------------------------------------------- */
/* Api */
/* -------------------------------------------------------------------------- */
export class IamModule extends TanstackQueryModule<'iam'> {
logout(): ApiMutation<Logout> {
return {
mutationFn: async () => await this.api.iam.logout.$post().then(parseApiResponse)
};
}
requestLogin(): ApiMutation<RequestLogin> {
return {
mutationFn: async (data: InferRequestType<RequestLogin>) =>
await this.api.iam.login.request.$post(data).then(parseApiResponse)
};
}
verifyLogin(): ApiMutation<VerifyLogin> {
return {
mutationFn: async (data: InferRequestType<VerifyLogin>) =>
await this.api.iam.login.verify.$post(data).then(parseApiResponse)
};
}
}

View file

@ -0,0 +1,11 @@
import { IamModule } from './iam';
import type { ClientRequestOptions } from 'hono';
import { UsersModule } from './users';
import { TanstackQueryModule } from './query-module';
class TanstackQueryHandler extends TanstackQueryModule {
iam = new IamModule(this.opts);
users = new UsersModule(this.opts);
}
export const queryHandler = (opts?: ClientRequestOptions) => new TanstackQueryHandler(opts);

View file

@ -0,0 +1,13 @@
import type { ClientRequestOptions } from 'hono';
import { api } from '$lib/utils/api';
export abstract class TanstackQueryModule<T extends string | null = null> {
protected readonly opts: ClientRequestOptions | undefined;
protected readonly api: ReturnType<typeof api>;
public namespace: T | null = null;
constructor(opts?: ClientRequestOptions) {
this.opts = opts;
this.api = api(opts);
}
}

View file

@ -0,0 +1,37 @@
import { parseApiResponse } from '$lib/utils/api';
import type { Api, ApiMutation, ApiQuery } from '$lib/utils/types';
import type { InferRequestType } from 'hono';
import { TanstackQueryModule } from './query-module';
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
type Me = Api['users']['me']['$get'];
type UpdateEmailRequest = Api['users']['me']['email']['request']['$post'];
type VerifyEmailRequest = Api['users']['me']['email']['verify']['$post'];
/* -------------------------------------------------------------------------- */
/* Api */
/* -------------------------------------------------------------------------- */
export class UsersModule extends TanstackQueryModule<'users'> {
me(): ApiQuery<Me> {
return {
queryKey: [this.namespace, 'me'],
queryFn: async () => await this.api.users.me.$get().then(parseApiResponse)
};
}
updateEmailRequest(): ApiMutation<UpdateEmailRequest> {
return {
mutationFn: async (args: InferRequestType<UpdateEmailRequest>) =>
await this.api.users.me.email.request.$post(args).then(parseApiResponse)
};
}
verifyEmailRequest(): ApiMutation<VerifyEmailRequest> {
return {
mutationFn: async (args: InferRequestType<VerifyEmailRequest>) =>
await this.api.users.me.email.verify.$post(args).then(parseApiResponse)
};
}
}

View file

@ -6,9 +6,28 @@
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import * as Sheet from '$lib/components/ui/sheet/index.js'; import * as Sheet from '$lib/components/ui/sheet/index.js';
import ThemeDropdown from '@/components/theme-dropdown.svelte'; import { createMutation } from '@tanstack/svelte-query';
import { authContext } from '$lib/hooks/session.svelte.js';
import { queryHandler } from '$lib/tanstack-query/index.js';
import { goto, invalidateAll } from '$app/navigation';
import UserAvatar from '$lib/components/user-avatar.svelte';
import ThemeDropdown from '$lib/components/theme-dropdown.svelte';
const { children, data } = $props(); const { children, data } = $props();
$effect.pre(() => {
authContext.setAuthedUser(data.authedUser);
});
const logoutMutation = createMutation({
...queryHandler().iam.logout(),
onSuccess: async () => {
await data.queryClient.invalidateQueries();
invalidateAll();
goto('/login');
}
});
queryHandler;
</script> </script>
<div class="flex min-h-screen w-full flex-col"> <div class="flex min-h-screen w-full flex-col">
@ -71,7 +90,7 @@
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<Button variant="secondary" size="icon" class="rounded-lg"> <Button variant="secondary" size="icon" class="rounded-lg">
<!-- <UserAvatar class="h-8 w-8 rounded-lg" user={data.authedUser} /> --> <UserAvatar class="h-8 w-8 rounded-lg" user={data.authedUser} />
<span class="sr-only">Toggle user menu</span> <span class="sr-only">Toggle user menu</span>
</Button> </Button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
@ -82,7 +101,7 @@
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item >Logout</DropdownMenu.Item> <DropdownMenu.Item >Logout</DropdownMenu.Item>
<!-- onclick={$logoutMutation.mutate} --> onclick={$logoutMutation.mutate}
</DropdownMenu.Group> </DropdownMenu.Group>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>

View file

@ -0,0 +1,7 @@
import { queryHandler } from '$lib/tanstack-query';
export const load = async ({ parent, fetch }) => {
const { queryClient } = await parent();
const authedUser = await queryClient.fetchQuery(queryHandler({ fetch }).users.me());
return { authedUser };
};

View file

@ -1,14 +1,20 @@
<script lang="ts"> <script lang="ts">
import { i18n } from "$lib/i18n"; import { i18n } from "$lib/i18n";
import { QueryClientProvider } from "@tanstack/svelte-query";
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
import { ParaglideJS } from "@inlang/paraglide-sveltekit"; import { ParaglideJS } from "@inlang/paraglide-sveltekit";
import { ModeWatcher } from "mode-watcher"; import { ModeWatcher } from "mode-watcher";
import '../app.css'; import "../app.css";
let { children } = $props();
let { data, children } = $props();
</script> </script>
<QueryClientProvider client={data.queryClient}>
<ParaglideJS {i18n}> <ParaglideJS {i18n}>
<ModeWatcher /> <ModeWatcher />
<main> <main>
{@render children()} {@render children()}
</main> </main>
<SvelteQueryDevtools />
</ParaglideJS> </ParaglideJS>
</QueryClientProvider>

15
src/routes/+layout.ts Normal file
View file

@ -0,0 +1,15 @@
import { browser } from '$app/environment';
import { QueryClient } from '@tanstack/svelte-query';
export const load = async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
enabled: browser,
staleTime: 60 * 1000
}
}
});
return { queryClient };
};

View file

@ -1 +0,0 @@
<a href="/demo/paraglide">paraglide</a>

View file

@ -1,21 +0,0 @@
<script lang="ts">
import type { AvailableLanguageTag } from '$lib/paraglide/runtime';
import { i18n } from '$lib/i18n';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import * as m from '$lib/paraglide/messages.js';
function switchToLanguage(newLanguage: AvailableLanguageTag) {
const canonicalPath = i18n.route($page.url.pathname);
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
goto(localisedPath);
}
</script>
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>
<div>
<button onclick={() => switchToLanguage('en')}>en</button>
<button onclick={() => switchToLanguage('es')}>es</button>
</div>