diff --git a/.air.toml b/.air.toml index 87d0c6e..f3dc73a 100644 --- a/.air.toml +++ b/.air.toml @@ -2,8 +2,8 @@ root = "." tmp_dir = "target/cache" [build] -cmd = "templ generate && go build -o target/cache/polybased ./polybased" -full_bin = "./target/cache/polybased -c polybase.cfg" +cmd = "go build -o target/cache/polybased ./polybased" +full_bin = "./target/cache/polybased -c polybase.cfg -skip-ldap -dev" include_ext = ["go", "templ", "css"] exclude_dir = ["target"] exclude_regex = ["_templ\\.go$"] diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e6c0962..50b223b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,27 +12,39 @@ concurrency: group: deploy-polybase cancel-in-progress: false +env: + TARGET_OS: "openbsd" + TARGET_FOLDER: "target" + jobs: build: name: Build polybase package runs-on: ubuntu-latest steps: - - name: Checkout source code - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install nix - uses: cachix/install-nix-action@v22 + - name: Install Go + uses: actions/setup-go@v6 with: - nix_path: nixpkgs=channel:nixos-unstable + go-version: '1.25' + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + + - name: Install just and scdoc + run: sudo apt install -y just scdoc + + - name: Install deps + run: bun i - - name: Build package - run: nix build + - name: Build and publish + run: just publish - name: Upload build result - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: result - path: result/ + path: $TARGET_FOLDER/ deploy: name: Deploy to openbsd server @@ -40,10 +52,10 @@ jobs: needs: build steps: - name: Download build result - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: name: result - path: result/ + path: $TARGET_FOLDER/ - name: Install SSH key uses: shimataro/ssh-key-action@v2 @@ -53,5 +65,5 @@ jobs: - name: Deploy to server run: | - scp -r result/* ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOSTNAME }}:/tmp/ + scp -r $TARGET_FOLDER/* ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOSTNAME }}:/tmp/ ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOSTNAME }} 'cd /tmp && sh install.sh' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4b5ece..50c770a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,13 +16,12 @@ jobs: format: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v6 + - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: - go-version: 1.23 - - - name: Code - uses: actions/checkout@v3 + go-version: '1.25' - name: Check diff between gofmt and code run: diff <(gofmt -d .) <(echo -n) @@ -30,13 +29,21 @@ jobs: name: Build polybase package runs-on: ubuntu-latest steps: - - name: Checkout source code - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install nix - uses: cachix/install-nix-action@v22 + - name: Install Go + uses: actions/setup-go@v6 with: - nix_path: nixpkgs=channel:nixos-unstable + go-version: '1.24' + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + + - name: Install just and scdoc + run: sudo apt install -y just scdoc + + - name: Install deps + run: bun i - name: Build package - run: nix build + run: just build diff --git a/.gitignore b/.gitignore index 5519bc0..d314dd7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,9 @@ /views/*_templ.go /views/*_templ.txt /static/css/styles.css +/static/js/ /results .idea /result docker-compose.yml +node_modules diff --git a/Procfile b/Procfile index 6cd2306..6599a2d 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ -ldap: glauth -c glauth.cfg -tailwind: tailwindcss -w -i static/css/main.css -o static/css/styles.css -m -templ: templ generate --watch --proxy="http://127.0.0.1:8080" -go: air +ldap: just dev-ldap +frontend: just dev-frontend +templ: just dev +go: just dev-air diff --git a/README.md b/README.md index 086658d..871062a 100644 --- a/README.md +++ b/README.md @@ -8,53 +8,41 @@ Self-hosted user database with LDAP authentication. - `polybased/`: Web interface (Go + HTMX) - `internal/`: Core backend logic with tests -## Requirements +## Usage -- Go 1.21+ -- SQLite -- Tailwind CSS -- Templ +To develop or to build polybase, you must have Go 1.24+ and Bun installed. +We are using `just` as a command runner. -## Nix Users - -```shell -nix develop +Build: +```bash just build ``` -## Other Users - -Install dependencies: - -- `go install github.com/air-verse/air@latest` -- `go install github.com/a-h/templ/cmd/templ@v0.3.906` -- `npm install -g tailwindcss@3` -- requires just, Hivemind, and GLAuth - -Build: - -```shell -just build +Publish: +```bash +just publish ``` Development: - -```shell -just dev # hot reload +```bash +just dev # basic backend +just dev-ldap # ldap +just dev-frontend # frontend +just dev-rw # test high packet loss +just dev-hivemind # if you have hivemind installed (start dev, dev-ldap, dev-frontend and dev-air) just migrate # initialize database just clean # remove artifacts ``` -## LDAP Development +### LDAP Development -Start GLAuth development LDAP server: - -```shell +We use GLAuth as a development LDAP server. +Start it with: +```bash glauth -c glauth.cfg ``` -Test accounts: - +Test accounts are: - `paul:paul*` - `ionys:ionys*` - `lydia:lydia*` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..4d30c0d --- /dev/null +++ b/bun.lock @@ -0,0 +1,159 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "polybase-go", + "dependencies": { + "htmx.org": "^2.0.8", + }, + "devDependencies": { + "@tailwindcss/cli": "^4.1.18", + "@types/bun": "latest", + "tailwindcss": "^4.1.18", + }, + }, + }, + "packages": { + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + + "@tailwindcss/cli": ["@tailwindcss/cli@4.1.18", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.18" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], + + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "htmx.org": ["htmx.org@2.0.8", "", {}, "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + } +} diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 957f797..0000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1752687322, - "narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 16630ae..0000000 --- a/flake.nix +++ /dev/null @@ -1,111 +0,0 @@ -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = - { - self, - nixpkgs, - flake-utils, - }: - flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - - buildPkgs = with pkgs; [ - pkg-config - templ - scdoc - go - tailwindcss_4 - ]; - - devPkgs = with pkgs; [ - just - air - sqlite - glauth - openldap - hivemind - watchman - ]; - - mkPolybase = - os: - pkgs.buildGoModule { - pname = "polybase"; - version = "0.1.0"; - src = ./.; - vendorHash = "sha256-3tqKrCOhQXAWUBMip+UxBDW9EAiAEXMSqOtfg8qmKT8="; - - nativeBuildInputs = buildPkgs; - - postPatch = '' - tailwindcss -i static/css/main.css -o static/css/styles.css -m - templ generate - ''; - - buildPhase = '' - go test ./tests/... - - export GOOS=${os} GOARCH=amd64 CGO_ENABLED=0 - mkdir -p bin - go build -o bin/polybased ./polybased - go build -o bin/polybase ./polybase - scdoc < polybase.1.scd | sed "s/1980-01-01/$(date '+%B %Y')/" > polybase.1 - scdoc < polybased.1.scd | sed "s/1980-01-01/$(date '+%B %Y')/" > polybased.1 - ''; - - installPhase = '' - mkdir -p $out/dist/{usr/local/bin,usr/local/man/man1,etc/rc.d} - cp bin/polybased bin/polybase $out/dist/usr/local/bin/ - cp *.1 $out/dist/usr/local/man/man1/ - cp polybased.rc $out/dist/etc/rc.d/polybased - cp install.sh $out/ - ''; - }; - in - { - packages = { - default = mkPolybase "openbsd"; - - docker = pkgs.dockerTools.buildImage { - name = "polybase"; - tag = "latest"; - - extraCommands = '' - mkdir -p var/lib/polybase var/log/polybase etc/polybase - - find ${./.}/migrations -name "*.sql" | \ - sort -n | \ - xargs cat | \ - ${pkgs.sqlite}/bin/sqlite3 var/lib/polybase/polybase.db - - chmod 755 var/lib/polybase var/log/polybase - chmod 644 var/lib/polybase/polybase.db - - touch etc/polybase/polybase.cfg - ''; - - config = { - Cmd = [ "${mkPolybase "linux"}/dist/usr/local/bin/polybased" ]; - ExposedPorts = { - "1265/tcp" = { }; - }; - Env = [ - "POLYBASE_SERVER_HOST=0.0.0.0" - ]; - }; - }; - }; - - devShell = pkgs.mkShell { - nativeBuildInputs = buildPkgs; - buildInputs = devPkgs; - }; - } - ); -} diff --git a/go.mod b/go.mod index f6cb527..c36831f 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,48 @@ module github.com/alias-asso/polybase-go -go 1.23.0 - -toolchain go1.23.4 +go 1.25 require ( github.com/BurntSushi/toml v1.5.0 github.com/go-ldap/ldap/v3 v3.4.11 github.com/golang-jwt/jwt/v5 v5.2.3 - golang.org/x/term v0.33.0 + golang.org/x/term v0.34.0 modernc.org/sqlite v1.38.0 ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect + github.com/air-verse/air v1.64.5 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bep/godartsass/v2 v2.5.0 // indirect + github.com/bep/golibsass v1.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cli/browser v1.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gohugoio/hugo v0.149.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/natefinch/atomic v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/sys v0.34.0 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cast v1.9.2 // indirect + github.com/tdewolff/parse/v2 v2.8.3 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect modernc.org/libc v1.65.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect @@ -26,8 +50,13 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect - github.com/a-h/templ v0.3.906 + github.com/a-h/templ v0.3.977 github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/google/uuid v1.6.0 // indirect - golang.org/x/crypto v0.40.0 // indirect + golang.org/x/crypto v0.41.0 // indirect +) + +tool ( + github.com/a-h/templ/cmd/templ + github.com/air-verse/air ) diff --git a/go.sum b/go.sum index 8d31ff6..17e895f 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,129 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/a-h/templ v0.3.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg= -github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334= +github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= +github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= +github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= +github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= +github.com/air-verse/air v1.64.5 h1:+gs/NgTzYYe+gGPyfHy3XxpJReQWC1pIsiKIg0LgNt4= +github.com/air-verse/air v1.64.5/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= +github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= +github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc= +github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo= +github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= +github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= +github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg= +github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU= +github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= +github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw= +github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg= +github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q= +github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc= +github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw= +github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044= +github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k= +github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8= +github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8= +github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk= +github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= +github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= +github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE= +github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro= +github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= +github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= +github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= +github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs= +github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ= +github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= +github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= +github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs= +github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= +github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio= +github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0 h1:dco+7YiOryRoPOMXwwaf+kktZSCtlFtreNdiJbETvYE= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0/go.mod h1:CRrxQTKeM3imw+UoS4EHKyrqB7Zp6sAJiqHit+aMGTE= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog= +github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= +github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= +github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= +github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= +github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -36,33 +136,122 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= +github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= +github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= +github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= +github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= +github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= +github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0= +github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= +github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo= +github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE= +github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I= +github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= +github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= +github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= +golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= @@ -89,3 +278,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..67c3a0b --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +import "htmx.org" diff --git a/justfile b/justfile index 384ea4f..decb363 100644 --- a/justfile +++ b/justfile @@ -1,45 +1,58 @@ dev: - hivemind + bun run build + go tool templ generate --watch --proxy="http://127.0.0.1:8080" --cmd "go tool air" +# ldap +dev-ldap: + glauth -c glauth.cfg + +# frontend hot reloading +dev-frontend: + bun run dev + +# if there is high packet loss dev-rw: - sudo sh jammer.sh start - hivemind; sudo sh jammer.sh stop + sudo sh jammer.sh start + just dev; sudo sh jammer.sh stop + +dev-hivemind: + hivemind build: clean build-server build-cli publish: test build - mkdir -p target/dist/{usr/local/bin,usr/share/man/man1,etc/polybase,etc/rc.d} - cp target/polybased target/dist/usr/local/bin - cp target/polybase target/dist/usr/local/bin - cp target/polybase.1 target/dist/usr/share/man/man1/ - cp target/polybased.1 target/dist/usr/share/man/man1/ - touch target/dist/etc/polybase/polybase.cfg - cp polybased.rc target/dist/etc/rc.d/polybased - cp install.sh target/dist/ - cd target && tar czf dist.tar.gz dist + mkdir -p target/dist/{usr/local/bin,usr/share/man/man1,etc/polybase,etc/rc.d} + cp target/polybased target/dist/usr/local/bin + cp target/polybase target/dist/usr/local/bin + cp target/polybase.1 target/dist/usr/share/man/man1/ + cp target/polybased.1 target/dist/usr/share/man/man1/ + touch target/dist/etc/polybase/polybase.cfg + cp polybased.rc target/dist/etc/rc.d/polybased + cp install.sh target/dist/ + cd target && tar czf dist.tar.gz dist test: - go test -cover ./... + go test -cover ./... migrate: - find migrations -name "*.sql" | sort -n | xargs cat | sqlite3 polybase.db + find migrations -name "*.sql" | sort -n | xargs cat | sqlite3 polybase.db clean: - rm -fr .cache/ - rm -fr target/ + rm -fr .cache/ + rm -fr target/ build-server: - mkdir -p target - tailwindcss -i static/css/main.css -o static/css/styles.css -m - templ generate - go build -o target/polybased ./polybased - scdoc < polybased.1.scd | sed "s/1980-01-01/$(date '+%B %Y')/" > target/polybased.1 + mkdir -p target + bun run build + go tool templ generate + go build -o target/polybased ./polybased + scdoc < polybased.1.scd | sed "s/1980-01-01/$(date '+%B %Y')/" > target/polybased.1 build-cli: - mkdir -p target - go build -o target/polybase ./polybase - scdoc < polybase.1.scd | sed "s/1980-01-01/$(date '+%B %Y')/" > target/polybase.1 + mkdir -p target + go build -o target/polybase ./polybase + scdoc < polybase.1.scd | sed "s/1980-01-01/$(date '+%B %Y')/" > target/polybase.1 build-docker: - nix build .#docker - docker load < result + nix build .#docker + docker load < result diff --git a/libpolybase/courses.go b/libpolybase/courses.go index 02af601..404d2ec 100644 --- a/libpolybase/courses.go +++ b/libpolybase/courses.go @@ -454,10 +454,7 @@ func validateCourse(course Course) (Course, error) { // Validate Semester course.Semester = strings.TrimSpace(course.Semester) - switch course.Semester { - case "S1", "S2": - // valid - default: + if course.Semester != "S1" && course.Semester != "S2" { return Course{}, fmt.Errorf("SEMESTER must be either S1 or S2") } diff --git a/package.json b/package.json new file mode 100644 index 0000000..7090269 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "polybase-go", + "module": "", + "type": "module", + "private": true, + "scripts": { + "dev": "tailwindcss -w -i static/css/main.css -o static/css/styles.css -m", + "build:css": "tailwindcss -i static/css/main.css -o static/css/styles.css -m", + "build:js": "bun build --minify --outfile=static/js/scripts.js index.ts", + "build": "bun run build:css && bun run build:js" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.1.18", + "@types/bun": "latest", + "tailwindcss": "^4.1.18" + }, + "dependencies": { + "htmx.org": "^2.0.8" + } +} diff --git a/polybase/args.go b/polybase/args.go deleted file mode 100644 index e21b288..0000000 --- a/polybase/args.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "io" - "os" - - "github.com/alias-asso/polybase-go/libpolybase" -) - -const defaultDBPath = "/var/lib/polybase/polybase.db" -const version = "0.1.0" - -func parseArgs() (string, []string, error) { - flags := flag.NewFlagSet("polybase", flag.ContinueOnError) - flags.SetOutput(io.Discard) - flags.Usage = func() {} - - for i, arg := range os.Args[1:] { - if arg == "-h" || arg == "help" { - if err := flags.Parse(os.Args[i+2:]); err != nil { - printUsage() - return "", nil, err - } - - args := flags.Args() - - if err := runHelp(args); err != nil { - return "", nil, err - } - os.Exit(0) - } - } - - for _, arg := range os.Args[1:] { - if arg == "-v" || arg == "version" { - fmt.Printf("polybase version %s\n", version) - os.Exit(0) - } - } - - dbPath := flags.String("db", defaultDBPath, "Database path") - - if err := flags.Parse(os.Args[1:]); err != nil { - printUsage() - return "", nil, err - } - - args := flags.Args() - if len(args) == 0 { - printUsage() - return "", nil, nil - } - - return *dbPath, args, nil -} - -func dispatch(pb libpolybase.Polybase, args []string) error { - if len(args) == 0 { - return fmt.Errorf("no command specified") - } - - ctx := context.Background() - cmd := args[0] - cmdArgs := args[1:] - - switch cmd { - case "create": - return runCreate(pb, ctx, cmdArgs) - case "get": - return runGet(pb, ctx, cmdArgs) - case "update": - return runUpdate(pb, ctx, cmdArgs) - case "delete": - return runDelete(pb, ctx, cmdArgs) - case "list": - return runList(pb, ctx, cmdArgs) - case "quantity": - return runQuantity(pb, ctx, cmdArgs) - case "visibility": - return runVisibility(pb, ctx, cmdArgs) - case "help": - return runHelp(cmdArgs) - default: - printUsage() - return fmt.Errorf("unknown command: %s", cmd) - } -} diff --git a/polybase/commands.go b/polybase/commands.go index 78666b7..53faab2 100644 --- a/polybase/commands.go +++ b/polybase/commands.go @@ -2,28 +2,36 @@ package main import ( "context" + "errors" "flag" "fmt" - "io" "os" - "os/user" "strconv" - "strings" "golang.org/x/term" "github.com/alias-asso/polybase-go/libpolybase" ) -func runCreate(pb libpolybase.Polybase, ctx context.Context, args []string) error { +var ( + ErrInvalidUsage = errors.New("invalid usage") +) + +func scope(args []string, usage func()) ([]string, string, string, uint8, error) { if len(args) < 3 { - printCreateUsage() - return fmt.Errorf("CODE, KIND and PART are required") + usage() + return nil, "", "", 0, errors.Join(ErrInvalidUsage, errors.New("CODE, KIND and PART are required")) + } + part, err := strconv.Atoi(args[2]) + if err != nil || part < 0 || part > 255 { + return nil, "", "", 0, errors.Join(ErrInvalidUsage, fmt.Errorf("invalid part number: %s", args[2])) } + return args[3:], args[0], args[1], uint8(part), nil +} - flags := flag.NewFlagSet("create", flag.ContinueOnError) - flags.SetOutput(io.Discard) - flags.Usage = func() {} +func runCreate(ctx context.Context, pb libpolybase.Polybase, args []string) error { + flags := flag.NewFlagSet("create", flag.ExitOnError) + flags.Usage = createUsage(flags) name := flags.String("n", "", "course name") quantity := flags.Int("q", -1, "initial quantity") @@ -31,38 +39,34 @@ func runCreate(pb libpolybase.Polybase, ctx context.Context, args []string) erro semester := flags.String("s", "", "semester") jsonOutput := flags.Bool("json", false, "output in JSON format") - if err := flags.Parse(args[3:]); err != nil { + args, code, kind, part, err := scope(args, flags.Usage) + if err != nil { return err } - if *name == "" || *quantity == -1 || *semester == "" { - printCreateUsage() - return fmt.Errorf("name (-n), quantity (-q) and semester (-s) are required") + if err := flags.Parse(args); err != nil { + return err } - part, err := strconv.Atoi(args[2]) - if err != nil { - return fmt.Errorf("invalid part number: %s", args[2]) + if *name == "" || *quantity == -1 || *semester == "" { + flags.Usage() + return errors.Join(ErrInvalidUsage, fmt.Errorf("name (-n), quantity (-q) and semester (-s) are required")) } if *total == 0 { *total = *quantity } - course := libpolybase.Course{ - Code: args[0], - Kind: args[1], - Part: part, - Parts: 0, + created, err := pb.CreateCourse(ctx, getCurrentUser(), libpolybase.Course{ + Code: code, + Kind: kind, + Part: int(part), Name: *name, Quantity: *quantity, Total: *total, Shown: true, Semester: *semester, - } - - username := getCurrentUser() - created, err := pb.CreateCourse(ctx, username, course) + }) if err != nil { return err } @@ -70,28 +74,25 @@ func runCreate(pb libpolybase.Polybase, ctx context.Context, args []string) erro return printCourse(created, *jsonOutput) } -func runGet(pb libpolybase.Polybase, ctx context.Context, args []string) error { - if len(args) < 3 { - printGetUsage() - return fmt.Errorf("CODE, KIND and PART are required") - } +func runGet(ctx context.Context, pb libpolybase.Polybase, args []string) error { + flags := flag.NewFlagSet("get", flag.ExitOnError) + flags.Usage = getUsage(flags) - part, err := strconv.Atoi(args[2]) + jsonOutput := flags.Bool("json", false, "output in JSON format") + + args, code, kind, part, err := scope(args, flags.Usage) if err != nil { - return fmt.Errorf("invalid part number: %s", args[2]) + return err } - flags := flag.NewFlagSet("get", flag.ContinueOnError) - jsonOutput := flags.Bool("json", false, "output in JSON format") - - if err := flags.Parse(args[3:]); err != nil { + if err := flags.Parse(args); err != nil { return err } id := libpolybase.CourseID{ - Code: args[0], - Kind: args[1], - Part: part, + Code: code, + Kind: kind, + Part: int(part), } course, err := pb.GetCourse(ctx, id) @@ -102,26 +103,10 @@ func runGet(pb libpolybase.Polybase, ctx context.Context, args []string) error { return printCourse(course, *jsonOutput) } -func runUpdate(pb libpolybase.Polybase, ctx context.Context, args []string) error { - if len(args) < 3 { - printUpdateUsage() - return fmt.Errorf("CODE, KIND and PART are required") - } - - code := args[0] - kind := args[1] - part, err := strconv.Atoi(args[2]) - if err != nil { - return fmt.Errorf("invalid part number: %s", args[2]) - } +func runUpdate(ctx context.Context, pb libpolybase.Polybase, args []string) error { + flags := flag.NewFlagSet("update", flag.ExitOnError) + flags.Usage = updateUsage(flags) - id := libpolybase.CourseID{ - Code: code, - Kind: kind, - Part: part, - } - - flags := flag.NewFlagSet("update", flag.ContinueOnError) newCode := flags.String("c", "", "update code") newKind := flags.String("k", "", "update kind") newPart := flags.Int("p", 0, "update part") @@ -131,7 +116,18 @@ func runUpdate(pb libpolybase.Polybase, ctx context.Context, args []string) erro newSemester := flags.String("s", "", "update semester") jsonOutput := flags.Bool("json", false, "output in JSON format") - if err := flags.Parse(args[3:]); err != nil { + args, code, kind, part, err := scope(args, flags.Usage) + if err != nil { + return err + } + + id := libpolybase.CourseID{ + Code: code, + Kind: kind, + Part: int(part), + } + + if err := flags.Parse(args); err != nil { return err } @@ -152,6 +148,8 @@ func runUpdate(pb libpolybase.Polybase, ctx context.Context, args []string) erro partial.Total = newTotal case "s": partial.Semester = newSemester + default: + panic(errors.Join(ErrInvalidUsage, fmt.Errorf("unknown flag %s", f.Name))) } }) @@ -164,21 +162,16 @@ func runUpdate(pb libpolybase.Polybase, ctx context.Context, args []string) erro return printCourse(updated, *jsonOutput) } -func runDelete(pb libpolybase.Polybase, ctx context.Context, args []string) error { - if len(args) < 3 { - printDeleteUsage() - return fmt.Errorf("CODE, KIND and PART are required") - } - - part, err := strconv.Atoi(args[2]) +func runDelete(ctx context.Context, pb libpolybase.Polybase, args []string) error { + args, code, kind, part, err := scope(args, deleteUsage(nil)) if err != nil { - return fmt.Errorf("invalid part number: %s", args[2]) + return err } id := libpolybase.CourseID{ - Code: args[0], - Kind: args[1], - Part: part, + Code: code, + Kind: kind, + Part: int(part), } course, err := pb.GetCourse(ctx, id) @@ -214,8 +207,10 @@ func runDelete(pb libpolybase.Polybase, ctx context.Context, args []string) erro return pb.DeleteCourse(ctx, username, id) } -func runList(pb libpolybase.Polybase, ctx context.Context, args []string) error { - flags := flag.NewFlagSet("list", flag.ContinueOnError) +func runList(ctx context.Context, pb libpolybase.Polybase, args []string) error { + flags := flag.NewFlagSet("list", flag.ExitOnError) + flags.Usage = listUsage(flags) + showHidden := flags.Bool("a", false, "show hidden courses") semester := flags.String("s", "", "filter by semester") code := flags.String("c", "", "filter by course code") @@ -239,6 +234,8 @@ func runList(pb libpolybase.Polybase, ctx context.Context, args []string) error filterKind = kind case "p": filterPart = part + default: + panic(errors.Join(ErrInvalidUsage, fmt.Errorf("unknown flag %s", f.Name))) } }) @@ -250,33 +247,35 @@ func runList(pb libpolybase.Polybase, ctx context.Context, args []string) error return printCourses(courses, *jsonOutput) } -func runQuantity(pb libpolybase.Polybase, ctx context.Context, args []string) error { +func runQuantity(ctx context.Context, pb libpolybase.Polybase, args []string) error { + flags := flag.NewFlagSet("get", flag.ExitOnError) + flags.Usage = quantityUsage(flags) + + jsonOutput := flags.Bool("json", false, "output in JSON format") + if len(args) < 4 { - printQuantityUsage() + flags.Usage() return fmt.Errorf("CODE, KIND, PART and DELTA are required") } - part, err := strconv.Atoi(args[2]) + args, code, kind, part, err := scope(args, flags.Usage) if err != nil { - return fmt.Errorf("invalid part number: %s", args[2]) + return err } - delta, err := strconv.Atoi(args[3]) + delta, err := strconv.Atoi(args[0]) if err != nil { - return fmt.Errorf("invalid delta value: %s", args[3]) + return fmt.Errorf("invalid delta value: %s", args[0]) } - flags := flag.NewFlagSet("get", flag.ContinueOnError) - jsonOutput := flags.Bool("json", false, "output in JSON format") - - if err := flags.Parse(args[4:]); err != nil { + if err := flags.Parse(args[1:]); err != nil { return err } id := libpolybase.CourseID{ - Code: args[0], - Kind: args[1], - Part: part, + Code: code, + Kind: kind, + Part: int(part), } username := getCurrentUser() @@ -288,29 +287,26 @@ func runQuantity(pb libpolybase.Polybase, ctx context.Context, args []string) er return printCourse(updated, *jsonOutput) } -func runVisibility(pb libpolybase.Polybase, ctx context.Context, args []string) error { - if len(args) < 3 { - printVisibilityUsage() - return fmt.Errorf("CODE, KIND and PART are required") - } +func runVisibility(ctx context.Context, pb libpolybase.Polybase, args []string) error { + flags := flag.NewFlagSet("visibility", flag.ExitOnError) + flags.Usage = visibilityUsage(flags) - flags := flag.NewFlagSet("visibility", flag.ContinueOnError) shown := flags.Bool("s", true, "visibility state") jsonOutput := flags.Bool("json", false, "output in JSON format") - if err := flags.Parse(args[3:]); err != nil { + args, code, kind, part, err := scope(args, flags.Usage) + if err != nil { return err } - part, err := strconv.Atoi(args[2]) - if err != nil { - return fmt.Errorf("invalid part number: %s", args[2]) + if err := flags.Parse(args); err != nil { + return err } id := libpolybase.CourseID{ - Code: args[0], - Kind: args[1], - Part: part, + Code: code, + Kind: kind, + Part: int(part), } username := getCurrentUser() @@ -321,45 +317,3 @@ func runVisibility(pb libpolybase.Polybase, ctx context.Context, args []string) return printCourse(updated, *jsonOutput) } - -func runHelp(args []string) error { - if len(args) == 0 { - printUsage() - return nil - } - - switch args[0] { - case "create": - printCreateUsage() - case "get": - printGetUsage() - case "update": - printUpdateUsage() - case "delete": - printDeleteUsage() - case "list": - printListUsage() - case "quantity": - printQuantityUsage() - case "visibility": - printVisibilityUsage() - default: - printUsage() - return fmt.Errorf("unknown command %q", args[0]) - } - return nil -} - -func getCurrentUser() string { - currentUser, err := user.Current() - if err != nil { - return "unknown-user" - } - - // Extract just the username part, removing domain if present - username := currentUser.Username - if i := strings.LastIndex(username, "\\"); i >= 0 { - username = username[i+1:] - } - return username -} diff --git a/polybase/main.go b/polybase/main.go index 2636902..91bd9ce 100644 --- a/polybase/main.go +++ b/polybase/main.go @@ -1,36 +1,119 @@ package main import ( + "context" "database/sql" + "errors" + "flag" "fmt" - "os" + "os/user" + "strings" "github.com/alias-asso/polybase-go/libpolybase" _ "modernc.org/sqlite" ) +const ( + version = "0.1.0" + defaultDBPath = "/var/lib/polybase/polybase.db" +) + +// Global args +var ( + showHelp = false + showVersion = false + dbPath = defaultDBPath +) + +func init() { + flag.BoolVar(&showHelp, "h", showHelp, "display the help") + flag.BoolVar(&showHelp, "help", showHelp, "display the help") + flag.BoolVar(&showVersion, "v", showVersion, "display the version of polybase") + flag.StringVar(&dbPath, "db", dbPath, "path of the database") +} + func main() { - if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "\nerror: %v\n", err) - os.Exit(1) + flag.Parse() + if showHelp { + printUsage() + return + } + if showVersion { + printVersion() + return } -} -func run() error { - dbPath, args, err := parseArgs() - if err != nil { - return err + args := flag.Args() + if len(args) == 0 { + printUsage() + return + } + if args[0] == "version" { + printVersion() + return } db, err := sql.Open("sqlite", dbPath) if err != nil { - return fmt.Errorf("failed to open database: %w", err) + panic(err) } defer db.Close() - if err := db.Ping(); err != nil { - return fmt.Errorf("invalid database file: %w", err) + if err = db.Ping(); err != nil { + panic(err) + } + + err = dispatch(libpolybase.New(db, "/var/log/polybase/polybase.log", false), flag.Args()) + if err != nil { + panic(err) + } +} + +var ( + ErrNoCommand = errors.New("no command specified") + ErrUnknownCommand = errors.New("unknown command") +) + +func dispatch(pb libpolybase.Polybase, args []string) error { + if len(args) == 0 { + return ErrNoCommand } - return dispatch(libpolybase.New(db, "/var/log/polybase/polybase.log", false), args) + ctx := context.Background() + cmd := args[0] + cmdArgs := args[1:] + + switch cmd { + case "create": + return runCreate(ctx, pb, cmdArgs) + case "get": + return runGet(ctx, pb, cmdArgs) + case "update": + return runUpdate(ctx, pb, cmdArgs) + case "delete": + return runDelete(ctx, pb, cmdArgs) + case "list": + return runList(ctx, pb, cmdArgs) + case "quantity": + return runQuantity(ctx, pb, cmdArgs) + case "visibility": + return runVisibility(ctx, pb, cmdArgs) + default: + printUsage() + return errors.Join(ErrUnknownCommand, fmt.Errorf("command %s not supported", cmd)) + } +} + +func getCurrentUser() string { + currentUser, err := user.Current() + if err != nil { + return "unknown-user" + } + + // Extract just the username part, removing domain if present + username := currentUser.Username + if i := strings.LastIndex(username, "\\"); i >= 0 { + username = username[i+1:] + } + return username } diff --git a/polybase/print.go b/polybase/print.go index b188d61..a5f74dd 100644 --- a/polybase/print.go +++ b/polybase/print.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "flag" "fmt" "os" "text/tabwriter" @@ -25,87 +26,84 @@ COMMANDS list List all courses quantity Update course quantity visibility Set course visibility - help Show help message for a specific command - -Use "polybase help command" for more information about a command. `, defaultDBPath) } -func printCreateUsage() { - fmt.Print(`Usage: polybase create [OPTIONS] - Create a new course entry. - - Options: - -n NAME Course name (required) - -q QUANTITY Initial quantity (required) - -t TOTAL Total quantity (default: same as quantity) - -s SEMESTER Semester (required) - -json Output in JSON format -`) +func printVersion() { + fmt.Printf("polybase version %s\n", version) } -func printGetUsage() { - fmt.Print(`Usage: polybase get [OPTIONS] - Display details for a specific course - - Options: - -json Output in JSON format -`) +func usage(usage, target string, flags *flag.FlagSet) func() { + return func() { + fmt.Printf("Usage: %s\n\t%s\n\n", usage, target) + if flags != nil { + fmt.Println("Options:") + flags.VisitAll(func(f *flag.Flag) { + def := f.DefValue + if len(def) == 0 { + def = `""` + } + fmt.Printf("-%s\t%s (default: %v)\n", f.Name, f.Usage, def) + }) + fmt.Println() + } + } } -func printUpdateUsage() { - fmt.Print(`Usage: polybase update [OPTIONS] - Update course information - - Options: - -c CODE Update course code - -k KEY Update course key - -p PART Update course part - -n NAME Update course name - -q QUANTITY Update quantity - -t TOTAL Update total quantity - -s SEMESTER Update semester - -json Output in JSON format -`) +func createUsage(flags *flag.FlagSet) func() { + return usage( + `polybase create [OPTIONS]`, + `Create a new course entry.`, + flags, + ) } -func printDeleteUsage() { - fmt.Print(`Usage: polybase delete - Remove a course from the database -`) +func getUsage(flags *flag.FlagSet) func() { + return usage( + `polybase get [OPTIONS]`, + `Display details for a specific course`, + flags, + ) } -func printListUsage() { - fmt.Print(`Usage: polybase list [OPTIONS] - List all courses - - Options: - -a Show hidden courses - -s SEMESTER Filter by semester - -c CODE Filter by code prefix - -k KIND Filter by kind - -p PART Filter by part number - -json Output in JSON format -`) +func updateUsage(flags *flag.FlagSet) func() { + return usage( + `polybase update [OPTIONS]`, + `Update course information`, + flags, + ) } -func printQuantityUsage() { - fmt.Print(`Usage: polybase quantity [OPTIONS] - Update course quantity by adding DELTA (can be negative) +func deleteUsage(flags *flag.FlagSet) func() { + return usage( + `polybase delete `, + `Remove a course from the database`, + flags, + ) +} - Options: - -json Output in JSON format -`) +func listUsage(flags *flag.FlagSet) func() { + return usage( + `polybase list [OPTIONS]`, + `List all courses`, + flags, + ) } -func printVisibilityUsage() { - fmt.Print(`Usage: polybase visibility [-s STATE] [OPTIONS] - Set course visibility +func quantityUsage(flags *flag.FlagSet) func() { + return usage( + `polybase quantity [OPTIONS]`, + `Update course quantity by adding DELTA (can be negative)`, + flags, + ) +} - Options: - -s Set visibility state (default: true) - -json Output in JSON format -`) +func visibilityUsage(flags *flag.FlagSet) func() { + return usage( + `polybase visibility [-s STATE] [OPTIONS]`, + `Set course visibility`, + flags, + ) } type CourseJSON struct { @@ -120,23 +118,26 @@ type CourseJSON struct { Semester string `json:"semester"` } +func newCourseJSON(c *libpolybase.Course) CourseJSON { + return CourseJSON{ + Code: c.Code, + Kind: c.Kind, + Part: c.Part, + Parts: c.Parts, + Name: c.Name, + Quantity: c.Quantity, + Total: c.Total, + Shown: c.Shown, + Semester: c.Semester, + } +} + func printCourses(courses []libpolybase.Course, jsonOutput bool) error { if jsonOutput { var coursesJSON []CourseJSON for _, c := range courses { - coursesJSON = append(coursesJSON, CourseJSON{ - Code: c.Code, - Kind: c.Kind, - Part: c.Part, - Parts: c.Parts, - Name: c.Name, - Quantity: c.Quantity, - Total: c.Total, - Shown: c.Shown, - Semester: c.Semester, - }) + coursesJSON = append(coursesJSON, newCourseJSON(&c)) } - return json.NewEncoder(os.Stdout).Encode(courses) } @@ -153,18 +154,7 @@ func printCourses(courses []libpolybase.Course, jsonOutput bool) error { func printCourse(c libpolybase.Course, jsonOutput bool) error { if jsonOutput { - courseJSON := CourseJSON{ - Code: c.Code, - Kind: c.Kind, - Part: c.Part, - Parts: c.Parts, - Name: c.Name, - Quantity: c.Quantity, - Total: c.Total, - Shown: c.Shown, - Semester: c.Semester, - } - return json.NewEncoder(os.Stdout).Encode(courseJSON) + return json.NewEncoder(os.Stdout).Encode(newCourseJSON(&c)) } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) diff --git a/polybased/args.go b/polybased/args.go deleted file mode 100644 index 4b211ab..0000000 --- a/polybased/args.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" -) - -type Args struct { - ConfigPath string - ShowHelp bool - ShowVersion bool -} - -func parseArgs() (*Args, error) { - args := &Args{ - ConfigPath: "/etc/polybase/polybase.cfg", - } - - // If no arguments provided, return defaults - if len(os.Args) <= 1 { - return args, nil - } - - // First pass: check for help flag - for _, arg := range os.Args[1:] { - if arg == "-h" || arg == "--help" { - printUsage() - os.Exit(0) - } - } - - // Second pass: process other flags - osArgs := os.Args[1:] - for i := 0; i < len(osArgs); i++ { - arg := osArgs[i] - - switch arg { - case "-v": - args.ShowVersion = true - return args, nil - - case "-c": - if i+1 >= len(osArgs) { - return nil, fmt.Errorf("error: -c requires a path argument") - } - nextArg := osArgs[i+1] - if strings.HasPrefix(nextArg, "-") { - return nil, fmt.Errorf("error: -c requires a path argument") - } - args.ConfigPath = nextArg - i++ - - default: - if strings.HasPrefix(arg, "-") { - return nil, fmt.Errorf("error: unknown flag: %s", arg) - } - } - } - - return args, nil -} - -func printUsage() { - fmt.Printf(`Usage: polybased [OPTIONS] - -Manage polybase database from the web browser. - -Options: - -c Path to config file (default: /etc/polybase/config.cfg) - -v Print version information - -h Print this help message - -For bug reporting and more information, please see: -https://github.com/alias-asso/polybase-go -`) -} diff --git a/polybased/config/config.go b/polybased/config/config.go index 0548eb2..aa78712 100644 --- a/polybased/config/config.go +++ b/polybased/config/config.go @@ -15,7 +15,6 @@ import ( type Server struct { Host string Port string - Mode string Log string } @@ -46,7 +45,6 @@ func DefaultConfig() Config { Server: Server{ Host: "127.0.0.1", Port: "1265", - Mode: "prod", Log: "/var/log/polybase/polybase.log", }, Database: Database{ @@ -58,7 +56,7 @@ func DefaultConfig() Config { } } -func LoadConfig(configPath string) (Config, error) { +func LoadConfig(configPath string, skipLdap bool) (Config, error) { config := DefaultConfig() if _, err := toml.DecodeFile(configPath, &config); err != nil { return Config{}, err @@ -66,7 +64,7 @@ func LoadConfig(configPath string) (Config, error) { config.loadFromEnv() - if err := config.Validate(); err != nil { + if err := config.Validate(skipLdap); err != nil { return Config{}, fmt.Errorf("invalid configuration: %w", err) } @@ -80,9 +78,6 @@ func (c *Config) loadFromEnv() { if port := os.Getenv("POLYBASE_SERVER_PORT"); port != "" { c.Server.Port = port } - if mode := os.Getenv("POLYBASE_SERVER_MODE"); mode != "" { - c.Server.Mode = mode - } if log := os.Getenv("POLYBASE_SERVER_LOG"); log != "" { c.Server.Log = log } @@ -109,7 +104,7 @@ func (c *Config) loadFromEnv() { } } -func (c *Config) Validate() error { +func (c *Config) Validate(skipLdap bool) error { // Server validation if c.Server.Host == "" { return fmt.Errorf("server.host is required") @@ -125,10 +120,6 @@ func (c *Config) Validate() error { return fmt.Errorf("server.port must be a valid port number (1-65535)") } - if c.Server.Mode == "" { - return fmt.Errorf("server.mode is required") - } - if c.Server.Log == "" { return fmt.Errorf("server.log is required") } @@ -138,29 +129,31 @@ func (c *Config) Validate() error { return fmt.Errorf("database.path is required") } - // LDAP validation - if c.LDAP.Host == "" { - return fmt.Errorf("ldap.host is required") - } - if c.LDAP.Port == "" { - return fmt.Errorf("ldap.port is required") - } - if port, err := strconv.Atoi(c.LDAP.Port); err != nil || port < 1 || port > 65535 { - return fmt.Errorf("ldap.port must be a valid port number (1-65535)") - } - if c.LDAP.UserDN == "" { - return fmt.Errorf("ldap.user_dn is required") - } - if !strings.Contains(c.LDAP.UserDN, "%s") { - return fmt.Errorf("ldap.user_dn must contain %%s placeholder for username") - } - - // Test LDAP connection - l, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%s", c.LDAP.Host, c.LDAP.Port)) - if err != nil { - return fmt.Errorf("failed to connect to LDAP server: %w", err) + if !skipLdap { + // LDAP validation + if c.LDAP.Host == "" { + return fmt.Errorf("ldap.host is required") + } + if c.LDAP.Port == "" { + return fmt.Errorf("ldap.port is required") + } + if port, err := strconv.Atoi(c.LDAP.Port); err != nil || port < 1 || port > 65535 { + return fmt.Errorf("ldap.port must be a valid port number (1-65535)") + } + if c.LDAP.UserDN == "" { + return fmt.Errorf("ldap.user_dn is required") + } + if !strings.Contains(c.LDAP.UserDN, "%s") { + return fmt.Errorf("ldap.user_dn must contain %%s placeholder for username") + } + + // Test LDAP connection + l, err := ldap.DialURL(fmt.Sprintf("ldap://%s:%s", c.LDAP.Host, c.LDAP.Port)) + if err != nil { + return fmt.Errorf("failed to connect to LDAP server: %w", err) + } + defer l.Close() } - defer l.Close() // Auth validation if c.Auth.JWTSecret == "" { diff --git a/polybased/config/context.go b/polybased/config/context.go new file mode 100644 index 0000000..ef09b5d --- /dev/null +++ b/polybased/config/context.go @@ -0,0 +1,70 @@ +package config + +import ( + "context" + "net/http" + + "github.com/golang-jwt/jwt/v5" +) + +type key uint8 + +const ( + cfgKey key = 0 + userKey key = 1 + loggedIn key = 2 + devMode key = 3 +) + +func CreateContext(ctx context.Context, cfg *Config, dev bool) context.Context { + ctx = context.WithValue(ctx, cfgKey, cfg) + ctx = context.WithValue(ctx, devMode, dev) + return ctx +} + +func SetAuth(ctx context.Context, r *http.Request) context.Context { + logged, username := userConnected(GetConfig(ctx), r) + ctx = context.WithValue(ctx, loggedIn, logged) + if logged { + ctx = context.WithValue(ctx, userKey, username) + } + return ctx +} + +func userConnected(cfg *Config, r *http.Request) (bool, string) { + cookie, err := r.Cookie("X-Auth-Token") + if err != nil { + return false, "" + } + + type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims + } + + token, err := jwt.ParseWithClaims(cookie.Value, &Claims{}, func(token *jwt.Token) (any, error) { + return []byte(cfg.Auth.JWTSecret), nil + }) + if err != nil || !token.Valid { + return false, "" + } + + v, ok := token.Claims.(*Claims) + return ok, v.Username +} + +func GetConfig(ctx context.Context) *Config { + return ctx.Value(cfgKey).(*Config) +} + +func IsLogged(ctx context.Context) bool { + return ctx.Value(loggedIn).(bool) +} + +func GetUsername(ctx context.Context) string { + return ctx.Value(userKey).(string) +} + +func IsDev(ctx context.Context) bool { + return ctx.Value(devMode).(bool) +} diff --git a/polybased/main.go b/polybased/main.go index a51f5b7..a38d135 100644 --- a/polybased/main.go +++ b/polybased/main.go @@ -1,37 +1,76 @@ package main import ( + "context" + "flag" + "fmt" "log" - "os" "github.com/alias-asso/polybase-go/polybased/config" "github.com/alias-asso/polybase-go/polybased/routes" ) +const version = "0.1.0" + +var ( + showHelp bool = false + showVersion bool = false + skipLdap bool = false + devMode bool = false + configPath string = "" +) + +func init() { + flag.BoolVar(&showHelp, "h", showHelp, "show the help") + flag.BoolVar(&showHelp, "help", showHelp, "show the help") + flag.BoolVar(&showVersion, "v", showVersion, "show the version") + flag.BoolVar(&skipLdap, "skip-ldap", skipLdap, "skip ldap checks") + flag.BoolVar(&devMode, "dev", devMode, "enable dev mode") + flag.StringVar(&configPath, "c", configPath, "set the config path") +} + func main() { - args, err := parseArgs() - if err != nil { - log.Fatal(err) - } + flag.Parse() - if args.ShowHelp { + if showHelp { printUsage() - os.Exit(0) + return } - if args.ShowVersion { + if showVersion { printVersion() - os.Exit(0) + return } - config, err := config.LoadConfig(args.ConfigPath) + cfg, err := config.LoadConfig(configPath, skipLdap) if err != nil { log.Fatalf("Failed to load config: %v", err) } - srv, err := routes.NewServer(&config) + srv, err := routes.NewServer(&cfg) if err != nil { log.Printf("Could not create server %s", err) } - srv.Run() + srv.Run(config.CreateContext(context.Background(), &cfg, devMode)) +} + +func printUsage() { + fmt.Printf(`Usage: polybased [OPTIONS] + +Manage polybase database from the web browser. + +Options: + -c Path to config file (default: /etc/polybase/config.cfg) + -v Print version information + -h Print this help message + -skip-ldap Skip LDAP verification + -dev Enable dev mode + +For bug reporting and more information, please see: +https://github.com/alias-asso/polybase-go +`) +} + +func printVersion() { + fmt.Printf("polybased version %s\n", version) } diff --git a/polybased/routes/admin.go b/polybased/routes/admin.go index ca859f0..45c1db7 100644 --- a/polybased/routes/admin.go +++ b/polybased/routes/admin.go @@ -1,7 +1,6 @@ package routes import ( - "context" "fmt" "log" "net/http" @@ -9,12 +8,12 @@ import ( "strings" "github.com/alias-asso/polybase-go/libpolybase" + "github.com/alias-asso/polybase-go/polybased/config" "github.com/alias-asso/polybase-go/views" ) -// getAdmin func (s *Server) getAdmin(w http.ResponseWriter, r *http.Request) { - username := r.Context().Value("username").(string) + username := config.GetUsername(r.Context()) courses, err := s.pb.ListCourse(r.Context(), true, nil, nil, nil, nil) if err != nil { @@ -37,7 +36,6 @@ func (s *Server) getAdmin(w http.ResponseWriter, r *http.Request) { } } -// getAdminCoursesNew func (s *Server) getAdminCoursesNew(w http.ResponseWriter, r *http.Request) { err := views.NewCourseForm().Render(r.Context(), w) if err != nil { @@ -46,7 +44,6 @@ func (s *Server) getAdminCoursesNew(w http.ResponseWriter, r *http.Request) { } } -// getAdminCoursesEdit func (s *Server) getAdminCoursesEdit(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/edit/", r) if err != nil { @@ -68,7 +65,6 @@ func (s *Server) getAdminCoursesEdit(w http.ResponseWriter, r *http.Request) { } } -// getAdminCoursesDelete func (s *Server) getAdminCoursesDelete(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/delete/", r) if err != nil { @@ -90,7 +86,6 @@ func (s *Server) getAdminCoursesDelete(w http.ResponseWriter, r *http.Request) { } } -// getAdminPacksNew func (s *Server) getAdminPacksNew(w http.ResponseWriter, r *http.Request) { courses, err := s.pb.ListCourse(r.Context(), false, nil, nil, nil, nil) if err != nil { @@ -105,7 +100,6 @@ func (s *Server) getAdminPacksNew(w http.ResponseWriter, r *http.Request) { } } -// getAdminPacksEdit func (s *Server) getAdminPacksEdit(w http.ResponseWriter, r *http.Request) { id, err := parsePackUrl("/admin/packs/edit/", r) if err != nil { @@ -133,7 +127,6 @@ func (s *Server) getAdminPacksEdit(w http.ResponseWriter, r *http.Request) { } } -// getAdminPacksDelete func (s *Server) getAdminPacksDelete(w http.ResponseWriter, r *http.Request) { id, err := parsePackUrl("/admin/packs/delete/", r) if err != nil { @@ -196,13 +189,11 @@ func (s *Server) getAdminPack(w http.ResponseWriter, r *http.Request) { } } -// getAdminPacksNew -func (s *Server) getAdminStatistics(w http.ResponseWriter, r *http.Request) { - log.Printf("Get admin statistics - Config: %+v, Polybase: %+v", s.cfg, s.pb) - w.Write([]byte("Get admin statistics")) -} +//func (s *Server) getAdminStatistics(w http.ResponseWriter, r *http.Request) { +// log.Printf("Get admin statistics - Config: %+v, Polybase: %+v", s.cfg, s.pb) +// w.Write([]byte("Get admin statistics")) +//} -// postAdminCourses func (s *Server) postAdminCourses(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/", r) if err != nil { @@ -211,7 +202,7 @@ func (s *Server) postAdminCourses(w http.ResponseWriter, r *http.Request) { return } - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) err = r.ParseForm() if err != nil { @@ -307,7 +298,6 @@ func (s *Server) postAdminCourses(w http.ResponseWriter, r *http.Request) { } } -// putAdminCourses func (s *Server) putAdminCourses(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/", r) if err != nil { @@ -316,7 +306,7 @@ func (s *Server) putAdminCourses(w http.ResponseWriter, r *http.Request) { return } - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) err = r.ParseForm() if err != nil { @@ -418,7 +408,6 @@ func (s *Server) putAdminCourses(w http.ResponseWriter, r *http.Request) { } } -// deleteAdminCourses func (s *Server) deleteAdminCourses(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/", r) if err != nil { @@ -427,7 +416,7 @@ func (s *Server) deleteAdminCourses(w http.ResponseWriter, r *http.Request) { return } - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) _, err = s.pb.GetCourse(r.Context(), id) exists := true @@ -475,7 +464,6 @@ func (s *Server) deleteAdminCourses(w http.ResponseWriter, r *http.Request) { } } -// patchAdminCoursesQuantity func (s *Server) patchAdminCoursesQuantity(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/", r) if err != nil { @@ -484,7 +472,7 @@ func (s *Server) patchAdminCoursesQuantity(w http.ResponseWriter, r *http.Reques return } - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) delta, err := strconv.Atoi(r.URL.Query().Get("delta")) if err != nil { @@ -507,7 +495,6 @@ func (s *Server) patchAdminCoursesQuantity(w http.ResponseWriter, r *http.Reques } } -// patchAdminCoursesVisibility func (s *Server) patchAdminCoursesVisibility(w http.ResponseWriter, r *http.Request) { id, err := parseCourseUrl("/admin/courses/", r) if err != nil { @@ -516,7 +503,7 @@ func (s *Server) patchAdminCoursesVisibility(w http.ResponseWriter, r *http.Requ return } - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) visibility, err := strconv.ParseBool(r.URL.Query().Get("visibility")) if err != nil { @@ -547,7 +534,7 @@ func (s *Server) patchAdminPacksQuantity(w http.ResponseWriter, r *http.Request) return } - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) delta, err := strconv.Atoi(r.URL.Query().Get("delta")) if err != nil { @@ -585,7 +572,7 @@ func (s *Server) patchAdminPacksQuantity(w http.ResponseWriter, r *http.Request) } func (s *Server) postAdminPacks(w http.ResponseWriter, r *http.Request) { - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) // Parse form data err := r.ParseForm() @@ -661,7 +648,7 @@ func (s *Server) postAdminPacks(w http.ResponseWriter, r *http.Request) { func (s *Server) putAdminPacks(w http.ResponseWriter, r *http.Request) { log.Println("putAdminPacks") - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) // Parse the pack ID from URL id, err := parsePackUrl("/admin/packs/", r) @@ -746,7 +733,7 @@ func (s *Server) putAdminPacks(w http.ResponseWriter, r *http.Request) { } func (s *Server) deleteAdminPacks(w http.ResponseWriter, r *http.Request) { - username := getUsernameFromContext(r.Context()) + username := config.GetUsername(r.Context()) id, err := parsePackUrl("/admin/packs/", r) if err != nil { @@ -783,11 +770,3 @@ func (s *Server) deleteAdminPacks(w http.ResponseWriter, r *http.Request) { log.Printf("Failed to render template: %v", err) } } - -func getUsernameFromContext(ctx context.Context) string { - username, ok := ctx.Value("username").(string) - if !ok { - return "" - } - return username -} diff --git a/polybased/routes/middleware.go b/polybased/routes/middleware.go index f8b5108..7b31459 100644 --- a/polybased/routes/middleware.go +++ b/polybased/routes/middleware.go @@ -4,41 +4,24 @@ import ( "context" "net/http" - "github.com/golang-jwt/jwt/v5" + "github.com/alias-asso/polybase-go/polybased/config" ) +func (s *Server) withContext(ctx context.Context, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx = config.SetAuth(ctx, r) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + func (s *Server) withAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("X-Auth-Token") - if err != nil { - http.Redirect(w, r, "/", http.StatusSeeOther) - return - } - - // Parse and validate the token - type Claims struct { - Username string `json:"username"` - jwt.RegisteredClaims - } - - token, err := jwt.ParseWithClaims(cookie.Value, &Claims{}, func(token *jwt.Token) (any, error) { - return []byte(s.cfg.Auth.JWTSecret), nil - }) - - if err != nil || !token.Valid { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - - // Get username from claims - claims, ok := token.Claims.(*Claims) - if !ok { + connected := config.IsLogged(r.Context()) + if !connected { http.Redirect(w, r, "/login", http.StatusSeeOther) return } - // Add username to request context - ctx := context.WithValue(r.Context(), "username", claims.Username) - next(w, r.WithContext(ctx)) + next(w, r) } } diff --git a/polybased/routes/public.go b/polybased/routes/public.go index 2957b6e..a03db75 100644 --- a/polybased/routes/public.go +++ b/polybased/routes/public.go @@ -5,13 +5,12 @@ import ( "net/http" "time" + "github.com/alias-asso/polybase-go/polybased/config" "github.com/alias-asso/polybase-go/views" - "github.com/golang-jwt/jwt/v5" ) -// getHome func (s *Server) getHome(w http.ResponseWriter, r *http.Request) { - if ok := s.isLoggedIn(r); ok { + if ok := config.IsLogged(r.Context()); ok { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } @@ -22,6 +21,10 @@ func (s *Server) getHome(w http.ResponseWriter, r *http.Request) { log.Printf("%s", err) return } + for i, c := range courses { + c.Semester = "Semestre " + string([]rune(c.Semester)[1:]) + courses[i] = c + } s.count += 1 @@ -32,7 +35,6 @@ func (s *Server) getHome(w http.ResponseWriter, r *http.Request) { } } -// getLogin func (s *Server) getLogin(w http.ResponseWriter, r *http.Request) { err := views.Login().Render(r.Context(), w) if err != nil { @@ -41,7 +43,6 @@ func (s *Server) getLogin(w http.ResponseWriter, r *http.Request) { } } -// postAuth func (s *Server) postAuth(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "Échec de l'analyse du formulaire", http.StatusBadRequest) @@ -51,7 +52,9 @@ func (s *Server) postAuth(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") password := r.FormValue("password") - authorized, err := authenticate(username, password, s.cfg) + cfg := config.GetConfig(r.Context()) + + authorized, err := authenticate(username, password, cfg) if err != nil { log.Print(err) http.Error(w, "Service LDAP temporairement indisponible", http.StatusInternalServerError) @@ -64,13 +67,13 @@ func (s *Server) postAuth(w http.ResponseWriter, r *http.Request) { return } - token, err := generateToken(username, s.cfg) + token, err := generateToken(username, cfg) if err != nil { http.Error(w, "Erreur interne du serveur", http.StatusInternalServerError) return } - expiry, err := time.ParseDuration(s.cfg.Auth.JWTExpiry) + expiry, err := time.ParseDuration(cfg.Auth.JWTExpiry) if err != nil { http.Error(w, "Erreur interne du serveur", http.StatusInternalServerError) return @@ -97,25 +100,3 @@ func (s *Server) getNotFound(w http.ResponseWriter, r *http.Request) { log.Printf("Failed to render template: %v", err) } } - -func (s *Server) isLoggedIn(r *http.Request) bool { - cookie, err := r.Cookie("X-Auth-Token") - if err != nil { - return false - } - - type Claims struct { - Username string `json:"username"` - jwt.RegisteredClaims - } - - token, err := jwt.ParseWithClaims(cookie.Value, &Claims{}, func(token *jwt.Token) (any, error) { - return []byte(s.cfg.Auth.JWTSecret), nil - }) - if err != nil || !token.Valid { - return false - } - - _, ok := token.Claims.(*Claims) - return ok -} diff --git a/polybased/routes/routes.go b/polybased/routes/routes.go index 3fa5590..2230428 100644 --- a/polybased/routes/routes.go +++ b/polybased/routes/routes.go @@ -1,8 +1,11 @@ package routes import ( + "log" "net/http" + "os" + "github.com/alias-asso/polybase-go/polybased/config" "github.com/alias-asso/polybase-go/static" ) @@ -49,12 +52,19 @@ func (s *Server) registerRoutes() { func (s *Server) registerStatic() { fs := http.FileServer(static.FileSystem()) staticHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if s.cfg.Server.Mode == "dev" { - w.Header().Set("Cache-Control", "public, max-age=0") - } else { + if !config.IsDev(r.Context()) { w.Header().Set("Cache-Control", "public, max-age=63072000") + } else { + w.Header().Set("Cache-Control", "public, max-age=0") + pwd, err := os.Getwd() + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + log.Printf("error: %v", err) + } + fs = http.FileServer(http.FS(os.DirFS(pwd + "/static/"))) } http.StripPrefix("/static/", fs).ServeHTTP(w, r) + }) s.mux.Handle("GET /static/", staticHandler) } diff --git a/polybased/routes/server.go b/polybased/routes/server.go index 4881e5d..7dbd85f 100644 --- a/polybased/routes/server.go +++ b/polybased/routes/server.go @@ -1,6 +1,7 @@ package routes import ( + "context" "database/sql" "fmt" "log" @@ -15,7 +16,6 @@ import ( type Server struct { mux *http.ServeMux addr string - cfg *config.Config pb libpolybase.Polybase count int } @@ -32,7 +32,6 @@ func NewServer(cfg *config.Config) (*Server, error) { srv := &Server{ mux: http.NewServeMux(), addr: fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port), - cfg: cfg, pb: pb, count: 0, } @@ -43,9 +42,9 @@ func NewServer(cfg *config.Config) (*Server, error) { return srv, nil } -func (s *Server) Run() { +func (s *Server) Run(ctx context.Context) { log.Printf("Starting server on %s", s.addr) - if err := http.ListenAndServe(s.addr, s.mux); err != nil { + if err := http.ListenAndServe(s.addr, s.withContext(ctx, s.mux)); err != nil { log.Fatalf("Error when listening and serving %s", err) } } diff --git a/polybased/version.go b/polybased/version.go deleted file mode 100644 index 5882056..0000000 --- a/polybased/version.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import "fmt" - -const version = "0.1.0" - -func printVersion() { - fmt.Printf("polybased version %s\n", version) -} diff --git a/static/css/main.css b/static/css/main.css index c91b347..5be5d87 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -35,11 +35,11 @@ --base-900: #0f0f0f; --color-course-cm: var(--color-accent-100); --color-course-cm-light: var(--color-accent-600); - --color-course-td: hsl(145, 40%, 80%); - --color-course-td-light: hsl(145, 40%, 10%); + --color-course-td: hsl(337, 100%, 89%); + --color-course-td-light: hsl(337, 100%, 15%); --color-course-tme: hsl(170, 60%, 80%); --color-course-tme-light: hsl(170, 60%, 10%); - --color-course-memento: hsl(247, 65%, 85%); + --color-course-memento: hsl(247, 75%, 80%); --color-course-memento-light: hsl(247, 65%, 10%); } @@ -55,12 +55,12 @@ --base-900: #f0f0f0; --color-course-cm: var(--color-accent-200); --color-course-cm-light: var(--color-accent-700); - --color-course-td: var(--color-green-500); - --color-course-td-light: var(--color-green-950); + --color-course-td: hsl(337, 100%, 80%); + --color-course-td-light: hsl(337, 100%, 15%); --color-course-tme: var(--color-cyan-400); --color-course-tme-light: var(--color-cyan-950); - --color-course-memento: var(--color-indigo-400); - --color-course-memento-light: var(--color-indigo-950); + --color-course-memento: hsl(247, 80%, 65%); + --color-course-memento-light: hsl(247, 50%, 5%); } [data-kind="cours"] { diff --git a/static/js/htmx.min.js b/static/js/htmx.min.js deleted file mode 100644 index 423cf01..0000000 --- a/static/js/htmx.min.js +++ /dev/null @@ -1 +0,0 @@ -var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.3"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=de;Q.ajax=Rn;Q.find=r;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=h;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:dn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:i,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:dt,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Ft};const o=["get","post","put","delete","patch"];const R=o.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function i(e,t){while(e&&!t(e)){e=c(e)}return e||null}function H(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;i(t,function(e){return!!(r=H(t,ue(e),n))});if(r!=="unset"){return r}}function d(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function N(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function A(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(A(e)){const t=N(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){C(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){C(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ue(e),ge(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(f(e),ge(t.substr(5)))]}else if(t==="next"){return[ue(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[pe(e,ge(t.substr(5)),!!n)]}else if(t==="previous"){return[ue(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[me(e,ge(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[m(e,!!n)]}else if(t==="host"){return[e.getRootNode().host]}else if(t.indexOf("global ")===0){return p(e,t.slice(7),true)}else{return M(f(m(e,!!n)).querySelectorAll(ge(t)))}}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){C('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(i(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=r("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=r("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=r("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=r("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ne(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ae(f(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=$(d(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function w(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=w(e,Qe).trim();e.shift()}else{t=w(e,b)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{w(o,v);const l=o.length;const c=w(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};w(o,v);u.pollInterval=h(w(o,/[,\[\s]/));w(o,v);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}w(o,v);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(w(o,b))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=w(o,b);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=rt(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(w(o,b))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=w(o,b)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=w(o,b)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(d(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function dt(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(d(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ht(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(ht(l,e)){return}if(a||dt(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!d(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){de(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){de(l,"htmx:trigger");c(l,e)},u.delay)}else{de(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(o,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function Nt(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function At(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!d(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:Nn(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function dn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function hn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{C("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||d(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||!e&&!y(r.source)){e=ve}return he(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function Nn(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Se(r,"hx-sync")}else{d=ue(ae(r,I))}h=(A[1]||"drop").trim();u=ie(d);if(h==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(h==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const W=h.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!de(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=dn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:Nn(w),unfilteredFormData:v,unfilteredParameters:Nn(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function An(e,t){const n=t.xhr;let r=null;let o=null;if(O(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(O(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(O(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/static/static.go b/static/static.go index ccc0a3e..b7baec2 100644 --- a/static/static.go +++ b/static/static.go @@ -6,7 +6,7 @@ import ( "net/http" ) -//go:embed css/styles.css js/htmx.min.js svg/up.svg svg/down.svg svg/left.svg svg/right.svg svg/moon.svg svg/sun.svg svg/cross.svg svg/hide.svg svg/logo1.svg svg/minus.svg svg/pencil.svg svg/plus.svg svg/show.svg svg/favicon.ico +//go:embed css/styles.css js/scripts.js svg var content embed.FS func FileSystem() http.FileSystem { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/views/base.templ b/views/base.templ index 984f617..f1ef75b 100644 --- a/views/base.templ +++ b/views/base.templ @@ -10,7 +10,7 @@ templ Base(enableJS bool, enableIndex bool) { if enableJS { - + } if !enableIndex { diff --git a/views/course_card.templ b/views/course_card.templ index 20e8e2d..613feb8 100644 --- a/views/course_card.templ +++ b/views/course_card.templ @@ -55,7 +55,7 @@ templ CoursePart(course libpolybase.Course) { // CourseName presents the course title in a two-line clamped format with hover // tooltip for longer names. templ CourseName(course libpolybase.Course) { -

{ course.Kind } - { course.Name }

+

{ course.Kind } - { course.Name }

} // CourseAdminControl provides administrative functionality including edit, diff --git a/views/grid.templ b/views/grid.templ index 45244a9..0cd1cff 100644 --- a/views/grid.templ +++ b/views/grid.templ @@ -3,7 +3,7 @@ package views import "github.com/alias-asso/polybase-go/libpolybase" templ Grid(semesterGroups []SemesterGroup, packs []libpolybase.Pack, isAdmin bool) { -
+
if packs != nil {

Packs