Compare commits
29 Commits
cba49b734c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29fbb6c0d0 | ||
| 921560d14d | |||
| 8e02f673ca | |||
| 6bd3b76782 | |||
|
|
80947f7f37 | ||
| 7f0652bb6f | |||
| dbb44715ab | |||
| ee385585d8 | |||
| 13d0e7ee38 | |||
|
|
b7da567342 | ||
| 8db977c6f0 | |||
|
|
e8f6e0c6d3 | ||
| c434f4f86a | |||
| 693fc9cfe2 | |||
| bb1c537246 | |||
| 97fe97add1 | |||
| f86fc48841 | |||
| 670edf6744 | |||
| ceb530f5b8 | |||
| 3c92571fcb | |||
| 16ee74ea92 | |||
| b99f13a53c | |||
| b24aecf252 | |||
| 4cd586dee5 | |||
| 3b3ea92c8d | |||
| 2ee5a9f012 | |||
| 89d1bb1a4f | |||
| a8d06f5342 | |||
| 75d70f0db5 |
3
.dockingore
Normal file
3
.dockingore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
./deploy/
|
||||
./README.md
|
||||
@@ -11,3 +11,10 @@ BETTER_AUTH_SECRET=""
|
||||
# https://www.better-auth.com/docs/authentication/github
|
||||
GITHUB_CLIENT_ID=""
|
||||
GITHUB_CLIENT_SECRET=""
|
||||
|
||||
# GitHub App
|
||||
GITHUB_APP_ID=""
|
||||
GITHUB_APP_SLUG=""
|
||||
GITHUB_APP_SETUP_URL="http://localhost:5173/api/github/install/callback"
|
||||
# PEM private key (single quoted line, \n for line breaks)
|
||||
# GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
@@ -53,7 +53,8 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
DATABASE_URL=${{ secrets.DATABASE_URL }}
|
||||
DATABASE_URL="posgres://no:account@nowhere/db"
|
||||
ORIGIN="https://taskarr.milasholsting.dk"
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -94,9 +95,9 @@ jobs:
|
||||
|
||||
# 2. Update the manifest
|
||||
cd deploy/overlays/production
|
||||
kustomize edit set image election=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG
|
||||
kustomize edit set image main=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG
|
||||
|
||||
kustomize edit set image election-migration=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-migrator:$IMAGE_TAG
|
||||
kustomize edit set image migrator=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-migrator:$IMAGE_TAG
|
||||
- name: Commit and Push Change
|
||||
run: |
|
||||
# 1. Set identity to fix the "Author identity unknown" error
|
||||
@@ -111,5 +112,5 @@ jobs:
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore(deploy): update image to ${{ steps.meta.outputs.version }}"
|
||||
git push origin master
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@@ -0,0 +1,2 @@
|
||||
bun check
|
||||
bun db:generate
|
||||
@@ -14,6 +14,9 @@ WORKDIR /app
|
||||
ARG DATABASE_URL
|
||||
ENV DATABASE_URL=$DATABASE_URL
|
||||
|
||||
ARG ORIGIN
|
||||
ENV ORIGIN=$ORIGIN
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
|
||||
97
bun.lock
97
bun.lock
@@ -4,6 +4,10 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "taskarr-mgr",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"pg": "^8.20.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "~1.4.21",
|
||||
"@eslint/compat": "^2.0.4",
|
||||
@@ -15,6 +19,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^22",
|
||||
"@types/pg": "^8.20.0",
|
||||
"better-auth": "~1.4.21",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
@@ -22,8 +27,8 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"mdsvex": "^0.12.7",
|
||||
"postgres": "^3.4.9",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
@@ -273,12 +278,72 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="],
|
||||
|
||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg=="],
|
||||
|
||||
"@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
|
||||
|
||||
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
|
||||
|
||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ=="],
|
||||
|
||||
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.59.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.1.2", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA=="],
|
||||
@@ -333,6 +398,8 @@
|
||||
|
||||
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
@@ -409,6 +476,8 @@
|
||||
|
||||
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||
|
||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
@@ -455,6 +524,8 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.21.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
@@ -483,6 +554,8 @@
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||
@@ -511,6 +584,8 @@
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
|
||||
@@ -525,6 +600,10 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||
|
||||
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -535,6 +614,8 @@
|
||||
|
||||
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@@ -543,6 +624,8 @@
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
@@ -659,6 +742,8 @@
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
|
||||
@@ -735,10 +820,14 @@
|
||||
|
||||
"regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="],
|
||||
|
||||
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="],
|
||||
|
||||
"rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
|
||||
|
||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
@@ -775,6 +864,8 @@
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.4.8", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w=="],
|
||||
@@ -867,6 +958,8 @@
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
@@ -893,6 +986,8 @@
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: HCPStaticSecret
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: taskarr-ap
|
||||
name: taskarr-app
|
||||
spec:
|
||||
method: GET
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: taskarr/app
|
||||
destination:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: HCPStaticSecret
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: taskarr-db
|
||||
spec:
|
||||
method: GET
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: taskarr/db
|
||||
destination:
|
||||
|
||||
18
deploy/base/db-taskarr-user.yaml
Normal file
18
deploy/base/db-taskarr-user.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultDynamicSecret
|
||||
metadata:
|
||||
name: taskarr-db-app-user
|
||||
spec:
|
||||
mount: database
|
||||
path: creds/taskarr-app
|
||||
destination:
|
||||
name: taskarr-db-url
|
||||
create: true
|
||||
# This is where the magic happens
|
||||
transformation:
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
DATABASE_URL:
|
||||
text: |
|
||||
{{- printf "postgresql://%s:%s@postgres-service.taskarr.svc.cluster.local:5432/taskarr?sslmode=disable" (get .Secrets "username") (get .Secrets "password") -}}
|
||||
@@ -48,7 +48,7 @@ spec:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
name: taskarr-db-url
|
||||
key: DATABASE_URL
|
||||
- name: ORIGIN
|
||||
valueFrom:
|
||||
@@ -60,16 +60,31 @@ spec:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
key: BETTER_AUTH_SECRET
|
||||
- name: GITEA_CLIENT_SECRET
|
||||
- name: GITHUB_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
key: GITEA_CLIENT_SECRET
|
||||
- name: GITEA_CLIENT_ID
|
||||
key: GITHUB_CLIENT_SECRET
|
||||
- name: GITHUB_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
key: GITEA_CLIENT_ID
|
||||
key: GITHUB_CLIENT_ID
|
||||
- name: GITHUB_APP_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
key: GITHUB_APP_ID
|
||||
- name: GITHUB_APP_SLUG
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
key: GITHUB_APP_SLUG
|
||||
- name: GITHUB_APP_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: taskarr-app
|
||||
key: GITHUB_APP_PRIVATE_KEY
|
||||
|
||||
restartPolicy: Always
|
||||
---
|
||||
|
||||
@@ -13,3 +13,4 @@ resources:
|
||||
- ./postgres.yaml
|
||||
- ./database-secret.yaml
|
||||
- ./app-secret.yaml
|
||||
- ./db-taskarr-user.yaml
|
||||
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: election
|
||||
name: taskarr-db-url
|
||||
key: DATABASE_URL
|
||||
|
||||
restartPolicy: OnFailure
|
||||
|
||||
@@ -7,7 +7,7 @@ spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: main # This name must match base EXACTLY
|
||||
- name: taskarr # This name must match base EXACTLY
|
||||
ports: # Adding this back into the patch solves the diff
|
||||
- containerPort: 3000
|
||||
name: taskarr
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace: taskarr
|
||||
images:
|
||||
- name: main
|
||||
newName: reg.milasholsting.dk/taskarr/taskarr
|
||||
newTag: latest
|
||||
newTag: sha-921560d
|
||||
- name: migrator
|
||||
newName: reg.milasholsting.dk/taskarr/migrator
|
||||
newTag: latest
|
||||
newName: reg.milasholsting.dk/taskarr/taskarr-migrator
|
||||
newTag: sha-921560d
|
||||
|
||||
23
docs/evaluation.md
Normal file
23
docs/evaluation.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Evaluering
|
||||
|
||||
## Hvad fungerede godt
|
||||
- SvelteKit gjorde det hurtigt at samle UI og backend i samme repo.
|
||||
- Login og beskyttede routes gav et klart brugerflow.
|
||||
- Projekter og tasks kan udvides uden at ændre arkitekturen meget.
|
||||
- GitHub App-installationsflowet (redirect → callback → repo selection → project link) er en komplet brugerrejse.
|
||||
- Callback'et håndterer manglende private key graceful uden at crashe.
|
||||
|
||||
## Hvad var udfordrende
|
||||
- GitHub App kræver en PEM-private key, som skal formateres korrekt i `.env` (enkelt linje, `\n` for line breaks).
|
||||
- Flere komponenter (callback, repo listing, issue sync) crashede uden private key — krævede `hasGitHubAppPrivateKey()` gates.
|
||||
- `githubRepositoryLink.installationId` forvekslede intern DB-id med GitHub's `installationId`, hvilket gav 404 ved issue-sync.
|
||||
- Rigtig GitHub-sync kræver provider-specifik auth og API-håndtering (JWT, access tokens, installation tokens).
|
||||
- Realtidsopdateringer skal afstemmes med deploy-miljø og connection limits.
|
||||
- Progressberegning skal være konsistent, når tasks ændres samtidigt.
|
||||
|
||||
## Hvordan kan SvelteKit bruges professionelt
|
||||
- Som ramme til interne værktøjer og dashboards, hvor SSR og form actions reducerer klient-kompleksitet.
|
||||
- Til applikationer med behov for hurtig første render og god SEO gennem SSR.
|
||||
- Til realtime-funktionalitet med SSE, hvor tunge websocket-løsninger ikke er nødvendige.
|
||||
- Sammen med Drizzle ORM til typesikker databaseadgang i full-stack TypeScript-projekter.
|
||||
- Til prototyper og MVP'er der skal kunne skaleres til produktion uden at skifte framework.
|
||||
47
docs/technical-decisions.md
Normal file
47
docs/technical-decisions.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Tekniske Valg
|
||||
|
||||
## Hvad jeg byggede
|
||||
En SvelteKit-baseret produktivitetsapp med login, projekter, tasks, GitHub App installation, repository linking, issue-sync og live opdateringskanal (SSE).
|
||||
|
||||
## Hvorfor SvelteKit
|
||||
- Server `load` gør det enkelt at hente data tæt på routen.
|
||||
- Form actions passer godt til mutationer uden tung klientlogik.
|
||||
- SSR giver hurtig første render og bedre default UX.
|
||||
- SSE passer til lette live-opdateringer uden fuld websocket-kompleksitet.
|
||||
|
||||
## Hvordan det virker i praksis
|
||||
- Brugeren logger ind via Better Auth (GitHub OAuth).
|
||||
- Dashboardet henter projekter, tasks og issues server-side.
|
||||
- Brugeren kan oprette projekter og tasks via form actions.
|
||||
- Projekter har en GitHub App-installationsknap, der sender brugeren til GitHub.
|
||||
- GitHub App installeres på en konto, og der vælges repositorier på GitHub's side.
|
||||
- Efter installation redirectes brugeren til en setup-side i appen, hvor de vælger et projekt og krydser repos fra.
|
||||
- De valgte repos gemmes som `githubRepositoryLink`-rækker.
|
||||
- På projektets detailside kan brugeren synce issues fra det linkede repo via GitHub App's installation access token.
|
||||
- SSE-endpoint (`/api/events`) sender notifikationer om task-ændringer og issue-sync, så klienten reloader data.
|
||||
|
||||
## GitHub App flow i detaljer
|
||||
1. `getGitHubInstallUrl()` genererer en URL med en HMAC-signeret state (indeholder `userId` og `redirect`).
|
||||
2. GitHub sender brugeren tilbage til `GITHUB_APP_SETUP_URL` med `installation_id` og `state`.
|
||||
3. Callback-validering: state afkodes og verficeres, `installation_id` gemmes i `githubInstallation`.
|
||||
4. Hvis `GITHUB_APP_PRIVATE_KEY` er sat, hentes account-login fra GitHub API. Ellers bruges en fallback.
|
||||
5. Brugeren redirectes til `/install/setup?installation={id}` for at vælge repos og projekt.
|
||||
6. Setup-siden bruger `listGitHubInstallationRepositories()` til at vise repos.
|
||||
7. Form action opretter `githubRepositoryLink`-rækker og redirecter til projektet.
|
||||
|
||||
## Hvad der virkede godt
|
||||
- SvelteKit gjorde dataflowet kort og tydeligt (load → form action → redirect).
|
||||
- Server actions holdt mutationerne samlede og eliminerede behov for klient-state.
|
||||
- Drizzle passede godt til schema-drevet udvikling med relations og foreign keys.
|
||||
- GitHub App JWT-auth var overskuelig med `node:crypto` og RS256-signering.
|
||||
- HMAC-signeret state gav en sikker callback-verifikation uden session storage.
|
||||
|
||||
## Hvad der var udfordrende
|
||||
- GitHub App kræver en PEM-private key, som skal formateres som en enkelt linje med `\n` i `.env`.
|
||||
- Private key skal være til stede før JWT-signerede API-kald kan fungere.
|
||||
- `githubRepositoryLink.installationId` er en foreign key til den interne `githubInstallation.id`, ikke GitHub's `installationId` — skal joins for at få den rigtige værdi.
|
||||
- SSE er nemt til énvejskommunikation, men websocket kan blive nødvendig senere.
|
||||
- Progress bør helst beregnes robust i databasen eller via en service.
|
||||
|
||||
## Professionel brug
|
||||
Løsningen kan bruges som intern project tracker, support-overblik eller developer dashboard med integrerede GitHub issues og statusopdateringer.
|
||||
18
docs/weekly-plan.md
Normal file
18
docs/weekly-plan.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Weekly Plan
|
||||
|
||||
## Uge 1
|
||||
- Planlæg et lille MVP-projekt i SvelteKit.
|
||||
- Implementer login, dashboard, projekter og opgaver.
|
||||
- Dokumenter hvorfor SvelteKit passer til SSR, form actions og realtime opdateringer.
|
||||
|
||||
## Uge 2
|
||||
- Tilføj GitHub App-integration med installationsflow.
|
||||
- Implementer repository selection ved installation.
|
||||
- Byg issue-sync fra GitHub repos.
|
||||
- Arbejd med live opdateringer via SSE.
|
||||
- Dokumenter de tekniske valg undervejs.
|
||||
|
||||
## Leverancer
|
||||
- Kort teknisk dokumentation.
|
||||
- Kildekode med projekter, tasks, GitHub-installationer og issue-overblik.
|
||||
- En evaluering af styrker og begrænsninger.
|
||||
59
drizzle/0000_sturdy_harrier.sql
Normal file
59
drizzle/0000_sturdy_harrier.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
CREATE TABLE "task" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"priority" integer DEFAULT 1 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp,
|
||||
"refresh_token_expires_at" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "session" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"user_id" text NOT NULL,
|
||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean DEFAULT false NOT NULL,
|
||||
"image" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "verification" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");
|
||||
33
drizzle/0001_kind_jamie_braddock.sql
Normal file
33
drizzle/0001_kind_jamie_braddock.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE "issue" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"project_id" integer NOT NULL,
|
||||
"provider" text NOT NULL,
|
||||
"external_id" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"state" text NOT NULL,
|
||||
"labels" text DEFAULT '[]' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "project" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"owner_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"status" text DEFAULT 'planning' NOT NULL,
|
||||
"progress" integer DEFAULT 0 NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD COLUMN "project_id" integer NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD COLUMN "description" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD COLUMN "status" text DEFAULT 'todo' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD COLUMN "due_date" timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD COLUMN "created_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD COLUMN "updated_at" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "issue" ADD CONSTRAINT "issue_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "project" ADD CONSTRAINT "project_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "task" ADD CONSTRAINT "task_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE no action;
|
||||
28
drizzle/0002_eager_shen.sql
Normal file
28
drizzle/0002_eager_shen.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE "github_installation" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"installation_id" text NOT NULL,
|
||||
"account_login" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "github_installation_installation_id_unique" UNIQUE("installation_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "github_repository_link" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"project_id" integer NOT NULL,
|
||||
"installation_id" integer NOT NULL,
|
||||
"owner" text NOT NULL,
|
||||
"repo" text NOT NULL,
|
||||
"full_name" text NOT NULL,
|
||||
"default_branch" text DEFAULT 'main' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "github_repository_link_full_name_unique" UNIQUE("full_name")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "github_installation" ADD CONSTRAINT "github_installation_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "github_repository_link" ADD CONSTRAINT "github_repository_link_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "github_repository_link" ADD CONSTRAINT "github_repository_link_installation_id_github_installation_id_fk" FOREIGN KEY ("installation_id") REFERENCES "public"."github_installation"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "github_installation_installation_id_idx" ON "github_installation" USING btree ("installation_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "github_repository_link_project_id_idx" ON "github_repository_link" USING btree ("project_id");
|
||||
406
drizzle/meta/0000_snapshot.json
Normal file
406
drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,406 @@
|
||||
{
|
||||
"id": "9b42afc7-ca3d-4f87-b3d1-3ec8549767c5",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.task": {
|
||||
"name": "task",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"priority": {
|
||||
"name": "priority",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_userId_idx": {
|
||||
"name": "account_userId_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"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
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_userId_idx": {
|
||||
"name": "session_userId_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"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": {
|
||||
"verification_identifier_idx": {
|
||||
"name": "verification_identifier_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "identifier",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
630
drizzle/meta/0001_snapshot.json
Normal file
630
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,630 @@
|
||||
{
|
||||
"id": "c6ca10a5-c791-4bd8-b5ab-a9e4aeac4f6b",
|
||||
"prevId": "9b42afc7-ca3d-4f87-b3d1-3ec8549767c5",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.issue": {
|
||||
"name": "issue",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"external_id": {
|
||||
"name": "external_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"labels": {
|
||||
"name": "labels",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"issue_project_id_project_id_fk": {
|
||||
"name": "issue_project_id_project_id_fk",
|
||||
"tableFrom": "issue",
|
||||
"tableTo": "project",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.project": {
|
||||
"name": "project",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"owner_id": {
|
||||
"name": "owner_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'planning'"
|
||||
},
|
||||
"progress": {
|
||||
"name": "progress",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"project_owner_id_user_id_fk": {
|
||||
"name": "project_owner_id_user_id_fk",
|
||||
"tableFrom": "project",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"owner_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.task": {
|
||||
"name": "task",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'todo'"
|
||||
},
|
||||
"priority": {
|
||||
"name": "priority",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"due_date": {
|
||||
"name": "due_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"task_project_id_project_id_fk": {
|
||||
"name": "task_project_id_project_id_fk",
|
||||
"tableFrom": "task",
|
||||
"tableTo": "project",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_userId_idx": {
|
||||
"name": "account_userId_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"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
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_userId_idx": {
|
||||
"name": "session_userId_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"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": {
|
||||
"verification_identifier_idx": {
|
||||
"name": "verification_identifier_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "identifier",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
840
drizzle/meta/0002_snapshot.json
Normal file
840
drizzle/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,840 @@
|
||||
{
|
||||
"id": "924d9707-48ad-4091-82d5-4322719e0ef0",
|
||||
"prevId": "c6ca10a5-c791-4bd8-b5ab-a9e4aeac4f6b",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.github_installation": {
|
||||
"name": "github_installation",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"installation_id": {
|
||||
"name": "installation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"account_login": {
|
||||
"name": "account_login",
|
||||
"type": "text",
|
||||
"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": {
|
||||
"github_installation_installation_id_idx": {
|
||||
"name": "github_installation_installation_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "installation_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"github_installation_user_id_user_id_fk": {
|
||||
"name": "github_installation_user_id_user_id_fk",
|
||||
"tableFrom": "github_installation",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"github_installation_installation_id_unique": {
|
||||
"name": "github_installation_installation_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"installation_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.github_repository_link": {
|
||||
"name": "github_repository_link",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"installation_id": {
|
||||
"name": "installation_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"repo": {
|
||||
"name": "repo",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"full_name": {
|
||||
"name": "full_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"default_branch": {
|
||||
"name": "default_branch",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'main'"
|
||||
},
|
||||
"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": {
|
||||
"github_repository_link_project_id_idx": {
|
||||
"name": "github_repository_link_project_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "project_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"github_repository_link_project_id_project_id_fk": {
|
||||
"name": "github_repository_link_project_id_project_id_fk",
|
||||
"tableFrom": "github_repository_link",
|
||||
"tableTo": "project",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"github_repository_link_installation_id_github_installation_id_fk": {
|
||||
"name": "github_repository_link_installation_id_github_installation_id_fk",
|
||||
"tableFrom": "github_repository_link",
|
||||
"tableTo": "github_installation",
|
||||
"columnsFrom": [
|
||||
"installation_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"github_repository_link_full_name_unique": {
|
||||
"name": "github_repository_link_full_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"full_name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.issue": {
|
||||
"name": "issue",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"external_id": {
|
||||
"name": "external_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"labels": {
|
||||
"name": "labels",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"issue_project_id_project_id_fk": {
|
||||
"name": "issue_project_id_project_id_fk",
|
||||
"tableFrom": "issue",
|
||||
"tableTo": "project",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.project": {
|
||||
"name": "project",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"owner_id": {
|
||||
"name": "owner_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'planning'"
|
||||
},
|
||||
"progress": {
|
||||
"name": "progress",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"project_owner_id_user_id_fk": {
|
||||
"name": "project_owner_id_user_id_fk",
|
||||
"tableFrom": "project",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"owner_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.task": {
|
||||
"name": "task",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'todo'"
|
||||
},
|
||||
"priority": {
|
||||
"name": "priority",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"due_date": {
|
||||
"name": "due_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"task_project_id_project_id_fk": {
|
||||
"name": "task_project_id_project_id_fk",
|
||||
"tableFrom": "task",
|
||||
"tableTo": "project",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_userId_idx": {
|
||||
"name": "account_userId_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"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
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_userId_idx": {
|
||||
"name": "session_userId_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"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": {
|
||||
"verification_identifier_idx": {
|
||||
"name": "verification_identifier_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "identifier",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
27
drizzle/meta/_journal.json
Normal file
27
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1779102031969,
|
||||
"tag": "0000_sturdy_harrier",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1779806328935,
|
||||
"tag": "0001_kind_jamie_braddock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1779807906067,
|
||||
"tag": "0002_eager_shen",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
43
package.json
43
package.json
@@ -1,24 +1,6 @@
|
||||
{
|
||||
"name": "taskarr-mgr",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"db:start": "podman compose up",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "~1.4.21",
|
||||
"@eslint/compat": "^2.0.4",
|
||||
@@ -30,6 +12,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^22",
|
||||
"@types/pg": "^8.20.0",
|
||||
"better-auth": "~1.4.21",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
@@ -37,8 +20,8 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"mdsvex": "^0.12.7",
|
||||
"postgres": "^3.4.9",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
@@ -48,5 +31,27 @@
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"vite": "^8.0.7"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"db:start": "podman compose up",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"pg": "^8.20.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,35 @@ import { env } from '$env/dynamic/private';
|
||||
import { getRequestEvent } from '$app/server';
|
||||
import { db } from '$lib/server/db';
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: env.ORIGIN,
|
||||
secret: env.BETTER_AUTH_SECRET,
|
||||
database: drizzleAdapter(db, { provider: 'pg' }),
|
||||
emailAndPassword: { enabled: true },
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: env.GITHUB_CLIENT_ID,
|
||||
clientSecret: env.GITHUB_CLIENT_SECRET
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
sveltekitCookies(getRequestEvent) // make sure this is the last plugin in the array
|
||||
]
|
||||
function createAuth() {
|
||||
const origin = env.ORIGIN;
|
||||
if (!origin) throw new Error('ORIGIN is not set');
|
||||
const secret = env.BETTER_AUTH_SECRET;
|
||||
if (!secret) throw new Error('BETTER_AUTH_SECRET is not set');
|
||||
const clientId = env.GITHUB_CLIENT_ID;
|
||||
if (!clientId) throw new Error('GITHUB_CLIENT_ID is not set');
|
||||
const clientSecret = env.GITHUB_CLIENT_SECRET;
|
||||
if (!clientSecret) throw new Error('GITHUB_CLIENT_SECRET is not set');
|
||||
|
||||
return betterAuth({
|
||||
baseURL: origin,
|
||||
secret,
|
||||
database: drizzleAdapter(db, { provider: 'pg' }),
|
||||
emailAndPassword: { enabled: true },
|
||||
socialProviders: {
|
||||
github: { clientId, clientSecret }
|
||||
},
|
||||
plugins: [
|
||||
sveltekitCookies(getRequestEvent)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
let _auth: ReturnType<typeof betterAuth>;
|
||||
|
||||
export const auth = new Proxy({} as ReturnType<typeof betterAuth>, {
|
||||
get(target, prop) {
|
||||
if (!_auth) _auth = createAuth();
|
||||
return Reflect.get(_auth, prop, target);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1 +1,93 @@
|
||||
// If you see this file, you have not run the auth:schema script yet, but you should!
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const session = pgTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => [index("session_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const account = pgTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const verification = pgTable(
|
||||
"verification",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from './schema';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
|
||||
const client = postgres(env.DATABASE_URL);
|
||||
|
||||
export const db = drizzle(client, { schema });
|
||||
export const db = drizzle(env.DATABASE_URL, { schema });
|
||||
|
||||
@@ -1,9 +1,140 @@
|
||||
import { pgTable, serial, integer, text } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { pgTable, serial, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
import { user } from './auth.schema';
|
||||
|
||||
export const projectStatus = ['planning', 'active', 'paused', 'done'] as const;
|
||||
export const taskStatus = ['todo', 'doing', 'blocked', 'done'] as const;
|
||||
export const issueSource = ['github', 'gitea'] as const;
|
||||
|
||||
export const githubInstallation = pgTable(
|
||||
'github_installation',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
installationId: text('installation_id').notNull().unique(),
|
||||
accountLogin: text('account_login').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull()
|
||||
},
|
||||
(table) => [uniqueIndex('github_installation_installation_id_idx').on(table.installationId)]
|
||||
);
|
||||
|
||||
export const githubRepositoryLink = pgTable(
|
||||
'github_repository_link',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
projectId: integer('project_id')
|
||||
.notNull()
|
||||
.references(() => project.id, { onDelete: 'cascade' }),
|
||||
installationId: integer('installation_id')
|
||||
.notNull()
|
||||
.references(() => githubInstallation.id, { onDelete: 'cascade' }),
|
||||
owner: text('owner').notNull(),
|
||||
repo: text('repo').notNull(),
|
||||
fullName: text('full_name').notNull().unique(),
|
||||
defaultBranch: text('default_branch').notNull().default('main'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull()
|
||||
},
|
||||
(table) => [uniqueIndex('github_repository_link_project_id_idx').on(table.projectId)]
|
||||
);
|
||||
|
||||
export const task = pgTable('task', {
|
||||
id: serial('id').primaryKey(),
|
||||
projectId: integer('project_id')
|
||||
.notNull()
|
||||
.references(() => project.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
priority: integer('priority').notNull().default(1)
|
||||
description: text('description').notNull().default(''),
|
||||
status: text('status', { enum: taskStatus }).notNull().default('todo'),
|
||||
priority: integer('priority').notNull().default(1),
|
||||
dueDate: timestamp('due_date'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const project = pgTable('project', {
|
||||
id: serial('id').primaryKey(),
|
||||
ownerId: text('owner_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description').notNull().default(''),
|
||||
status: text('status', { enum: projectStatus }).notNull().default('planning'),
|
||||
progress: integer('progress').notNull().default(0),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const issue = pgTable('issue', {
|
||||
id: serial('id').primaryKey(),
|
||||
projectId: integer('project_id')
|
||||
.notNull()
|
||||
.references(() => project.id, { onDelete: 'cascade' }),
|
||||
provider: text('provider', { enum: issueSource }).notNull(),
|
||||
externalId: text('external_id').notNull(),
|
||||
url: text('url').notNull(),
|
||||
title: text('title').notNull(),
|
||||
state: text('state').notNull(),
|
||||
labels: text('labels').notNull().default('[]'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at')
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const githubInstallationRelations = relations(githubInstallation, ({ one, many }) => ({
|
||||
user: one(user, {
|
||||
fields: [githubInstallation.userId],
|
||||
references: [user.id]
|
||||
}),
|
||||
repositories: many(githubRepositoryLink)
|
||||
}));
|
||||
|
||||
export const githubRepositoryLinkRelations = relations(githubRepositoryLink, ({ one }) => ({
|
||||
project: one(project, {
|
||||
fields: [githubRepositoryLink.projectId],
|
||||
references: [project.id]
|
||||
}),
|
||||
installation: one(githubInstallation, {
|
||||
fields: [githubRepositoryLink.installationId],
|
||||
references: [githubInstallation.id]
|
||||
})
|
||||
}));
|
||||
|
||||
export const projectRelations = relations(project, ({ many }) => ({
|
||||
tasks: many(task),
|
||||
issues: many(issue),
|
||||
githubRepositoryLinks: many(githubRepositoryLink)
|
||||
}));
|
||||
|
||||
export const taskRelations = relations(task, ({ one }) => ({
|
||||
project: one(project, {
|
||||
fields: [task.projectId],
|
||||
references: [project.id]
|
||||
})
|
||||
}));
|
||||
|
||||
export const issueRelations = relations(issue, ({ one }) => ({
|
||||
project: one(project, {
|
||||
fields: [issue.projectId],
|
||||
references: [project.id]
|
||||
})
|
||||
}));
|
||||
|
||||
export * from './auth.schema';
|
||||
|
||||
16
src/lib/server/events.ts
Normal file
16
src/lib/server/events.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const listeners = new Set<ReadableStreamDefaultController<string>>();
|
||||
|
||||
export function subscribe(controller: ReadableStreamDefaultController<string>) {
|
||||
listeners.add(controller);
|
||||
return () => listeners.delete(controller);
|
||||
}
|
||||
|
||||
export function publish(message: string) {
|
||||
for (const controller of listeners) {
|
||||
try {
|
||||
controller.enqueue(`data: ${message}\n\n`);
|
||||
} catch {
|
||||
listeners.delete(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
157
src/lib/server/github-app.ts
Normal file
157
src/lib/server/github-app.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { createHmac, createPrivateKey, createSign } from 'node:crypto';
|
||||
|
||||
export type GitHubRepository = {
|
||||
id: number;
|
||||
name: string;
|
||||
full_name: string;
|
||||
default_branch: string;
|
||||
owner: { login: string };
|
||||
};
|
||||
|
||||
type InstallState = {
|
||||
redirect: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export function hasGitHubAppPrivateKey() {
|
||||
return Boolean(env.GITHUB_APP_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
function normalizePrivateKey(key: string) {
|
||||
return key.includes('-----BEGIN') ? key.replace(/\\n/g, '\n') : Buffer.from(key, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
function base64urlJson(value: unknown) {
|
||||
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
||||
}
|
||||
|
||||
function signState(payload: InstallState, secret: string) {
|
||||
const serialized = base64urlJson(payload);
|
||||
const signature = createHmac('sha256', secret).update(serialized).digest('base64url');
|
||||
return `${serialized}.${signature}`;
|
||||
}
|
||||
|
||||
function verifyState(state: string, secret: string) {
|
||||
const [payload, signature] = state.split('.');
|
||||
if (!payload || !signature) return null;
|
||||
const expected = createHmac('sha256', secret).update(payload).digest('base64url');
|
||||
if (expected !== signature) return null;
|
||||
try {
|
||||
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as InstallState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getGitHubInstallUrl(input: { redirect?: string; userId: string }) {
|
||||
if (!env.GITHUB_APP_SLUG) throw new Error('GITHUB_APP_SLUG is not set');
|
||||
if (!env.BETTER_AUTH_SECRET) throw new Error('BETTER_AUTH_SECRET is not set');
|
||||
const redirect = input.redirect ?? '/integrations';
|
||||
const base = new URL(`https://github.com/apps/${env.GITHUB_APP_SLUG}/installations/new`);
|
||||
base.searchParams.set('state', signState({ redirect, userId: input.userId }, env.BETTER_AUTH_SECRET));
|
||||
return base.toString();
|
||||
}
|
||||
|
||||
export function decodeInstallState(state?: string) {
|
||||
if (!state) return { redirect: '/integrations' };
|
||||
if (!env.BETTER_AUTH_SECRET) throw new Error('BETTER_AUTH_SECRET is not set');
|
||||
const parsed = verifyState(state, env.BETTER_AUTH_SECRET);
|
||||
if (!parsed) return { redirect: '/integrations' };
|
||||
return { redirect: parsed.redirect || '/integrations', userId: parsed.userId };
|
||||
}
|
||||
|
||||
function createGitHubAppJwt() {
|
||||
if (!env.GITHUB_APP_ID) throw new Error('GITHUB_APP_ID is not set');
|
||||
if (!env.GITHUB_APP_PRIVATE_KEY) throw new Error('GITHUB_APP_PRIVATE_KEY is not set');
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const header = base64urlJson({ alg: 'RS256', typ: 'JWT' });
|
||||
const payload = base64urlJson({ iat: now - 60, exp: now + 9 * 60, iss: env.GITHUB_APP_ID });
|
||||
const signer = createSign('RSA-SHA256');
|
||||
signer.update(`${header}.${payload}`);
|
||||
signer.end();
|
||||
const key = createPrivateKey(normalizePrivateKey(env.GITHUB_APP_PRIVATE_KEY));
|
||||
const signature = signer.sign(key, 'base64url');
|
||||
return `${header}.${payload}.${signature}`;
|
||||
}
|
||||
|
||||
async function githubAppRequest(path: string, init: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
accept: 'application/vnd.github+json',
|
||||
'x-github-api-version': '2022-11-28',
|
||||
...(init.headers ?? {})
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub App request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function getGitHubInstallation(installationId: string) {
|
||||
const response = await githubAppRequest(`/app/installations/${installationId}`, {
|
||||
headers: { authorization: `Bearer ${createGitHubAppJwt()}` }
|
||||
});
|
||||
return (await response.json()) as {
|
||||
id: number;
|
||||
account?: { login?: string };
|
||||
};
|
||||
}
|
||||
|
||||
export async function getGitHubInstallationAccessToken(installationId: string) {
|
||||
const response = await githubAppRequest(`/app/installations/${installationId}/access_tokens`, {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${createGitHubAppJwt()}` }
|
||||
});
|
||||
return (await response.json()) as { token: string; expires_at: string };
|
||||
}
|
||||
|
||||
export async function listGitHubInstallationRepositories(installationId: string) {
|
||||
const { token } = await getGitHubInstallationAccessToken(installationId);
|
||||
const response = await fetch('https://api.github.com/installation/repositories?per_page=100', {
|
||||
headers: {
|
||||
accept: 'application/vnd.github+json',
|
||||
'x-github-api-version': '2022-11-28',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub repository list failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { repositories: GitHubRepository[] };
|
||||
return data.repositories;
|
||||
}
|
||||
|
||||
export async function listGitHubRepositoryIssues(input: { installationId: string; owner: string; repo: string }) {
|
||||
const { token } = await getGitHubInstallationAccessToken(input.installationId);
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/issues?state=all&per_page=100`,
|
||||
{
|
||||
headers: {
|
||||
accept: 'application/vnd.github+json',
|
||||
'x-github-api-version': '2022-11-28',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub issue list failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as Array<{
|
||||
id: number;
|
||||
number: number;
|
||||
title: string;
|
||||
state: 'open' | 'closed';
|
||||
html_url: string;
|
||||
labels: Array<{ name: string }>;
|
||||
pull_request?: unknown;
|
||||
}>;
|
||||
}
|
||||
72
src/routes/+page.server.ts
Normal file
72
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { project, task, issue } from '$lib/server/db/schema';
|
||||
import { desc, eq, sql } from 'drizzle-orm';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
const projects = await db
|
||||
.select({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
status: project.status,
|
||||
taskCount: sql<number>`(
|
||||
select count(*) from ${task} where ${task.projectId} = ${project.id}
|
||||
)`.mapWith(Number),
|
||||
doneCount: sql<number>`(
|
||||
select count(*) from ${task}
|
||||
where ${task.projectId} = ${project.id} and ${task.status} = 'done'
|
||||
)`.mapWith(Number),
|
||||
issueCount: sql<number>`(
|
||||
select count(*) from ${issue} where ${issue.projectId} = ${project.id}
|
||||
)`.mapWith(Number)
|
||||
})
|
||||
.from(project)
|
||||
.where(eq(project.ownerId, event.locals.user.id))
|
||||
.orderBy(desc(project.updatedAt));
|
||||
|
||||
const normalizedProjects = projects.map((item) => ({
|
||||
...item,
|
||||
progress: item.taskCount ? Math.round((item.doneCount / item.taskCount) * 100) : 0
|
||||
}));
|
||||
|
||||
const recentTasks = await db
|
||||
.select({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
priority: task.priority,
|
||||
projectId: task.projectId
|
||||
})
|
||||
.from(task)
|
||||
.innerJoin(project, eq(task.projectId, project.id))
|
||||
.where(eq(project.ownerId, event.locals.user.id))
|
||||
.orderBy(desc(task.updatedAt))
|
||||
.limit(5);
|
||||
|
||||
const recentIssues = await db
|
||||
.select({
|
||||
id: issue.id,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
provider: issue.provider,
|
||||
projectId: issue.projectId
|
||||
})
|
||||
.from(issue)
|
||||
.innerJoin(project, eq(issue.projectId, project.id))
|
||||
.where(eq(project.ownerId, event.locals.user.id))
|
||||
.orderBy(desc(issue.updatedAt))
|
||||
.limit(5);
|
||||
|
||||
return {
|
||||
user: event.locals.user,
|
||||
projects: normalizedProjects,
|
||||
recentTasks,
|
||||
recentIssues
|
||||
};
|
||||
};
|
||||
@@ -1,2 +1,104 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
|
||||
let { data }: { data: PageServerData } = $props();
|
||||
|
||||
onMount(() => {
|
||||
const events = new EventSource('/api/events');
|
||||
events.onmessage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
return () => events.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Taskarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto flex min-h-screen max-w-6xl flex-col gap-8 px-6 py-10">
|
||||
<header class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm uppercase tracking-[0.3em] text-slate-500">Taskarr</p>
|
||||
<h1 class="text-4xl font-bold text-slate-900">Projects, tasks, and synced issues in one place</h1>
|
||||
<p class="max-w-2xl text-slate-600">A compact SvelteKit workspace for tracking progress, handling tasks, and keeping GitHub or Gitea issues close to the work.</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a class="rounded-md border border-slate-300 px-4 py-2 text-slate-900" href="/projects">Projects</a>
|
||||
<a class="rounded-md bg-blue-600 px-4 py-2 text-white" href="/integrations">Install GitHub App</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<p class="text-sm text-slate-500">Projects</p>
|
||||
<p class="mt-2 text-3xl font-semibold">{data.projects.length}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<p class="text-sm text-slate-500">Recent tasks</p>
|
||||
<p class="mt-2 text-3xl font-semibold">{data.recentTasks.length}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<p class="text-sm text-slate-500">Recent issues</p>
|
||||
<p class="mt-2 text-3xl font-semibold">{data.recentIssues.length}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 lg:grid-cols-2">
|
||||
<div class="rounded-2xl border border-blue-200 bg-blue-50 p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Connect GitHub</h2>
|
||||
<p class="mt-2 text-sm text-slate-600">Install the GitHub App to link repositories and sync issues into your projects.</p>
|
||||
<a class="mt-4 inline-flex rounded-md bg-slate-900 px-4 py-2 text-white" href="/integrations">Go to integrations</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Projects</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{#each data.projects as project}
|
||||
<div class="rounded-xl border border-slate-100 bg-slate-50 p-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{project.name}</p>
|
||||
<p class="text-sm text-slate-500">{project.description}</p>
|
||||
</div>
|
||||
<span class="text-sm text-slate-500">{project.progress}%</span>
|
||||
</div>
|
||||
<div class="mt-3 h-2 rounded-full bg-slate-200">
|
||||
<div class="h-2 rounded-full bg-blue-600" style={`width: ${project.progress}%`}></div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-slate-500">{project.taskCount} tasks · {project.issueCount} issues · {project.status}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Recent tasks</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{#each data.recentTasks as task}
|
||||
<div class="flex items-center justify-between rounded-xl bg-slate-50 p-4">
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{task.title}</p>
|
||||
<p class="text-sm text-slate-500">Priority {task.priority} · {task.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Recent issues</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{#each data.recentIssues as item}
|
||||
<div class="rounded-xl bg-slate-50 p-4">
|
||||
<p class="font-medium text-slate-900">{item.title}</p>
|
||||
<p class="text-sm text-slate-500">{item.provider} · {item.state}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
34
src/routes/api/events/+server.ts
Normal file
34
src/routes/api/events/+server.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { subscribe } from '$lib/server/events';
|
||||
|
||||
export const GET = () => {
|
||||
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
||||
let stop: (() => void) | undefined;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
stop = subscribe(controller);
|
||||
heartbeat = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(': ping\n\n');
|
||||
} catch {
|
||||
if (heartbeat) clearInterval(heartbeat);
|
||||
stop?.();
|
||||
}
|
||||
}, 25000);
|
||||
|
||||
controller.enqueue(': connected\n\n');
|
||||
},
|
||||
cancel() {
|
||||
if (heartbeat) clearInterval(heartbeat);
|
||||
stop?.();
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'content-type': 'text/event-stream',
|
||||
'cache-control': 'no-cache',
|
||||
'connection': 'keep-alive'
|
||||
}
|
||||
});
|
||||
};
|
||||
40
src/routes/api/github/install/callback/+server.ts
Normal file
40
src/routes/api/github/install/callback/+server.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { githubInstallation } from '$lib/server/db/schema';
|
||||
import {
|
||||
decodeInstallState,
|
||||
getGitHubInstallation,
|
||||
hasGitHubAppPrivateKey
|
||||
} from '$lib/server/github-app';
|
||||
|
||||
export const GET = async ({ url, locals }) => {
|
||||
const installationId = url.searchParams.get('installation_id');
|
||||
const state = decodeInstallState(url.searchParams.get('state') ?? undefined);
|
||||
|
||||
if (!installationId) throw error(400, 'Missing installation_id');
|
||||
if (!state.userId) throw error(400, 'Missing install state');
|
||||
if (!locals.user || locals.user.id !== state.userId) throw redirect(302, '/auth/login');
|
||||
|
||||
const accountLogin = hasGitHubAppPrivateKey()
|
||||
? (await getGitHubInstallation(installationId)).account?.login ?? 'github'
|
||||
: 'github';
|
||||
|
||||
const [record] = await db
|
||||
.insert(githubInstallation)
|
||||
.values({
|
||||
userId: locals.user.id,
|
||||
installationId,
|
||||
accountLogin
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: githubInstallation.installationId,
|
||||
set: {
|
||||
userId: locals.user.id,
|
||||
accountLogin,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
.returning({ id: githubInstallation.id });
|
||||
|
||||
throw redirect(303, `/install/setup?installation=${record.id}`);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { auth } from '$lib/server/auth';
|
||||
|
||||
export const load: PageServerLoad = (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/auth/login');
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
return { user: event.locals.user };
|
||||
};
|
||||
@@ -15,6 +15,6 @@ export const actions: Actions = {
|
||||
await auth.api.signOut({
|
||||
headers: event.request.headers
|
||||
});
|
||||
return redirect(302, '/auth/login');
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { APIError } from 'better-auth/api';
|
||||
|
||||
export const load: PageServerLoad = (event) => {
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/demo/auth');
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
return {};
|
||||
};
|
||||
@@ -32,7 +32,7 @@ export const actions: Actions = {
|
||||
return fail(500, { message: 'Unexpected error' });
|
||||
}
|
||||
|
||||
return redirect(302, '/demo/auth');
|
||||
throw redirect(302, '/');
|
||||
},
|
||||
signUpEmail: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
@@ -56,12 +56,12 @@ export const actions: Actions = {
|
||||
return fail(500, { message: 'Unexpected error' });
|
||||
}
|
||||
|
||||
return redirect(302, '/demo/auth');
|
||||
throw redirect(302, '/');
|
||||
},
|
||||
signInSocial: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const provider = formData.get('provider')?.toString() ?? 'github';
|
||||
const callbackURL = formData.get('callbackURL')?.toString() ?? '/demo/auth';
|
||||
const callbackURL = formData.get('callbackURL')?.toString() ?? '/';
|
||||
|
||||
const result = await auth.api.signInSocial({
|
||||
body: {
|
||||
@@ -71,7 +71,7 @@ export const actions: Actions = {
|
||||
});
|
||||
|
||||
if (result.url) {
|
||||
return redirect(302, result.url);
|
||||
throw redirect(302, result.url);
|
||||
}
|
||||
return fail(400, { message: 'Social sign-in failed' });
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<form method="post" action="?/signInSocial" use:enhance>
|
||||
<input type="hidden" name="provider" value="github" />
|
||||
<input type="hidden" name="callbackURL" value="/demo/better-auth" />
|
||||
<input type="hidden" name="callbackURL" value="/" />
|
||||
<button class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700"
|
||||
>Sign in with GitHub</button
|
||||
>
|
||||
|
||||
89
src/routes/install/setup/+page.server.ts
Normal file
89
src/routes/install/setup/+page.server.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { githubInstallation, githubRepositoryLink, project } from '$lib/server/db/schema';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { listGitHubInstallationRepositories, hasGitHubAppPrivateKey } from '$lib/server/github-app';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
|
||||
const installationId = Number(event.url.searchParams.get('installation'));
|
||||
if (!Number.isFinite(installationId)) throw error(400, 'Missing installation');
|
||||
|
||||
const [installation] = await db
|
||||
.select()
|
||||
.from(githubInstallation)
|
||||
.where(and(eq(githubInstallation.id, installationId), eq(githubInstallation.userId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!installation) throw error(404, 'Installation not found');
|
||||
|
||||
const projects = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(eq(project.ownerId, event.locals.user.id))
|
||||
.orderBy(project.name);
|
||||
|
||||
const repositories = hasGitHubAppPrivateKey()
|
||||
? await listGitHubInstallationRepositories(installation.installationId)
|
||||
: [];
|
||||
|
||||
return { installation, projects, repositories };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
link: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
if (!hasGitHubAppPrivateKey()) throw error(400, 'GitHub App private key is not configured');
|
||||
|
||||
const installationId = Number(event.url.searchParams.get('installation'));
|
||||
if (!Number.isFinite(installationId)) throw error(400, 'Missing installation');
|
||||
|
||||
const formData = await event.request.formData();
|
||||
const projectId = Number(formData.get('projectId'));
|
||||
const selectedRepos = formData.getAll('repos') as string[];
|
||||
|
||||
if (!Number.isFinite(projectId)) return { error: 'Choose a project' };
|
||||
if (selectedRepos.length === 0) return { error: 'Select at least one repository' };
|
||||
|
||||
const [installation] = await db
|
||||
.select()
|
||||
.from(githubInstallation)
|
||||
.where(and(eq(githubInstallation.id, installationId), eq(githubInstallation.userId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!installation) throw error(404, 'Installation not found');
|
||||
|
||||
const [projectRecord] = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(and(eq(project.id, projectId), eq(project.ownerId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!projectRecord) throw error(404, 'Project not found');
|
||||
|
||||
for (const repoJson of selectedRepos) {
|
||||
const repo = JSON.parse(repoJson) as {
|
||||
owner: string;
|
||||
repo: string;
|
||||
fullName: string;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
await db
|
||||
.insert(githubRepositoryLink)
|
||||
.values({
|
||||
projectId,
|
||||
installationId: installation.id,
|
||||
owner: repo.owner,
|
||||
repo: repo.repo,
|
||||
fullName: repo.fullName,
|
||||
defaultBranch: repo.defaultBranch || 'main'
|
||||
})
|
||||
.onConflictDoNothing({ target: githubRepositoryLink.fullName });
|
||||
}
|
||||
|
||||
throw redirect(303, `/projects/${projectId}`);
|
||||
}
|
||||
};
|
||||
71
src/routes/install/setup/+page.svelte
Normal file
71
src/routes/install/setup/+page.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageServerData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Set up installation · Taskarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto min-h-screen max-w-3xl px-6 py-10">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h1 class="text-3xl font-bold text-slate-900">GitHub App installed</h1>
|
||||
<p class="mt-2 text-slate-600">
|
||||
Installation for <strong>{data.installation.accountLogin}</strong> is ready. Choose which repositories to link and which project to add them to.
|
||||
</p>
|
||||
|
||||
{#if data.repositories.length === 0}
|
||||
<p class="mt-6 text-sm text-slate-500">
|
||||
No repositories found. Install the app on an account that has repositories, or add a private key in <code>.env</code>.
|
||||
</p>
|
||||
<a class="mt-4 inline-block rounded-md bg-slate-900 px-4 py-2 text-white" href="/integrations">Back to integrations</a>
|
||||
{:else}
|
||||
<form method="post" action="?/link" class="mt-6 grid gap-6">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-700" for="project">Project</label>
|
||||
<select name="projectId" id="project" class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2">
|
||||
<option value="">Choose a project</option>
|
||||
{#each data.projects as item}
|
||||
<option value={item.id}>{item.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend class="text-sm font-medium text-slate-700">Repositories</legend>
|
||||
<div class="mt-2 space-y-2">
|
||||
{#each data.repositories as repo}
|
||||
<label class="flex items-center gap-3 rounded-xl border border-slate-200 p-3 hover:bg-slate-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="repos"
|
||||
value={JSON.stringify({
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
fullName: repo.full_name,
|
||||
defaultBranch: repo.default_branch
|
||||
})}
|
||||
class="h-4 w-4 rounded border-slate-300"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-medium text-slate-900">{repo.full_name}</p>
|
||||
<p class="text-sm text-slate-500">{repo.default_branch}</p>
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{#if form?.error}
|
||||
<p class="text-sm text-red-600">{form.error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button class="rounded-md bg-blue-600 px-4 py-2 text-white">Add to project</button>
|
||||
<a class="rounded-md border border-slate-300 px-4 py-2" href="/integrations">Skip</a>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
53
src/routes/integrations/+page.server.ts
Normal file
53
src/routes/integrations/+page.server.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { githubInstallation, githubRepositoryLink, project } from '$lib/server/db/schema';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { getGitHubInstallUrl, listGitHubInstallationRepositories } from '$lib/server/github-app';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
|
||||
const installations = await db
|
||||
.select()
|
||||
.from(githubInstallation)
|
||||
.where(eq(githubInstallation.userId, event.locals.user.id))
|
||||
.orderBy(desc(githubInstallation.updatedAt));
|
||||
|
||||
const linkedRepos = await db
|
||||
.select({
|
||||
id: githubRepositoryLink.id,
|
||||
fullName: githubRepositoryLink.fullName,
|
||||
owner: githubRepositoryLink.owner,
|
||||
repo: githubRepositoryLink.repo,
|
||||
projectName: project.name
|
||||
})
|
||||
.from(githubRepositoryLink)
|
||||
.innerJoin(project, eq(githubRepositoryLink.projectId, project.id))
|
||||
.where(eq(project.ownerId, event.locals.user.id))
|
||||
.orderBy(desc(githubRepositoryLink.updatedAt));
|
||||
|
||||
const installUrl = getGitHubInstallUrl({ redirect: '/integrations', userId: event.locals.user.id });
|
||||
|
||||
const repositoryCatalog = await Promise.all(
|
||||
installations.map(async (installation) => ({
|
||||
installationId: installation.id,
|
||||
accountLogin: installation.accountLogin,
|
||||
repositories: await listGitHubInstallationRepositories(installation.installationId)
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
installUrl,
|
||||
installations,
|
||||
linkedRepos,
|
||||
repositoryCatalog
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
startInstall: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
throw redirect(303, getGitHubInstallUrl({ redirect: '/integrations', userId: event.locals.user.id }));
|
||||
}
|
||||
};
|
||||
64
src/routes/integrations/+page.svelte
Normal file
64
src/routes/integrations/+page.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageServerData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Integrations · Taskarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto min-h-screen max-w-5xl px-6 py-10">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900">GitHub App integration</h1>
|
||||
<p class="text-slate-600">Install the app on GitHub, then link repositories to projects.</p>
|
||||
</div>
|
||||
<a class="rounded-md bg-slate-900 px-4 py-2 text-white" href="/">Dashboard</a>
|
||||
</div>
|
||||
|
||||
<section class="mt-8 grid gap-6 lg:grid-cols-2">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Install app</h2>
|
||||
<p class="mt-2 text-sm text-slate-600">Install the GitHub App to choose the repositories it can access.</p>
|
||||
<div class="mt-4 flex gap-3">
|
||||
<a class="rounded-md bg-blue-600 px-4 py-2 text-white" href={data.installUrl}>Install GitHub App</a>
|
||||
<form method="post" action="?/startInstall">
|
||||
<button class="rounded-md border border-slate-300 px-4 py-2">Open install flow</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Installed accounts</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{#if data.installations.length}
|
||||
{#each data.installations as installation}
|
||||
<div class="rounded-xl bg-slate-50 p-4">
|
||||
<p class="font-medium text-slate-900">{installation.accountLogin}</p>
|
||||
<p class="text-sm text-slate-500">Installation ID {installation.installationId}</p>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-sm text-slate-500">No installations yet.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Linked repositories</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{#if data.linkedRepos.length}
|
||||
{#each data.linkedRepos as repo}
|
||||
<div class="rounded-xl bg-slate-50 p-4">
|
||||
<p class="font-medium text-slate-900">{repo.fullName}</p>
|
||||
<p class="text-sm text-slate-500">Linked to {repo.projectName}</p>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="text-sm text-slate-500">No linked repositories yet.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
39
src/routes/projects/+page.server.ts
Normal file
39
src/routes/projects/+page.server.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { project } from '$lib/server/db/schema';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { publish } from '$lib/server/events';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
|
||||
const projects = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(eq(project.ownerId, event.locals.user.id))
|
||||
.orderBy(desc(project.updatedAt));
|
||||
|
||||
return { projects };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
const formData = await event.request.formData();
|
||||
const name = formData.get('name')?.toString().trim() ?? '';
|
||||
const description = formData.get('description')?.toString().trim() ?? '';
|
||||
|
||||
if (!name) return { error: 'Project name is required' };
|
||||
|
||||
await db.insert(project).values({
|
||||
ownerId: event.locals.user.id,
|
||||
name,
|
||||
description
|
||||
});
|
||||
|
||||
publish('project-created');
|
||||
|
||||
throw redirect(303, '/projects');
|
||||
}
|
||||
};
|
||||
38
src/routes/projects/+page.svelte
Normal file
38
src/routes/projects/+page.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageServerData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Projects · Taskarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto min-h-screen max-w-5xl px-6 py-10">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900">Projects</h1>
|
||||
<p class="text-slate-600">Create and track the work that matters.</p>
|
||||
</div>
|
||||
<a class="rounded-md bg-slate-900 px-4 py-2 text-white" href="/">Dashboard</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="?/create" class="mt-8 grid gap-3 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<input name="name" placeholder="Project name" class="rounded-md border border-slate-300 px-3 py-2" />
|
||||
<textarea name="description" placeholder="Short description" class="rounded-md border border-slate-300 px-3 py-2"></textarea>
|
||||
{#if form?.error}
|
||||
<p class="text-sm text-red-600">{form.error}</p>
|
||||
{/if}
|
||||
<button class="w-fit rounded-md bg-blue-600 px-4 py-2 text-white">Create project</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 grid gap-4 md:grid-cols-2">
|
||||
{#each data.projects as project}
|
||||
<a href={`/projects/${project.id}`} class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-blue-300">
|
||||
<p class="text-lg font-semibold text-slate-900">{project.name}</p>
|
||||
<p class="mt-1 text-sm text-slate-500">{project.description}</p>
|
||||
<p class="mt-3 text-xs uppercase tracking-[0.2em] text-slate-400">{project.status}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</main>
|
||||
152
src/routes/projects/[id]/+page.server.ts
Normal file
152
src/routes/projects/[id]/+page.server.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { githubInstallation, githubRepositoryLink, issue, project, task } from '$lib/server/db/schema';
|
||||
import { desc, eq, and, ne } from 'drizzle-orm';
|
||||
import { publish } from '$lib/server/events';
|
||||
import { listGitHubRepositoryIssues } from '$lib/server/github-app';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
|
||||
const projectId = Number(event.params.id);
|
||||
if (!Number.isFinite(projectId)) throw error(404, 'Project not found');
|
||||
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(and(eq(project.id, projectId), eq(project.ownerId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!record) throw error(404, 'Project not found');
|
||||
|
||||
const tasks = await db
|
||||
.select()
|
||||
.from(task)
|
||||
.where(eq(task.projectId, projectId))
|
||||
.orderBy(desc(task.updatedAt));
|
||||
|
||||
const completedTasks = tasks.filter((item) => item.status === 'done').length;
|
||||
const progress = tasks.length ? Math.round((completedTasks / tasks.length) * 100) : 0;
|
||||
|
||||
if (record.progress !== progress) {
|
||||
await db.update(project).set({ progress }).where(eq(project.id, projectId));
|
||||
}
|
||||
|
||||
const issues = await db
|
||||
.select()
|
||||
.from(issue)
|
||||
.where(eq(issue.projectId, projectId))
|
||||
.orderBy(desc(issue.updatedAt));
|
||||
|
||||
const repositoryLinks = await db
|
||||
.select()
|
||||
.from(githubRepositoryLink)
|
||||
.where(eq(githubRepositoryLink.projectId, projectId));
|
||||
|
||||
return { project: record, tasks, issues, repositoryLinks };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
createTask: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
const projectId = Number(event.params.id);
|
||||
if (!Number.isFinite(projectId)) throw error(404, 'Project not found');
|
||||
const formData = await event.request.formData();
|
||||
const title = formData.get('title')?.toString().trim() ?? '';
|
||||
const description = formData.get('description')?.toString().trim() ?? '';
|
||||
|
||||
if (!title) return { error: 'Task title is required' };
|
||||
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(and(eq(project.id, projectId), eq(project.ownerId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!record) throw error(404, 'Project not found');
|
||||
|
||||
await db.insert(task).values({
|
||||
projectId,
|
||||
title,
|
||||
description
|
||||
});
|
||||
|
||||
await db.update(project).set({ updatedAt: new Date() }).where(eq(project.id, projectId));
|
||||
publish(`task-created:${projectId}`);
|
||||
|
||||
throw redirect(303, `/projects/${projectId}`);
|
||||
},
|
||||
updateTaskStatus: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
const projectId = Number(event.params.id);
|
||||
if (!Number.isFinite(projectId)) throw error(404, 'Project not found');
|
||||
const formData = await event.request.formData();
|
||||
const taskId = Number(formData.get('taskId'));
|
||||
const status = formData.get('status')?.toString();
|
||||
|
||||
if (!Number.isFinite(taskId) || !status) throw error(400, 'Invalid task update');
|
||||
|
||||
await db
|
||||
.update(task)
|
||||
.set({ status: status as 'todo' | 'doing' | 'blocked' | 'done' })
|
||||
.where(and(eq(task.id, taskId), eq(task.projectId, projectId)));
|
||||
|
||||
await db.update(project).set({ updatedAt: new Date() }).where(eq(project.id, projectId));
|
||||
publish(`task-updated:${projectId}`);
|
||||
|
||||
throw redirect(303, `/projects/${projectId}`);
|
||||
},
|
||||
syncIssues: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
const projectId = Number(event.params.id);
|
||||
if (!Number.isFinite(projectId)) throw error(404, 'Project not found');
|
||||
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(and(eq(project.id, projectId), eq(project.ownerId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!record) throw error(404, 'Project not found');
|
||||
|
||||
const [linkedRepository] = await db
|
||||
.select({
|
||||
link: githubRepositoryLink,
|
||||
githubInstallationId: githubInstallation.installationId
|
||||
})
|
||||
.from(githubRepositoryLink)
|
||||
.innerJoin(githubInstallation, eq(githubRepositoryLink.installationId, githubInstallation.id))
|
||||
.where(eq(githubRepositoryLink.projectId, projectId))
|
||||
.limit(1);
|
||||
|
||||
if (!linkedRepository) throw error(400, 'Link a repository first');
|
||||
|
||||
const githubIssues = await listGitHubRepositoryIssues({
|
||||
installationId: linkedRepository.githubInstallationId,
|
||||
owner: linkedRepository.link.owner,
|
||||
repo: linkedRepository.link.repo
|
||||
});
|
||||
|
||||
await db.delete(issue).where(and(eq(issue.projectId, projectId), eq(issue.provider, 'github')));
|
||||
|
||||
for (const item of githubIssues.filter((entry) => !entry.pull_request)) {
|
||||
await db
|
||||
.insert(issue)
|
||||
.values({
|
||||
projectId,
|
||||
provider: 'github',
|
||||
externalId: String(item.id),
|
||||
url: item.html_url,
|
||||
title: item.title,
|
||||
state: item.state,
|
||||
labels: JSON.stringify(item.labels.map((label) => label.name))
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
publish(`issues-synced:${projectId}`);
|
||||
|
||||
throw redirect(303, `/projects/${projectId}`);
|
||||
}
|
||||
};
|
||||
94
src/routes/projects/[id]/+page.svelte
Normal file
94
src/routes/projects/[id]/+page.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { ActionData, PageServerData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
|
||||
onMount(() => {
|
||||
const events = new EventSource('/api/events');
|
||||
events.onmessage = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
return () => events.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.project.name} · Taskarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto min-h-screen max-w-5xl px-6 py-10">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900">{data.project.name}</h1>
|
||||
<p class="text-slate-600">{data.project.description}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a class="rounded-md border border-slate-300 px-4 py-2" href={`/projects/${data.project.id}/link-repository`}>Link repo</a>
|
||||
<a class="rounded-md bg-slate-900 px-4 py-2 text-white" href="/projects">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Tasks</h2>
|
||||
<form method="post" action="?/createTask" class="mt-4 grid gap-3">
|
||||
<input name="title" placeholder="Task title" class="rounded-md border border-slate-300 px-3 py-2" />
|
||||
<textarea name="description" placeholder="Task description" class="rounded-md border border-slate-300 px-3 py-2"></textarea>
|
||||
{#if form?.error}
|
||||
<p class="text-sm text-red-600">{form.error}</p>
|
||||
{/if}
|
||||
<button class="w-fit rounded-md bg-blue-600 px-4 py-2 text-white">Add task</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 space-y-3">
|
||||
{#each data.tasks as item}
|
||||
<div class="rounded-xl bg-slate-50 p-4">
|
||||
<p class="font-medium text-slate-900">{item.title}</p>
|
||||
<p class="text-sm text-slate-500">{item.status} · priority {item.priority}</p>
|
||||
<form method="post" action="?/updateTaskStatus" class="mt-3 flex gap-2">
|
||||
<input type="hidden" name="taskId" value={item.id} />
|
||||
<select name="status" class="rounded-md border border-slate-300 px-2 py-1 text-sm">
|
||||
<option value="todo">Todo</option>
|
||||
<option value="doing">Doing</option>
|
||||
<option value="blocked">Blocked</option>
|
||||
<option value="done">Done</option>
|
||||
</select>
|
||||
<button class="rounded-md bg-slate-900 px-3 py-1 text-sm text-white">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Issues</h2>
|
||||
<form method="post" action="?/syncIssues">
|
||||
<button class="rounded-md bg-slate-900 px-4 py-2 text-white">Sync GitHub issues</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-3">
|
||||
{#each data.issues as item}
|
||||
<div class="rounded-xl bg-slate-50 p-4">
|
||||
<p class="font-medium text-slate-900">{item.title}</p>
|
||||
<p class="text-sm text-slate-500">{item.provider} · {item.state}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h2 class="text-lg font-semibold">Linked repositories</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{#each data.repositoryLinks as repo}
|
||||
<div class="rounded-xl bg-slate-50 p-4">
|
||||
<p class="font-medium text-slate-900">{repo.fullName}</p>
|
||||
<p class="text-sm text-slate-500">{repo.owner}/{repo.repo}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
89
src/routes/projects/[id]/link-repository/+page.server.ts
Normal file
89
src/routes/projects/[id]/link-repository/+page.server.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { githubInstallation, githubRepositoryLink, project } from '$lib/server/db/schema';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { listGitHubInstallationRepositories } from '$lib/server/github-app';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
const projectId = Number(event.params.id);
|
||||
if (!Number.isFinite(projectId)) throw error(404, 'Project not found');
|
||||
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(and(eq(project.id, projectId), eq(project.ownerId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!record) throw error(404, 'Project not found');
|
||||
|
||||
const installations = await db
|
||||
.select()
|
||||
.from(githubInstallation)
|
||||
.where(eq(githubInstallation.userId, event.locals.user.id));
|
||||
|
||||
const repositoryCatalog = await Promise.all(
|
||||
installations.map(async (installation) => ({
|
||||
installationId: installation.id,
|
||||
accountLogin: installation.accountLogin,
|
||||
repositories: await listGitHubInstallationRepositories(installation.installationId)
|
||||
}))
|
||||
);
|
||||
|
||||
return { project: record, installations, repositoryCatalog };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
link: async (event) => {
|
||||
if (!event.locals.user) throw redirect(302, '/auth/login');
|
||||
const projectId = Number(event.params.id);
|
||||
const formData = await event.request.formData();
|
||||
const repository = formData.get('repository')?.toString() ?? '';
|
||||
|
||||
if (!repository) {
|
||||
return { error: 'Choose a repository to link' };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(repository) as {
|
||||
installationId: number;
|
||||
owner: string;
|
||||
repo: string;
|
||||
fullName: string;
|
||||
defaultBranch: string;
|
||||
};
|
||||
|
||||
if (!parsed.installationId || !parsed.fullName) {
|
||||
return { error: 'Choose a valid repository' };
|
||||
}
|
||||
|
||||
const [projectRecord] = await db
|
||||
.select()
|
||||
.from(project)
|
||||
.where(and(eq(project.id, projectId), eq(project.ownerId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!projectRecord) throw error(404, 'Project not found');
|
||||
|
||||
const [installation] = await db
|
||||
.select()
|
||||
.from(githubInstallation)
|
||||
.where(and(eq(githubInstallation.id, parsed.installationId), eq(githubInstallation.userId, event.locals.user.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!installation) throw error(404, 'Installation not found');
|
||||
|
||||
await db
|
||||
.insert(githubRepositoryLink)
|
||||
.values({
|
||||
projectId,
|
||||
installationId: installation.id,
|
||||
owner: parsed.owner,
|
||||
repo: parsed.repo,
|
||||
fullName: parsed.fullName,
|
||||
defaultBranch: parsed.defaultBranch || 'main'
|
||||
});
|
||||
|
||||
throw redirect(303, `/projects/${projectId}`);
|
||||
}
|
||||
};
|
||||
44
src/routes/projects/[id]/link-repository/+page.svelte
Normal file
44
src/routes/projects/[id]/link-repository/+page.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData, PageServerData } from './$types';
|
||||
|
||||
let { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Link repository · Taskarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="mx-auto min-h-screen max-w-3xl px-6 py-10">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<h1 class="text-3xl font-bold text-slate-900">Link a repository</h1>
|
||||
<p class="mt-2 text-slate-600">Attach a GitHub repository to {data.project.name} using an installed app.</p>
|
||||
|
||||
<form method="post" action="?/link" class="mt-6 grid gap-3">
|
||||
<select name="repository" class="rounded-md border border-slate-300 px-3 py-2">
|
||||
<option value="">Choose a repository</option>
|
||||
{#each data.repositoryCatalog as installation}
|
||||
<optgroup label={installation.accountLogin}>
|
||||
{#each installation.repositories as repo}
|
||||
<option
|
||||
value={JSON.stringify({
|
||||
installationId: installation.installationId,
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
fullName: repo.full_name,
|
||||
defaultBranch: repo.default_branch
|
||||
})}
|
||||
>
|
||||
{repo.full_name}
|
||||
</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-sm text-slate-500">Repositories shown here come from the installed GitHub App.</p>
|
||||
{#if form?.error}
|
||||
<p class="text-sm text-red-600">{form.error}</p>
|
||||
{/if}
|
||||
<button class="w-fit rounded-md bg-blue-600 px-4 py-2 text-white">Link repository</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,27 +1,27 @@
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
compilerOptions: {
|
||||
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
||||
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
||||
},
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
compilerOptions: {
|
||||
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
||||
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
||||
},
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
|
||||
typescript: {
|
||||
config: (config) => ({
|
||||
...config,
|
||||
include: [...config.include, '../drizzle.config.ts']
|
||||
})
|
||||
}
|
||||
},
|
||||
preprocess: [mdsvex({ extensions: ['.svx', '.md'] })],
|
||||
extensions: ['.svelte', '.svx', '.md']
|
||||
typescript: {
|
||||
config: (config) => ({
|
||||
...config,
|
||||
include: [...config.include, '../drizzle.config.ts']
|
||||
})
|
||||
}
|
||||
},
|
||||
preprocess: [mdsvex({ extensions: ['.svx', '.md'] })],
|
||||
extensions: ['.svelte', '.svx', '.md']
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
27
taskarr.pem
Normal file
27
taskarr.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAl+cyWUQ1WTrEFnkuBRfWbRbG5DRa9ZbyIyHDkMCQF4NEZ7WS
|
||||
6mZiGzx/ulkVG/fOxzV0dNC+iiyESvOOmrchj1HwbjQee2OHf1TsUeXtck5Q69Wv
|
||||
IQhKocZsmptbz4WXvPk4W3iF4d6D9DZpt9rc9pu/1URBwFucPebc0ZKUDQOAADxx
|
||||
PyOBgjUHeET2h7xJBcE0JAVKnu5YDJeak+fmYPz/j8JD+/t4lEeQby0hn6naJ+Hu
|
||||
G60lgMildf2/Q8AQkZMiEH6uaLXsbEVY7tsiEEs3jLKW7QucD+0afM51bAJtzOat
|
||||
Rf3z+aXMgGyqhqHeRLAPexk3wJKbeKxxni66FwIDAQABAoIBAQCR2oVGny6WWc3U
|
||||
QlDExSm3n0oj7n10GJaw0wejorH4UatJ5VeGx/3lZwbBmC2jqCKctp/2VkTOCYaR
|
||||
LSE9Px/zLFsuhc7K7Ts6MQGkdaCzw60BdTDuB7cZdJvtK8VRElYrNiU99xCeWvja
|
||||
cbC6v2SxScU9coeruorgCR73/8U2ZC4expBtObjKFah5liMdREQfotbhb/JkbEKR
|
||||
3xFkO1xHwsJtnGGHD8Dz9E9GY6GOCn4yL/7HfDkX5GSFoV1WuNOzKYjn43jh4MRl
|
||||
venrTveuE8eurRoALhl2lvinIh/QSp1LF0jtCiZMGvGvEaXDONw4l6BeDQGrp993
|
||||
mARbwShRAoGBAMpNnuLTh66T7xKS3xEOzoEd0ktZ1aHtdvee48nMU9Udtw1unm9R
|
||||
tGqpefZkWyjSVnToUv0F3Ffv5J/xT6MEu7iW+A3G2oW38xtw/NcEvDkWf4+JGtsG
|
||||
OW+fjyNxD77g348ZS5JvoN5+gGqpqv8DNkeW5RiNB4LG+1LkrSmYlwO/AoGBAMA4
|
||||
66Dh+gLQd7/K9M43hMdl2j2hj8/LmhUjQGt4z5AXuhvNemOuXniM1asFJkYCglyq
|
||||
aLV4yn+9oHWMZuvBwZhLEeMJRtCcxXF6dlQ9aNqUBPQyjYpWaVZd/DFeBGfc2j76
|
||||
FMS+oiosypjSFbpZw2xla4ltaBSEpe21carSvv+pAoGBAJf2eIxwUvJrg2FTXCaH
|
||||
Fc3dZdNeNuB87SmSfA9g4fQrbw6y8mYyXLDmf4v61JM0dOc6gOQ5m8uekwEmFikh
|
||||
bBV7qfdHUWyywfXyCKtHjk/fu5BJcBfodAqKM85upmI+rw/h82ebXJ7lLXdx3dXT
|
||||
Rtm92nd3sJWm/LDGv+CIE6AnAoGAH5y805aWYUQbjYU5QXjL82cCv0QEvK3FR1im
|
||||
zYXuzMm6J7xhNmeEzdqLgTa1lqnu+oJj+gRFeUCcVPikLH2O8xbVeDscVE6UAheb
|
||||
wjrfNaNGNUrjEC8p37RRN2U44EPn+Jd0Nu8LCFiZcQyL2jvM8dS0HMPLbCRsjW1b
|
||||
pjZGhxECgYBUmlVGqx3rYW0Tozyz7MQW5dHOjs12ufJtfkQ2vE2su0KNQuBAyzCM
|
||||
miHHmMcUEx3S0X0JY7ZWThuWKW9tbn6UnwwIBc+v5ghfAKL8cBUSvLyu8ipDghIR
|
||||
LfB41PGV9F1WsoupnazPWFqyvJX7xYUZ95/ahCbw4utU9hIwPz5O1w==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user