6 Commits

28 changed files with 205 additions and 163 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = tab
indent_size = 2
max_line_length = 100
quote_type = single
trim_trailing_whitespace = true

View File

@@ -0,0 +1,32 @@
when:
- event: [push]
steps:
- name: echos
image: alpine
commands:
- echo $CI_REPO
- echo $CI_REPO_REMOTE_ID
- echo $CI_REPO_URL
- echo $CI_REPO_CLONE_URL
- echo $${CI_COMMIT_BRANCH/\\//-}
- echo $USERNAME:$PASSWORD
environment:
USERNAME:
from_secret: registry-username
PASSWORD:
from_secret: registry-password
- name: build
image: woodpeckerci/plugin-kaniko
settings:
registry: gitea.cazzzer.com
username:
from_secret: registry-username
password:
from_secret: registry-password
repo: gitea.cazzzer.com/${CI_REPO}
# replace '/' with '_' in branch name
# remove 'feature/' prefix in branch name
# tags: ${CI_COMMIT_BRANCH##feature/}
# auto_tag: true
tags: ci
# cache: true

7
.zed/settings.json Normal file
View File

@@ -0,0 +1,7 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"formatter": "prettier"
}

View File

@@ -18,7 +18,7 @@ and [wg-quick](https://www.wireguard.com/quickstart/) for standalone setups.
## Development
Development uses bun.
An additional prepare step is needed to set up typia for type validation.
An additional prepare step is needed to set up typia for type validation.
For example .env settings, see [.env.example](.env.example)
@@ -27,3 +27,14 @@ bun install
bun run prepare
bun run dev
```
## To Do
- [ ] Proper invite page
- [ ] Proper error page for login without invite
- [ ] Support file provider (for wg-quick)
- [ ] Fix accessibility issues (lighthouse)
- [ ] Add title to profile page (lighthouse)
- [ ] wg-quick scripts (maybe?)
- [ ] Get rid of api routes (maybe?)
- [ ] Get rid of custom result types (maybe?)

View File

@@ -9,6 +9,7 @@
"drizzle-orm": "^0.38.4",
},
"devDependencies": {
"@lucide/svelte": "^0.487.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@ryoppippi/unplugin-typia": "npm:@jsr/ryoppippi__unplugin-typia",
@@ -24,14 +25,13 @@
"@types/qrcode-svg": "^1.1.5",
"arctic": "^2.3.4",
"autoprefixer": "^10.4.21",
"bits-ui": "^0.22.0",
"bits-ui": "^1.3.18",
"clsx": "^2.1.1",
"eslint": "^9.22.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"ip-address": "^10.0.1",
"lucide-svelte": "^0.469.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
@@ -181,7 +181,7 @@
"@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.4.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw=="],
"@melt-ui/svelte": ["@melt-ui/svelte@0.76.2", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": ">=3 <5" } }, "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA=="],
"@lucide/svelte": ["@lucide/svelte@0.487.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-27b/wUzWrqDJu97+1iSV2X8L2JGRWH/mAWAjHgazWxhGxVu/kS0p3SbNu6w3skNmQNEku33EKU1v44IVwULzbw=="],
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
@@ -347,7 +347,7 @@
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bits-ui": ["bits-ui@0.22.0", "", { "dependencies": { "@internationalized/date": "^3.5.1", "@melt-ui/svelte": "0.76.2", "nanoid": "^5.0.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw=="],
"bits-ui": ["bits-ui@1.3.18", "", { "dependencies": { "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", "tabbable": "^6.2.0" }, "peerDependencies": { "svelte": "^5.11.0" } }, "sha512-4ZsJqMZUpCEGYMHfVSNZDbOTg3Haxwe01I5LMs4ZImjY7huDac7Dkh2KWTtOdc7jhj2vJprE9bJI4Ayg8gexXQ=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
@@ -421,8 +421,6 @@
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
@@ -515,8 +513,6 @@
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
"focus-trap": ["focus-trap@7.6.2", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w=="],
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
@@ -563,6 +559,8 @@
"ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
"inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="],
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
@@ -635,8 +633,6 @@
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"lucide-svelte": ["lucide-svelte@0.469.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-PMIJ8jrFqVUsXJz4d1yfAQplaGhNOahwwkzbunha8DhpiD73xqX24n8dE1dPpUk3vcrdWVsHc1y/liHHotOnGQ=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@@ -665,7 +661,7 @@
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@5.0.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -787,6 +783,8 @@
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
@@ -827,6 +825,8 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -839,6 +839,8 @@
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
@@ -969,8 +971,6 @@
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
@@ -1099,8 +1099,6 @@
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
"eslint-plugin-svelte/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"gel/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
@@ -1111,14 +1109,10 @@
"pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
"svelte-eslint-parser/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"tailwindcss/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"tailwindcss/postcss-load-config/yaml": ["yaml@2.6.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg=="],
"vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="],

View File

@@ -19,6 +19,7 @@
"prepare": "ts-patch install"
},
"devDependencies": {
"@lucide/svelte": "^0.487.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@ryoppippi/unplugin-typia": "npm:@jsr/ryoppippi__unplugin-typia",
@@ -34,14 +35,13 @@
"@types/qrcode-svg": "^1.1.5",
"arctic": "^2.3.4",
"autoprefixer": "^10.4.21",
"bits-ui": "^0.22.0",
"bits-ui": "^1.3.18",
"clsx": "^2.1.1",
"eslint": "^9.22.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"ip-address": "^10.0.1",
"lucide-svelte": "^0.469.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",

View File

@@ -1,24 +1,24 @@
<script lang="ts">
import { LucideLoaderCircle } from 'lucide-svelte';
import { LucideLoaderCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js';
import googleIcon from '$lib/assets/google.svg';
let { inviteToken, class: className, ...rest }: { inviteToken?: string; class?: string; rest?: { [p: string]: unknown } } = $props();
let { inviteToken, class: className, ...rest }: {
inviteToken?: string;
class?: string;
rest?: { [p: string]: unknown }
} = $props();
let isLoading = $state(false);
let submitted = $state(false);
</script>
<div class={cn('flex gap-6', className)} {...rest}>
<form method="get" action="/auth/authentik{inviteToken ? `?invite=${inviteToken}` : ''}">
<form method="get" onsubmit={() => submitted = true}
action="/auth/authentik{inviteToken ? `?invite=${inviteToken}` : ''}">
<input type="hidden" value={inviteToken} name="invite" />
<Button
type="submit"
onclick={() => {
isLoading = true;
}}
>
{#if isLoading}
<Button type="submit" disabled={submitted}>
{#if submitted}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img
@@ -30,15 +30,11 @@
Sign in with Authentik
</Button>
</form>
<form method="get" action="/auth/google">
<form method="get" onsubmit={() => submitted = true}
action="/auth/google{inviteToken ? `?invite=${inviteToken}` : ''}">
<input type="hidden" value={inviteToken} name="invite" />
<Button
type="submit"
onclick={() => {
isLoading = true;
}}
>
{#if isLoading}
<Button type="submit" disabled={submitted}>
{#if submitted}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { LucideClipboardCopy, LucideDownload } from 'lucide-svelte';
import { LucideClipboardCopy, LucideDownload } from '@lucide/svelte';
const {
data,

View File

@@ -43,7 +43,7 @@
this={href ? "a" : "span"}
bind:this={ref}
{href}
class={cn(badgeVariants({ variant, className }))}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}

View File

@@ -56,7 +56,7 @@
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
@@ -65,7 +65,7 @@
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import Check from "@lucide/svelte/icons/check";
import Minus from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
let {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import X from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
@@ -17,7 +17,7 @@
} = $props();
</script>
<Dialog.Portal class="absolute" {...portalProps}>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
@@ -14,6 +13,4 @@
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</DialogPrimitive.Description>
/>

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
@@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</DialogPrimitive.Overlay>
/>

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
@@ -14,6 +13,4 @@
bind:ref
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</DialogPrimitive.Title>
/>

View File

@@ -1,22 +1,46 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
}: Props = $props();
</script>
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:value
{...restProps}
/>
{#if type === "file"}
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
@@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</LabelPrimitive.Root>
/>

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
@@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</TabsPrimitive.Content>
/>

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: TabsPrimitive.ListProps = $props();
</script>
@@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</TabsPrimitive.List>
/>

View File

@@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>
@@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</TabsPrimitive.Trigger>
/>

View File

@@ -3,7 +3,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import type { RequestEvent } from '@sveltejs/kit';
import type { Cookies } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
@@ -22,14 +22,14 @@ export async function createSession(userId: string): Promise<table.Session> {
const session: table.Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
expiresAt: new Date(Date.now() + DAY_IN_MS * 30),
};
await db.insert(table.sessions).values(session);
return session;
}
export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) {
event.cookies.set(sessionCookieName, sessionId, {
export function setSessionTokenCookie(cookies: Cookies, sessionId: string, expiresAt: Date) {
cookies.set(sessionCookieName, sessionId, {
path: '/',
sameSite: 'lax',
httpOnly: true,
@@ -42,16 +42,21 @@ export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
}
export function deleteSessionTokenCookie(event: RequestEvent) {
event.cookies.delete(sessionCookieName, { path: '/' });
export function deleteSessionTokenCookie(cookies: Cookies) {
cookies.delete(sessionCookieName, { path: '/' });
}
export async function validateSession(sessionId: string) {
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: { id: table.users.id, authSource: table.users.authSource, username: table.users.username, name: table.users.name },
session: table.sessions
user: {
id: table.users.id,
authSource: table.users.authSource,
username: table.users.username,
name: table.users.name,
},
session: table.sessions,
})
.from(table.sessions)
.innerJoin(table.users, eq(table.sessions.userId, table.users.id))

View File

@@ -1,14 +1,14 @@
import { fail, redirect } from "@sveltejs/kit";
import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/auth";
import type { Actions } from "./$types";
import { fail, redirect } from '@sveltejs/kit';
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from './$types';
export const actions: Actions = {
logout: async (event) => {
if (event.locals.session === null) {
logout: async ({ locals, cookies }) => {
if (locals.session === null) {
return fail(401);
}
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, "/");
}
await invalidateSession(locals.session.id);
deleteSessionTokenCookie(cookies);
redirect(302, '/');
},
};

View File

@@ -48,7 +48,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
@@ -67,7 +67,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
try {
await db.insert(table.users).values(user);
const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
} catch (e) {
console.error('failed to create user', e);
return new Response(null, {

View File

@@ -8,7 +8,7 @@ export const GET: RequestHandler = ({ url, cookies }) => {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const scopes = ['openid', 'profile', 'email'];
const authUrl = google.createAuthorizationURL(state, codeVerifier, scopes);
const authUrl = google.createAuthorizationURL(state + inviteToken, codeVerifier, scopes);
cookies.set('google_oauth_state', state, {
path: '/',
@@ -22,12 +22,6 @@ export const GET: RequestHandler = ({ url, cookies }) => {
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
if (inviteToken !== null) cookies.set('invite_token', inviteToken, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
return new Response(null, {
status: 302,

View File

@@ -1,25 +1,30 @@
import type { RequestHandler } from './$types';
import * as arctic from 'arctic';
import { google } from '$lib/server/oauth';
import { db } from '$lib/server/db';
import { eq } from 'drizzle-orm';
import * as table from '$lib/server/db/schema';
import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { google } from '$lib/server/oauth';
import { error, redirect } from '@sveltejs/kit';
import type { OAuth2Tokens } from 'arctic';
import * as arctic from 'arctic';
import { eq } from 'drizzle-orm';
import { assertGuard } from 'typia';
import type { PageServerLoad } from './$types';
export const GET: RequestHandler = async (event) => {
export const load: PageServerLoad = async (event) => {
const { url, cookies } = event;
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = cookies.get('google_oauth_state') ?? null;
const codeVerifier = cookies.get('google_code_verifier', ) ?? null;
const inviteToken = cookies.get('invite_token') ?? null;
const codeVerifier = cookies.get('google_code_verifier') ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400,
});
error(400, 'Missing url parameters');
}
const stateGeneratedToken = state.slice(0, storedState.length);
const stateInviteToken = state.slice(storedState.length);
if (stateGeneratedToken !== storedState) {
error(400, 'Invalid state in url');
}
let tokens: OAuth2Tokens;
@@ -28,20 +33,14 @@ export const GET: RequestHandler = async (event) => {
} catch (e) {
if (e instanceof arctic.OAuth2RequestError) {
console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e);
return new Response(null, {
status: 400,
});
throw error(400, 'Invalid authorization code');
}
if (e instanceof arctic.ArcticFetchError) {
console.debug('Arctic: failed to call `fetch()`', e);
return new Response(null, {
status: 400,
});
error(400, 'Failed to validate authorization code');
}
return new Response(null, {
status: 500,
});
console.error('Unxepcted error validating authorization code', code, e);
error(500);
}
const idToken = tokens.idToken();
@@ -60,20 +59,14 @@ export const GET: RequestHandler = async (event) => {
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
setSessionTokenCookie(cookies, session.id, session.expiresAt);
redirect(302, '/');
}
// TODO: proper error page
if (inviteToken === null || !isValidInviteToken(inviteToken)) {
return new Response(null, {
status: 400,
});
if (!isValidInviteToken(stateInviteToken)) {
const message =
stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link';
error(403, 'Not Authorized: ' + message);
}
const user: table.User = {
@@ -85,16 +78,11 @@ export const GET: RequestHandler = async (event) => {
// TODO: proper error handling, delete cookies
await db.insert(table.users).values(user);
console.log('created user', user, 'with invite token', inviteToken);
console.log('created user', user, 'with invite token', stateInviteToken);
const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
setSessionTokenCookie(cookies, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
redirect(302, '/');
};

View File

@@ -4,7 +4,7 @@
import { Badge } from '$lib/components/ui/badge';
import { Button, buttonVariants } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { LucideLoaderCircle, LucidePlus } from 'lucide-svelte';
import { LucideLoaderCircle, LucidePlus } from '@lucide/svelte';
import type { PageData } from './$types';
import { Label } from '$lib/components/ui/label';
import { page } from '$app/state';

View File

@@ -1,7 +1,7 @@
<script>
import { Button, buttonVariants } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { LucideLoaderCircle, LucideTrash } from 'lucide-svelte';
import { LucideLoaderCircle, LucideTrash } from '@lucide/svelte';
const { device } = $props();
let submitted = $state(false);
@@ -26,10 +26,12 @@
<Button type="submit" variant="destructive" disabled={submitted}>
Delete
</Button>
<Dialog.Close asChild let:builder>
<button class={buttonVariants()} disabled={submitted} use:builder.action {...builder}>
Cancel
</button>
<Dialog.Close>
{#snippet child({ props })}
<Button {...props} disabled={submitted}>
Cancel
</Button>
{/snippet}
</Dialog.Close>
</Dialog.Footer>
</form>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { invalidate, invalidateAll } from '$app/navigation';
import { Button } from '$lib/components/ui/button';
import { LucideLoaderCircle, LucideLogOut, LucideRefreshCw } from 'lucide-svelte';
import { LucideLoaderCircle, LucideLogOut, LucideRefreshCw } from '@lucide/svelte';
import { CodeSnippet } from '$lib/components/app/code-snippet/index.js';
let { data } = $props();