diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..57dc561 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +node_modules +docs + +internal/web/static/styles.css +internal/web/static/js/*.js +Dockerfile +README.md +LICENSE +.env* +.prettier* +.gitignore diff --git a/.env b/.env new file mode 100644 index 0000000..d976916 --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +# If a value is left empty, it will get set to the default during application startup. + +LDAP_SERVER="" +LDAP_IS_AD="" +LDAP_BASE_DN="" +LDAP_READONLY_USER="" +LDAP_READONLY_PASSWORD="" + +MIN_LENGTH="" +MIN_NUMBERS="" +MIN_SYMBOLS="" +MIN_UPPERCASE="" +MIN_LOWERCASE="" +PASSWORD_CAN_INCLUDE_USERNAME="" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f1b219b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..00fd604 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,135 @@ +name: Check + +on: + release: + types: [published] + pull_request: + push: + branches: + - main + +jobs: + go-test: + name: Run Go tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.20" + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - uses: pnpm/action-setup@v2 + name: Install pnpm + id: pnpm-install + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install && go mod download + + - name: Check types + run: pnpm build:assets + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... + + types: + name: Check types + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - uses: pnpm/action-setup@v2 + name: Install pnpm + id: pnpm-install + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Check types + run: pnpm js:build + + formatting: + name: Check formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - uses: pnpm/action-setup@v2 + name: Install pnpm + id: pnpm-install + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Check formatting + run: pnpm prettier --check . diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..f710cf5 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,66 @@ +name: Docker + +on: + release: + types: [published] + schedule: + - cron: "0 0 * * 0" + pull_request: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "${{ github.repository_owner }}/ldap-selfservice-password-changer" + +jobs: + docker: + name: Build Images + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache docker layers + uses: actions/cache@v3 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Gather Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{version}}.{{major}} + labels: | + cache-from=type=local,src=/tmp/.buildx-cache + cache-to=type=local,dest=/tmp/.buildx-cache + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm/v7,linux/arm64 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ad4900 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +internal/web/static/js/*.js +internal/web/static/styles.css +.env.local +*.bbolt \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2f788d5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +internal/web/static/styles.css +internal/web/static/js/*.js +pnpm-lock.yaml diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..e6ca573 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,16 @@ +module.exports = { + printWidth: 120, + trailingComma: "none", + tabWidth: 2, + semi: true, + singleQuote: false, + plugins: [require("prettier-plugin-tailwindcss"), require("prettier-plugin-go-template")], + overrides: [ + { + files: ["*.html"], + options: { + parser: "go-template" + } + } + ] +}; diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0f23057 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM --platform=amd64 node:18 AS frontend-builder +WORKDIR /build +RUN npm i -g pnpm + +COPY package.json . +COPY pnpm-lock.yaml . +RUN pnpm i + +COPY . . + +RUN pnpm build:assets + +FROM golang:1.20-alpine AS backend-builder +WORKDIR /build +RUN apk add git + +COPY ./go.mod . +COPY ./go.sum . +RUN go mod download + +COPY . . +COPY --from=frontend-builder /build/internal/web/static/styles.css /build/internal/web/static/styles.css +COPY --from=frontend-builder /build/internal/web/static/js/*.js /build/internal/web/static/js +RUN CGO_ENABLED=0 go build -o /build/ldap-passwd + +FROM alpine:3 AS runner + +COPY --from=backend-builder /build/ldap-passwd /usr/local/bin/ldap-passwd + +ENTRYPOINT [ "/usr/local/bin/ldap-passwd" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f59c096 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Netresearch DTT GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9567beb --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +
+

LDAP Selfservice Password Changer

+ +LDAP Selfservice Password Changer is a web frontend and JSON RPC API for allowing your users to change their own passwords in your LDAP or ActiveDirectory server. + + +
+ +## Running + +### Natively + +If you want to run this service without a Docker container, you have to build it yourself. + +Prerequisites: + +- Go 1.20+ +- Node.js v16+ +- Corepack (`npm i -g corepack`) + +You can configure this via a `.env.local` file or via command options (for more information you can run `./ldap-selfservice-password-changer --help`). + + + +```bash +corepack enable +pnpm i +pnpm build + +./ldap-selfservice-password-changer \ + `# You can also configure these via environment variables,` \ + `# please see the .env file for available options.` \ + -ldap-server ldaps://dc1.example.com:636 -active-directory \ + -readonly-password readonly -readonly-user readonly \ + -base-dn DC=example,DC=com +``` + +### Docker + +We have a Docker image available [here](https://github.com/netresearch/ldap-selfservice-password-changer/pkgs/container/ldap-selfservice-password-changer). + +You can ignore the warning that the service could not load a `.env` file. + + + +```bash +docker run \ + `# Run the password-changer container detached from the current terminal` \ + -d --name ldap-password-changer \ + `# You might want to mount your host SSL certificate directory,` \ + `# if you have a self-signed certificate for your LDAPS connection` \ + -v /etc/ssl/certs:/etc/ssl/certs:ro \ + -p 3000:3000 \ + ghcr.io/netresearch/ldap-selfservice-password-changer \ + `# You can also configure these via environment variables,` \ + `# please see the .env file for available options.` \ + -ldap-server ldaps://dc1.example.com:636 -active-directory \ + -readonly-password readonly -readonly-user readonly \ + -base-dn DC=example,DC=com +``` + +## Developing + +Prerequisites: + +- Go 1.20+ +- Node.js v16+ +- Corepack (`npm i -g corepack`) + +```bash +corepack enable + +# Install dependencies +pnpm i + +touch .env.local +# Edit the `.env.local` to include the arguments, you want to give to the application. +# Required are: +# - LDAP_SERVER +# - LDAP_BASE_DN +# - LDAP_READONLY_USER +# - LDAP_READONLY_PASSWORD + +# Running normally +pnpm start + +# Running in dev mode +# This will restart the application every time, you make +# a change. +pnpm dev +``` + +## License + +LDAP Selfservice Password Changer is licensed under the MIT license, for more information please refer to the [included LICENSE file](LICENSE). + +## Contributing + +Feel free to contribute by creating a Pull Request! + +This project uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages and the default `gofmt` and `prettier` formatting rules. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ab54073 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,4 @@ +# Architecture + +The following architecture will be used: +![Architecture](./architecture.png) diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 0000000..3b531d1 Binary files /dev/null and b/docs/architecture.png differ diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..6810d87 Binary files /dev/null and b/docs/logo.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d084f9a --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module github.com/netresearch/ldap-selfservice-password-changer + +go 1.20 + +require ( + github.com/gofiber/fiber/v2 v2.48.0 + github.com/gofiber/storage/bbolt v1.3.5 + github.com/gofiber/template/html/v2 v2.0.5 + github.com/joho/godotenv v1.5.1 + github.com/netresearch/simple-ldap-go v0.0.0-20231002103847-cb56d7d4e6c7 + github.com/rs/zerolog v1.29.1 +) + +require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/go-ldap/ldap/v3 v3.4.6 // indirect + github.com/gofiber/template v1.8.2 // indirect + github.com/gofiber/utils v1.1.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.48.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.etcd.io/bbolt v1.3.7 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0eab55c --- /dev/null +++ b/go.sum @@ -0,0 +1,119 @@ +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/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= +github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0= +github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8= +github.com/gofiber/storage/bbolt v1.3.5 h1:9ZDMTbeah5tfj3eX+hFu3F1AHiBO117ce3Gel7tkxlk= +github.com/gofiber/storage/bbolt v1.3.5/go.mod h1:GibrOAQTFOzzzWWVCgq+V+gS8dUbaPeAMGI4FNZ32sI= +github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk= +github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= +github.com/gofiber/template/html/v2 v2.0.5 h1:BKLJ6Qr940NjntbGmpO3zVa4nFNGDCi/IfUiDB9OC20= +github.com/gofiber/template/html/v2 v2.0.5/go.mod h1:RCF14eLeQDCSUPp0IGc2wbSSDv6yt+V54XB/+Unz+LM= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/netresearch/simple-ldap-go v0.0.0-20231002091551-aa3c68c43a90 h1:A4sgcCe3ZrT/FpgsUY8tCfOHae6yI5VvHauo9zxitcQ= +github.com/netresearch/simple-ldap-go v0.0.0-20231002091551-aa3c68c43a90/go.mod h1:v00qbwupQnx2Mk3oJoJylMWD54yJk6vHsbFj5RIOK5o= +github.com/netresearch/simple-ldap-go v0.0.0-20231002094326-383f3eda4fe0 h1:C8jIl/OEDMHwu/OSZ5Pd2pDk2ePx8JaX+9YLLioIfIA= +github.com/netresearch/simple-ldap-go v0.0.0-20231002094326-383f3eda4fe0/go.mod h1:v00qbwupQnx2Mk3oJoJylMWD54yJk6vHsbFj5RIOK5o= +github.com/netresearch/simple-ldap-go v0.0.0-20231002095049-edec788b6515 h1:1ZJoAqs/0F1Dn3dNqiMJleurJy4Wk5mwNT+7QeQ30p8= +github.com/netresearch/simple-ldap-go v0.0.0-20231002095049-edec788b6515/go.mod h1:v00qbwupQnx2Mk3oJoJylMWD54yJk6vHsbFj5RIOK5o= +github.com/netresearch/simple-ldap-go v0.0.0-20231002103847-cb56d7d4e6c7 h1:hN2eW111sh8hpPT4GwIXafbSqHtJfAnLGc+hZAQehnk= +github.com/netresearch/simple-ldap-go v0.0.0-20231002103847-cb56d7d4e6c7/go.mod h1:v00qbwupQnx2Mk3oJoJylMWD54yJk6vHsbFj5RIOK5o= +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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= +github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= +github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/ldap_cache/cache.go b/internal/ldap_cache/cache.go new file mode 100644 index 0000000..d6c0476 --- /dev/null +++ b/internal/ldap_cache/cache.go @@ -0,0 +1,182 @@ +package ldap_cache + +import ( + "sync" + "time" + + ldap "github.com/netresearch/simple-ldap-go" + "github.com/rs/zerolog/log" +) + +type Cache struct { + stop chan struct{} + + m sync.RWMutex + client *ldap.LDAP + users []ldap.User + groups []ldap.Group +} + +type FullLDAPUser struct { + ldap.User + Groups []ldap.Group +} + +type FullLDAPGroup struct { + ldap.Group + Members []ldap.User +} + +func New(client *ldap.LDAP) *Cache { + return &Cache{ + stop: make(chan struct{}), + client: client, + users: make([]ldap.User, 0), + groups: make([]ldap.Group, 0), + } +} + +func (l *Cache) Run() { + t := time.NewTicker(30 * time.Second) + + l.refresh() + + for { + select { + case <-l.stop: + t.Stop() + log.Info().Msg("LDAP cache stopped") + + return + case <-t.C: + l.refresh() + } + } +} + +func (l *Cache) Stop() { + l.stop <- struct{}{} +} + +func (l *Cache) refreshUsers() error { + users, err := l.client.FindUsers() + if err != nil { + return err + } + + l.m.Lock() + l.users = users + l.m.Unlock() + + return nil +} + +func (l *Cache) refreshGroups() error { + groups, err := l.client.FindGroups() + if err != nil { + return err + } + + l.m.Lock() + l.groups = groups + l.m.Unlock() + + return nil +} + +func (l *Cache) refresh() { + if err := l.refreshUsers(); err != nil { + log.Error().Err(err).Send() + } + + if err := l.refreshGroups(); err != nil { + log.Error().Err(err).Send() + } + + log.Debug().Msgf("Refreshed LDAP cache with %d users and %d groups", len(l.users), len(l.groups)) +} + +func (l *Cache) FindUsers() []ldap.User { + l.m.RLock() + defer l.m.RUnlock() + + return l.users +} + +func (l *Cache) FindUserByDN(dn string) (ldap.User, error) { + l.m.RLock() + defer l.m.RUnlock() + + for _, user := range l.users { + if user.DN == dn { + return user, nil + } + } + + return ldap.User{}, ldap.ErrUserNotFound +} + +func (l *Cache) FindUserBySAMAccountName(samAccountName string) (ldap.User, error) { + l.m.RLock() + defer l.m.RUnlock() + + for _, user := range l.users { + if user.SAMAccountName == samAccountName { + return user, nil + } + } + + return ldap.User{}, ldap.ErrUserNotFound +} + +func (l *Cache) FindGroups() []ldap.Group { + l.m.RLock() + defer l.m.RUnlock() + + return l.groups +} + +func (l *Cache) FindGroupByDN(dn string) (ldap.Group, error) { + l.m.RLock() + defer l.m.RUnlock() + + for _, group := range l.groups { + if group.DN == dn { + return group, nil + } + } + + return ldap.Group{}, ldap.ErrGroupNotFound +} + +func (l *Cache) PopulateGroupsForUser(user *ldap.User) *FullLDAPUser { + full := &FullLDAPUser{ + User: *user, + Groups: make([]ldap.Group, 0), + } + + for _, groupDN := range user.Groups { + group, err := l.FindGroupByDN(groupDN) + if err == nil { + full.Groups = append(full.Groups, group) + } + } + + return full +} + +func (l *Cache) PopulateUsersForGroup(group *ldap.Group) *FullLDAPGroup { + full := &FullLDAPGroup{ + Group: *group, + Members: make([]ldap.User, 0), + } + + for _, userDN := range group.Members { + user, err := l.FindUserByDN(userDN) + if err == nil { + full.Members = append(full.Members, user) + } + } + + return full +} diff --git a/internal/options/app.go b/internal/options/app.go new file mode 100644 index 0000000..4e15aaa --- /dev/null +++ b/internal/options/app.go @@ -0,0 +1,93 @@ +package options + +import ( + "flag" + "fmt" + "os" + "strconv" + + "github.com/joho/godotenv" + "github.com/rs/zerolog/log" +) + +type Opts struct { + LdapServer string + IsActiveDirectory bool + BaseDN string + ReadonlyUser string + ReadonlyPassword string + + DBPath string +} + +func panicWhenEmpty(name string, value *string) { + if *value == "" { + log.Fatal().Msgf("err: The option --%s is required", name) + } +} + +func envStringOrDefault(name, d string) string { + if v, exists := os.LookupEnv(name); exists && v != "" { + return v + } + + return d +} + +func envIntOrDefault(name string, d uint64) uint { + raw := envStringOrDefault(name, fmt.Sprintf("%v", d)) + + v, err := strconv.ParseUint(raw, 10, 8) + if err != nil { + log.Fatal().Msgf("err: could not parse environment variable \"%s\" (containing \"%s\") as uint: %v", name, raw, err) + } + + return uint(v) +} + +func envBoolOrDefault(name string, d bool) bool { + raw := envStringOrDefault(name, fmt.Sprintf("%v", d)) + + v2, err := strconv.ParseBool(raw) + if err != nil { + log.Fatal().Msgf("err: could not parse environment variable \"%s\" (containing \"%s\") as bool: %v", name, raw, err) + } + + return v2 +} + +func Parse() *Opts { + if err := godotenv.Load(".env.local", ".env"); err != nil { + log.Warn().Err(err).Msg("could not load .env file") + } + + var ( + fLdapServer = flag.String("ldap-server", envStringOrDefault("LDAP_SERVER", ""), "LDAP server URI, has to begin with `ldap://` or `ldaps://`. If this is an ActiveDirectory server, this *has* to be `ldaps://`.") + fIsActiveDirectory = flag.Bool("active-directory", envBoolOrDefault("LDAP_IS_AD", false), "Mark the LDAP server as ActiveDirectory.") + fBaseDN = flag.String("base-dn", envStringOrDefault("LDAP_BASE_DN", ""), "Base DN of your LDAP directory.") + fReadonlyUser = flag.String("readonly-user", envStringOrDefault("LDAP_READONLY_USER", ""), "User that can read all users in your LDAP directory.") + fReadonlyPassword = flag.String("readonly-password", envStringOrDefault("LDAP_READONLY_PASSWORD", ""), "Password for the readonly user.") + + fDBPath = flag.String("db-path", envStringOrDefault("DB_PATH", "db.bbolt"), "Path to the SQLite database file.") + ) + + if !flag.Parsed() { + flag.Parse() + } + + panicWhenEmpty("ldap-server", fLdapServer) + panicWhenEmpty("base-dn", fBaseDN) + panicWhenEmpty("readonly-user", fReadonlyUser) + panicWhenEmpty("readonly-password", fReadonlyPassword) + panicWhenEmpty("db-path", fDBPath) + + return &Opts{ + LdapServer: *fLdapServer, + IsActiveDirectory: *fIsActiveDirectory, + BaseDN: *fBaseDN, + ReadonlyUser: *fReadonlyUser, + ReadonlyPassword: *fReadonlyPassword, + + DBPath: *fDBPath, + } +} diff --git a/internal/web/auth.go b/internal/web/auth.go new file mode 100644 index 0000000..545f4fe --- /dev/null +++ b/internal/web/auth.go @@ -0,0 +1,57 @@ +package web + +import ( + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog/log" +) + +func (a *App) logoutHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + if err := sess.Destroy(); err != nil { + return handle500(c, err) + } + + return c.Redirect("/login") +} + +func (a *App) loginHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + username := c.Query("username") + password := c.Query("password") + + if username != "" && password != "" { + user, err := a.ldap.CheckPasswordForSAMAccountName(username, password) + if err != nil { + log.Error().Err(err).Msg("could not check password") + + return c.Render("views/login", fiber.Map{ + "session": sess, + "title": "Login", + "headscripts": "", + "flashes": []string{"Invalid username or password"}, + }, "layouts/base") + } + + sess.Set("username", user.SAMAccountName) + sess.Set("password", password) + if err := sess.Save(); err != nil { + return handle500(c, err) + } + + return c.Redirect("/") + } + + return c.Render("views/login", fiber.Map{ + "session": sess, + "title": "Login", + "headscripts": "", + }, "layouts/base") +} diff --git a/internal/web/groups.go b/internal/web/groups.go new file mode 100644 index 0000000..e267478 --- /dev/null +++ b/internal/web/groups.go @@ -0,0 +1,62 @@ +package web + +import ( + "net/url" + + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog/log" +) + +func (a *App) groupsHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + if sess.Fresh() { + return c.Redirect("/login") + } + + groups := a.ldapCache.FindGroups() + + return c.Render("views/groups", fiber.Map{ + "session": sess, + "title": "All groups", + "activePage": "/groups", + "headscripts": "", + "groups": groups, + }, "layouts/logged-in") +} + +func (a *App) groupHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + if sess.Fresh() { + return c.Redirect("/login") + } + + groupDN, err := url.PathUnescape(c.Params("groupDN")) + if err != nil { + return handle500(c, err) + } + + thinGroup, err := a.ldapCache.FindGroupByDN(groupDN) + if err != nil { + return handle500(c, err) + } + + group := a.ldapCache.PopulateUsersForGroup(&thinGroup) + + log.Debug().Interface("group", group).Msg("Populated group") + + return c.Render("views/group", fiber.Map{ + "session": sess, + "title": "All groups", + "activePage": "/groups", + "headscripts": "", + "group": group, + }, "layouts/logged-in") +} diff --git a/internal/web/layouts/base.html b/internal/web/layouts/base.html new file mode 100644 index 0000000..e94dcd0 --- /dev/null +++ b/internal/web/layouts/base.html @@ -0,0 +1,29 @@ + + + + {{ .title }} - LDAP Manager + + + + + + + + + + + + + + + + {{ .headscripts }} + + + + + + + {{ embed }} + + diff --git a/internal/web/layouts/logged-in.html b/internal/web/layouts/logged-in.html new file mode 100644 index 0000000..d0e2105 --- /dev/null +++ b/internal/web/layouts/logged-in.html @@ -0,0 +1,53 @@ + + + + {{ .title }} - LDAP Manager + + + + + + + + + + + + + + + + {{ .headscripts }} + + + + + + + + +
+ {{ embed }} +
+ + diff --git a/internal/web/server.go b/internal/web/server.go new file mode 100644 index 0000000..d7bd1ca --- /dev/null +++ b/internal/web/server.go @@ -0,0 +1,124 @@ +package web + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" + "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/storage/bbolt" + "github.com/gofiber/template/html/v2" + "github.com/netresearch/ldap-selfservice-password-changer/internal/ldap_cache" + "github.com/netresearch/ldap-selfservice-password-changer/internal/options" + "github.com/netresearch/ldap-selfservice-password-changer/internal/web/static" + ldap "github.com/netresearch/simple-ldap-go" + "github.com/rs/zerolog/log" +) + +type App struct { + ldap *ldap.LDAP + ldapCache *ldap_cache.Cache + sessionStore *session.Store + fiber *fiber.App +} + +func NewApp(opts *options.Opts) (*App, error) { + ldap, err := ldap.New(opts.LdapServer, opts.BaseDN, opts.ReadonlyUser, opts.ReadonlyPassword, opts.IsActiveDirectory) + if err != nil { + return nil, err + } + + views := html.NewFileSystem(http.FS(templates), ".html") + views.AddFunc("inputOpts", tplInputOpts) + views.AddFunc("navbarActive", tplNavbarActive) + + sessionStorage := bbolt.New(bbolt.Config{ + Database: opts.DBPath, + Bucket: "sessions", + Reset: false, + }) + sessionStore := session.New(session.Config{ + Storage: sessionStorage, + }) + + f := fiber.New(fiber.Config{ + AppName: "netresearch/ldap-manager", + BodyLimit: 4 * 1024, + Views: views, + }) + f.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, + })) + f.Use("/static", filesystem.New(filesystem.Config{ + Root: http.FS(static.Static), + MaxAge: 24 * 60 * 60, + })) + + a := &App{ + ldap: ldap, + ldapCache: ldap_cache.New(ldap), + sessionStore: sessionStore, + fiber: f, + } + + f.Get("/", a.indexHandler) + f.Get("/users", a.usersHandler) + f.Get("/users/:userDN", a.userHandler) + f.Get("/groups", a.groupsHandler) + f.Get("/groups/:groupDN", a.groupHandler) + f.Get("/login", a.loginHandler) + f.Get("/logout", a.logoutHandler) + + f.Use(a.fourOhFourHandler) + + return a, nil +} + +func (a *App) Listen(addr string) error { + go a.ldapCache.Run() + + return a.fiber.Listen(addr) +} + +func handle500(c *fiber.Ctx, err error) error { + log.Error().Err(err).Msg("could not get session") + + return c.Render("views/500", fiber.Map{ + "title": "error", + "headscripts": "", + "error": err.Error(), + }, "layouts/base") +} + +func (a *App) indexHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + // TODO: put this into a middleware + if sess.Fresh() { + return c.Redirect("/login") + } + + user, err := a.ldapCache.FindUserBySAMAccountName(sess.Get("username").(string)) + if err != nil { + return handle500(c, err) + } + + return c.Render("views/index", fiber.Map{ + "session": sess, + "title": "List", + "activePage": "/", + "headscripts": "", + "user": user, + }, "layouts/logged-in") +} + +func (a *App) fourOhFourHandler(c *fiber.Ctx) error { + return c.Render("views/404", fiber.Map{ + "title": "404", + "headscripts": "", + }, "layouts/base") +} diff --git a/internal/web/static/android-chrome-192x192.png b/internal/web/static/android-chrome-192x192.png new file mode 100644 index 0000000..a784471 Binary files /dev/null and b/internal/web/static/android-chrome-192x192.png differ diff --git a/internal/web/static/android-chrome-512x512.png b/internal/web/static/android-chrome-512x512.png new file mode 100644 index 0000000..95d42e4 Binary files /dev/null and b/internal/web/static/android-chrome-512x512.png differ diff --git a/internal/web/static/apple-touch-icon.png b/internal/web/static/apple-touch-icon.png new file mode 100644 index 0000000..314c931 Binary files /dev/null and b/internal/web/static/apple-touch-icon.png differ diff --git a/internal/web/static/browserconfig.xml b/internal/web/static/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/internal/web/static/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/internal/web/static/favicon-16x16.png b/internal/web/static/favicon-16x16.png new file mode 100644 index 0000000..451b87e Binary files /dev/null and b/internal/web/static/favicon-16x16.png differ diff --git a/internal/web/static/favicon-32x32.png b/internal/web/static/favicon-32x32.png new file mode 100644 index 0000000..5e943bd Binary files /dev/null and b/internal/web/static/favicon-32x32.png differ diff --git a/internal/web/static/favicon.ico b/internal/web/static/favicon.ico new file mode 100644 index 0000000..b79e9fc Binary files /dev/null and b/internal/web/static/favicon.ico differ diff --git a/internal/web/static/js/app.ts b/internal/web/static/js/app.ts new file mode 100644 index 0000000..dfb9067 --- /dev/null +++ b/internal/web/static/js/app.ts @@ -0,0 +1,196 @@ +import { + mustBeLongerThan, + mustIncludeLowercase, + mustIncludeNumbers, + mustIncludeSymbols, + mustIncludeUppercase, + mustMatchNewPassword, + mustNotBeEmpty, + mustNotIncludeUsername, + mustNotMatchCurrentPassword, + toggleValidator +} from "./validators.js"; + +type Opts = { + minLength: number; + minNumbers: number; + minSymbols: number; + minUppercase: number; + minLowercase: number; + passwordCanIncludeUsername: boolean; +}; + +export const init = (opts: Opts) => { + const successContainer = document.querySelector("div[data-purpose='successContainer']"); + if (!successContainer) throw new Error("Could not find success container element"); + + const form = document.querySelector("#form"); + if (!form) throw new Error("Could not find form element"); + + const submitButton = form.querySelector("& > div[data-purpose='submit'] > button[type='submit']"); + if (!submitButton) throw new Error("Could not find submit button element"); + + const submitErrorContainer = form.querySelector( + "& > div[data-purpose='submit'] > div[data-purpose='errors']" + ); + if (!submitErrorContainer) throw new Error("Could not find submit error container element"); + + type Field = [string, ((v: string) => string)[]]; + + const fieldsWithValidators = [ + ["username", [mustNotBeEmpty]], + ["current", [mustNotBeEmpty]], + [ + "new", + [ + mustNotBeEmpty, + mustBeLongerThan(opts.minLength), + mustNotMatchCurrentPassword, + toggleValidator(mustNotIncludeUsername, !opts.passwordCanIncludeUsername), + mustIncludeNumbers(opts.minNumbers), + mustIncludeSymbols(opts.minSymbols), + mustIncludeUppercase(opts.minUppercase), + mustIncludeLowercase(opts.minLowercase) + ] + ], + ["new2", [mustNotBeEmpty, mustMatchNewPassword]] + ] satisfies Field[]; + + const fields = fieldsWithValidators.map(([name, validators]) => { + const f = form.querySelector(`#${name}`); + if (!f) throw new Error(`Field "${name}" does not exist`); + + const inputContainer = f.querySelector('div[data-purpose="inputContainer"]'); + if (!inputContainer) throw new Error(`Input container for "${name}" does not exist`); + + const input = inputContainer.querySelector("input"); + if (!input) throw new Error(`Input for "${name}" does not exist`); + + const revealButton = inputContainer.querySelector('button[data-purpose="reveal"]'); + if (!revealButton && input.type === "password") throw new Error(`Reveal button for "${name}" does not exist`); + + const errorContainer = f.querySelector('div[data-purpose="errors"]'); + if (!errorContainer) throw new Error(`Error for "${name}" does not exist`); + + const getValue = () => input.value; + const setErrors = (errors: string[]) => { + errorContainer.innerHTML = ""; + + if (errors.length > 0) { + inputContainer.classList.add("border-red-500"); + } else { + inputContainer.classList.remove("border-red-500"); + } + + for (const error of errors) { + const el = document.createElement("p"); + el.innerText = error; + + errorContainer.appendChild(el); + } + }; + + const validate = () => { + const value = getValue(); + + const errors = validators + .map((validate) => validate(value)) + .reduce((acc, v) => { + if (v.length > 0) acc.push(v); + + return acc; + }, [] as string[]); + + console.log(`Validated "${name}": ${errors.length} error(s)`); + + setErrors(errors); + + return errors.length > 0; + }; + + if (revealButton) { + revealButton.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const newType = input.type === "password" ? "text" : "password"; + const revealed = newType === "text"; + + console.log(`${revealed ? "Showing" : "Hiding"} content of "${name}"`); + + input.type = newType; + f.dataset["revealed"] = revealed.toString(); + }; + } + + return { input, errorContainer, getValue, validate }; + }); + + const toggleFields = (enabled: boolean) => { + [submitButton, ...fields.map(({ input }) => input)].forEach((el) => (el.disabled = !enabled)); + submitButton.dataset["loading"] = (!enabled).toString(); + }; + + form.onsubmit = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + const [username, oldPassword, newPassword] = fields.map((f) => f.getValue()); + + const hasErrors = fields.map(({ validate }) => validate()).some((e) => e === true); + submitButton.disabled = hasErrors; + if (hasErrors) return; + + console.log("Changing password..."); + toggleFields(false); + + try { + const res = await fetch("/api/rpc", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + method: "change-password", + params: [username, oldPassword, newPassword] + }) + }); + + const body = await res.text(); + + if (!res.ok) { + let err = body; + + try { + const parsed = JSON.parse(body); + + err = parsed.data[0]; + } catch (e) {} + + throw new Error(`An error occurred: ${err}`); + } + + console.log("Changed successfully"); + + form.style.display = "none"; + successContainer.style.display = "block"; + } catch (e) { + console.error(e); + + submitErrorContainer.innerText = (e as Error).message; + + // Re-enable inputs but keep the submit button disabled, + // since we know that this isn't going to work. After the validators + // successfully re-run, it will enable the submit button again. + toggleFields(true); + submitButton.disabled = true; + } + }; + + form.onchange = (e) => { + e.stopPropagation(); + + const hasErrors = fields.map(({ validate }) => validate()).some((e) => e === true); + submitButton.disabled = hasErrors; + }; +}; diff --git a/internal/web/static/js/validators.ts b/internal/web/static/js/validators.ts new file mode 100644 index 0000000..32e7bbd --- /dev/null +++ b/internal/web/static/js/validators.ts @@ -0,0 +1,74 @@ +const specialCharacters = (() => { + // Generate an array of special characters according to the ASCII table: + // https://en.wikipedia.org/wiki/ASCII + const specialCharacters = []; + + for (let i = "!".charCodeAt(0); i <= "/".charCodeAt(0); i++) { + specialCharacters.push(String.fromCharCode(i)); + } + + for (let i = ":".charCodeAt(0); i <= "@".charCodeAt(0); i++) { + specialCharacters.push(String.fromCharCode(i)); + } + + for (let i = "[".charCodeAt(0); i <= "`".charCodeAt(0); i++) { + specialCharacters.push(String.fromCharCode(i)); + } + + for (let i = "{".charCodeAt(0); i <= "~".charCodeAt(0); i++) { + specialCharacters.push(String.fromCharCode(i)); + } + + return specialCharacters; +})(); +const specialCharsString = specialCharacters.join(", "); + +const pluralize = (singular: string, amount: number) => (amount === 1 ? singular : singular + "s"); + +const form = document.querySelector("#form"); +if (!form) throw new Error("Could not find form element"); + +const submitButton = form.querySelector("button[type='submit']"); +if (!submitButton) throw new Error("Could not find submit button element"); + +export const mustNotBeEmpty = (v: string) => (v.length === 0 ? "The input must not be empty" : ""); +export const mustBeLongerThan = (minLength: number) => (v: string) => + v.length < minLength ? `The input must be at least ${minLength} ${pluralize("character", minLength)} long` : ""; +export const mustIncludeNumbers = (amount: number) => (v: string) => + v.split("").filter((c) => !isNaN(+c)).length < amount + ? `The input must include at least ${amount} ${pluralize("number", amount)}` + : ""; +export const mustIncludeSymbols = (amount: number) => (v: string) => + v.split("").filter((c) => specialCharacters.includes(c)).length < amount + ? `The input must include at least ${amount} ${pluralize("symbol", amount)}: ${specialCharsString}}` + : ""; +export const mustIncludeUppercase = (amount: number) => (v: string) => + v.split("").filter((c) => c === c.toUpperCase() && c !== c.toLowerCase()).length < amount + ? `The input must include at least ${amount} uppercase ${pluralize("character", amount)}` + : ""; +export const mustIncludeLowercase = (amount: number) => (v: string) => + v.split("").filter((c) => c === c.toLowerCase() && c !== c.toUpperCase()).length < amount + ? `The input must include at least ${amount} lowercase ${pluralize("character", amount)}` + : ""; + +export const mustMatchNewPassword = (v: string) => { + const passwordInput = form.querySelector(`#new input`); + if (!passwordInput) throw new Error("Could not find password input element"); + + return passwordInput.value !== v ? "The input must match the new password" : ""; +}; +export const mustNotMatchCurrentPassword = (v: string) => { + const passwordInput = form.querySelector(`#current input`); + if (!passwordInput) throw new Error("Could not find password input element"); + + return passwordInput.value === v ? "The input must not match the current password" : ""; +}; +export const mustNotIncludeUsername = (v: string) => { + const passwordInput = form.querySelector(`#username input`); + if (!passwordInput) throw new Error("Could not find username input element"); + + return v.includes(passwordInput.value) ? "The input must not include the username" : ""; +}; + +export const toggleValidator = (validate: (v: string) => string, enabled: boolean) => (v: string) => + enabled ? validate(v) : ""; diff --git a/internal/web/static/logo.webp b/internal/web/static/logo.webp new file mode 100644 index 0000000..9f91bcc Binary files /dev/null and b/internal/web/static/logo.webp differ diff --git a/internal/web/static/mstile-150x150.png b/internal/web/static/mstile-150x150.png new file mode 100644 index 0000000..94b174b Binary files /dev/null and b/internal/web/static/mstile-150x150.png differ diff --git a/internal/web/static/safari-pinned-tab.svg b/internal/web/static/safari-pinned-tab.svg new file mode 100644 index 0000000..1b8797c --- /dev/null +++ b/internal/web/static/safari-pinned-tab.svg @@ -0,0 +1,71 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/internal/web/static/site.webmanifest b/internal/web/static/site.webmanifest new file mode 100644 index 0000000..4bab644 --- /dev/null +++ b/internal/web/static/site.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "LDAP Password Changer", + "short_name": "Password Changer", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#b8e9f4", + "background_color": "#b8e9f4", + "display": "standalone", + "orientation": "portrait" +} diff --git a/internal/web/static/static.go b/internal/web/static/static.go new file mode 100644 index 0000000..04f73a7 --- /dev/null +++ b/internal/web/static/static.go @@ -0,0 +1,6 @@ +package static + +import "embed" + +//go:embed *.css js/*.js *.png *.ico *.svg *.webp site.webmanifest browserconfig.xml +var Static embed.FS diff --git a/internal/web/tailwind.css b/internal/web/tailwind.css new file mode 100644 index 0000000..e241530 --- /dev/null +++ b/internal/web/tailwind.css @@ -0,0 +1,5 @@ +/* This file is an entrypoint for the TailwindCSS compiler */ + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/internal/web/templates.go b/internal/web/templates.go new file mode 100644 index 0000000..870d801 --- /dev/null +++ b/internal/web/templates.go @@ -0,0 +1,38 @@ +package web + +import ( + "embed" +) + +//go:embed views layouts +var templates embed.FS + +type InputOpts struct { + Name string + Placeholder string + Type string + Autocomplete string +} + +func tplInputOpts(name, placeholder, type_, autocomplete string) InputOpts { + if type_ != "password" && type_ != "text" { + panic("InputOpts type must be either `password` or `text`") + } + + return InputOpts{ + name, + placeholder, + type_, + autocomplete, + } +} + +const NavbarItemBaseClass = "px-2 py-1 " + +func tplNavbarActive(activeTab, tab string) string { + if activeTab == tab { + return NavbarItemBaseClass + "text-white font-bold bg-gray-700 rounded-md" + } + + return NavbarItemBaseClass +} diff --git a/internal/web/users.go b/internal/web/users.go new file mode 100644 index 0000000..5e6c999 --- /dev/null +++ b/internal/web/users.go @@ -0,0 +1,59 @@ +package web + +import ( + "net/url" + + "github.com/gofiber/fiber/v2" +) + +func (a *App) usersHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + if sess.Fresh() { + return c.Redirect("/login") + } + + users := a.ldapCache.FindUsers() + + return c.Render("views/users", fiber.Map{ + "session": sess, + "title": "All users", + "activePage": "/users", + "headscripts": "", + "users": users, + }, "layouts/logged-in") +} + +func (a *App) userHandler(c *fiber.Ctx) error { + sess, err := a.sessionStore.Get(c) + if err != nil { + return handle500(c, err) + } + + if sess.Fresh() { + return c.Redirect("/login") + } + + userDN, err := url.PathUnescape(c.Params("userDN")) + if err != nil { + return handle500(c, err) + } + + thinUser, err := a.ldapCache.FindUserByDN(userDN) + if err != nil { + return handle500(c, err) + } + + user := a.ldapCache.PopulateGroupsForUser(&thinUser) + + return c.Render("views/user", fiber.Map{ + "session": sess, + "title": "All users", + "activePage": "/users", + "headscripts": "", + "user": user, + }, "layouts/logged-in") +} diff --git a/internal/web/views/404.html b/internal/web/views/404.html new file mode 100644 index 0000000..fba0b2a --- /dev/null +++ b/internal/web/views/404.html @@ -0,0 +1,10 @@ +
+

It appears that this page does not exist.

+ + + Back + +
diff --git a/internal/web/views/500.html b/internal/web/views/500.html new file mode 100644 index 0000000..a62d893 --- /dev/null +++ b/internal/web/views/500.html @@ -0,0 +1,5 @@ +
+

An error occurred:

+ +

{{ .error }}

+
diff --git a/internal/web/views/group.html b/internal/web/views/group.html new file mode 100644 index 0000000..87dc469 --- /dev/null +++ b/internal/web/views/group.html @@ -0,0 +1,25 @@ +

{{ .group.CN }}

+

{{ .group.DN }}

+ +

Members:

+
+ {{ range .group.Members }} + + {{ .CN }} ({{ .SAMAccountName }}) + + + + + + {{ end }} +
+ +{{ if not .group.Members }} +

No members

+{{ end }} diff --git a/internal/web/views/groups.html b/internal/web/views/groups.html new file mode 100644 index 0000000..4fb9b08 --- /dev/null +++ b/internal/web/views/groups.html @@ -0,0 +1,19 @@ +

All groups

+ +
+ {{ range .groups }} + + {{ .CN }} + + + + + + {{ end }} +
diff --git a/internal/web/views/index.html b/internal/web/views/index.html new file mode 100644 index 0000000..bbc0ad2 --- /dev/null +++ b/internal/web/views/index.html @@ -0,0 +1,16 @@ +{{ define "code" }} + {{ . }} +{{ end }} + + +

Hi {{ .user.CN }}!

+ +

Your user information

+
+

CN: {{ template "code" .user.CN }}

+

DN: {{ template "code" .user.DN }}

+

sAMAccountName: {{ template "code" .user.SAMAccountName }}

+
diff --git a/internal/web/views/login.html b/internal/web/views/login.html new file mode 100644 index 0000000..cc2c48e --- /dev/null +++ b/internal/web/views/login.html @@ -0,0 +1,36 @@ +
+
+ +
+ +
+ +
+ + + +

+ Powered by + + netresearch/ldap-manager + +

+
diff --git a/internal/web/views/user.html b/internal/web/views/user.html new file mode 100644 index 0000000..87c0ecc --- /dev/null +++ b/internal/web/views/user.html @@ -0,0 +1,25 @@ +

{{ .user.CN }} ({{ .user.SAMAccountName }})

+

{{ .user.DN }}

+ +

Groups:

+
+ {{ range .user.Groups }} + + {{ .CN }} + + + + + + {{ end }} +
+ +{{ if not .user.Groups }} +

No groups

+{{ end }} diff --git a/internal/web/views/users.html b/internal/web/views/users.html new file mode 100644 index 0000000..c2b6715 --- /dev/null +++ b/internal/web/views/users.html @@ -0,0 +1,19 @@ +

All users

+ +
+ {{ range .users }} + + {{ .CN }} + + + + + + {{ end }} +
diff --git a/main.go b/main.go new file mode 100644 index 0000000..603c9f6 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "os" + + "github.com/netresearch/ldap-selfservice-password-changer/internal/options" + "github.com/netresearch/ldap-selfservice-password-changer/internal/web" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func main() { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + + opts := options.Parse() + + app, err := web.NewApp(opts) + if err != nil { + log.Fatal().Err(err).Msg("could not initialize web app") + } + + if err := app.Listen(":3000"); err != nil { + log.Fatal().Err(err).Msg("could not start web server") + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a85c8da --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "@netresearch/ldap-selfservice-password-changer", + "version": "1.0.0", + "author": "DevMiner ", + "license": "MIT", + "packageManager": "pnpm@8.6.10", + "scripts": { + "start": "pnpm build:assets && pnpm go:start", + "dev": "pnpm build:assets && concurrently -n js,css,go \"pnpm js:dev\" \"pnpm css:dev\" \"pnpm go:dev\"", + "build": "pnpm build:assets && pnpm go:build", + "build:assets": "concurrently -n js,css \"pnpm js:build\" \"pnpm css:build\"", + "js:build": "tsc && pnpm js:minify", + "js:dev": "tsc -w --preserveWatchOutput", + "js:minify": "node scripts/minify.js", + "css:build": "postcss ./internal/web/tailwind.css -o ./internal/web/static/styles.css", + "css:dev": "pnpm css:build -w --verbose", + "go:start": "go run .", + "go:build": "go build", + "go:dev": "nodemon --signal SIGTERM -e go,html,css,js -w \"./**\" -x pnpm go:start" + }, + "devDependencies": { + "autoprefixer": "^10.4.14", + "concurrently": "^8.2.0", + "cssnano": "^6.0.1", + "nodemon": "^2.0.22", + "postcss": "^8.4.25", + "postcss-cli": "^10.1.0", + "prettier": "^2.8.8", + "prettier-plugin-go-template": "^0.0.13", + "prettier-plugin-tailwindcss": "^0.3.0", + "tailwindcss": "^3.3.2", + "typescript": "^5.1.6", + "uglify-js": "^3.17.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7614a98 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1648 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + autoprefixer: + specifier: ^10.4.14 + version: 10.4.14(postcss@8.4.25) + concurrently: + specifier: ^8.2.0 + version: 8.2.0 + cssnano: + specifier: ^6.0.1 + version: 6.0.1(postcss@8.4.25) + nodemon: + specifier: ^2.0.22 + version: 2.0.22 + postcss: + specifier: ^8.4.25 + version: 8.4.25 + postcss-cli: + specifier: ^10.1.0 + version: 10.1.0(postcss@8.4.25) + prettier: + specifier: ^2.8.8 + version: 2.8.8 + prettier-plugin-go-template: + specifier: ^0.0.13 + version: 0.0.13(prettier@2.8.8) + prettier-plugin-tailwindcss: + specifier: ^0.3.0 + version: 0.3.0(prettier@2.8.8) + tailwindcss: + specifier: ^3.3.2 + version: 3.3.2 + typescript: + specifier: ^5.1.6 + version: 5.1.6 + uglify-js: + specifier: ^3.17.4 + version: 3.17.4 + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@babel/runtime@7.22.6: + resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.18 + dev: true + + /@jridgewell/resolve-uri@3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.18: + resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /autoprefixer@10.4.14(postcss@8.4.25): + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.9 + caniuse-lite: 1.0.30001512 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist@4.21.9: + resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001512 + electron-to-chromium: 1.4.451 + node-releases: 2.0.12 + update-browserslist-db: 1.0.11(browserslist@4.21.9) + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.21.9 + caniuse-lite: 1.0.30001512 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + dev: true + + /caniuse-lite@1.0.30001512: + resolution: {integrity: sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /concurrently@8.2.0: + resolution: {integrity: sha512-nnLMxO2LU492mTUj9qX/az/lESonSZu81UznYDoXtz1IQf996ixVqPAgHXwvHiHCAef/7S8HIK+fTFK7Ifk8YA==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: true + + /css-declaration-sorter@6.4.0(postcss@8.4.25): + resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + dependencies: + postcss: 8.4.25 + dev: true + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.0.2 + dev: true + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /cssnano-preset-default@6.0.1(postcss@8.4.25): + resolution: {integrity: sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + css-declaration-sorter: 6.4.0(postcss@8.4.25) + cssnano-utils: 4.0.0(postcss@8.4.25) + postcss: 8.4.25 + postcss-calc: 9.0.1(postcss@8.4.25) + postcss-colormin: 6.0.0(postcss@8.4.25) + postcss-convert-values: 6.0.0(postcss@8.4.25) + postcss-discard-comments: 6.0.0(postcss@8.4.25) + postcss-discard-duplicates: 6.0.0(postcss@8.4.25) + postcss-discard-empty: 6.0.0(postcss@8.4.25) + postcss-discard-overridden: 6.0.0(postcss@8.4.25) + postcss-merge-longhand: 6.0.0(postcss@8.4.25) + postcss-merge-rules: 6.0.1(postcss@8.4.25) + postcss-minify-font-values: 6.0.0(postcss@8.4.25) + postcss-minify-gradients: 6.0.0(postcss@8.4.25) + postcss-minify-params: 6.0.0(postcss@8.4.25) + postcss-minify-selectors: 6.0.0(postcss@8.4.25) + postcss-normalize-charset: 6.0.0(postcss@8.4.25) + postcss-normalize-display-values: 6.0.0(postcss@8.4.25) + postcss-normalize-positions: 6.0.0(postcss@8.4.25) + postcss-normalize-repeat-style: 6.0.0(postcss@8.4.25) + postcss-normalize-string: 6.0.0(postcss@8.4.25) + postcss-normalize-timing-functions: 6.0.0(postcss@8.4.25) + postcss-normalize-unicode: 6.0.0(postcss@8.4.25) + postcss-normalize-url: 6.0.0(postcss@8.4.25) + postcss-normalize-whitespace: 6.0.0(postcss@8.4.25) + postcss-ordered-values: 6.0.0(postcss@8.4.25) + postcss-reduce-initial: 6.0.0(postcss@8.4.25) + postcss-reduce-transforms: 6.0.0(postcss@8.4.25) + postcss-svgo: 6.0.0(postcss@8.4.25) + postcss-unique-selectors: 6.0.0(postcss@8.4.25) + dev: true + + /cssnano-utils@4.0.0(postcss@8.4.25): + resolution: {integrity: sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + dev: true + + /cssnano@6.0.1(postcss@8.4.25): + resolution: {integrity: sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-preset-default: 6.0.1(postcss@8.4.25) + lilconfig: 2.1.0 + postcss: 8.4.25 + dev: true + + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: true + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.6 + dev: true + + /debug@3.2.7(supports-color@5.5.0): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + + /dependency-graph@0.11.0: + resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} + engines: {node: '>= 0.6.0'} + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /electron-to-chromium@1.4.451: + resolution: {integrity: sha512-YYbXHIBxAHe3KWvGOJOuWa6f3tgow44rBW+QAuwVp2DvGqNZeE//K2MowNdWS7XE8li5cgQDrX1LdBr41LufkA==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-glob@3.3.0: + resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /fraction.js@4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: true + + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.0 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module@2.12.1: + resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /jiti@1.18.2: + resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} + hasBin: true + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: true + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /node-releases@2.0.12: + resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} + dev: true + + /nodemon@2.0.22: + resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} + engines: {node: '>=8.10.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.1 + simple-update-notifier: 1.1.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /postcss-calc@9.0.1(postcss@8.4.25): + resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.2 + dependencies: + postcss: 8.4.25 + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-cli@10.1.0(postcss@8.4.25): + resolution: {integrity: sha512-Zu7PLORkE9YwNdvOeOVKPmWghprOtjFQU3srMUGbdz3pHJiFh7yZ4geiZFMkjMfB0mtTFR3h8RemR62rPkbOPA==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + postcss: ^8.0.0 + dependencies: + chokidar: 3.5.3 + dependency-graph: 0.11.0 + fs-extra: 11.1.1 + get-stdin: 9.0.0 + globby: 13.2.2 + picocolors: 1.0.0 + postcss: 8.4.25 + postcss-load-config: 4.0.1(postcss@8.4.25) + postcss-reporter: 7.0.5(postcss@8.4.25) + pretty-hrtime: 1.0.3 + read-cache: 1.0.0 + slash: 5.1.0 + yargs: 17.7.2 + transitivePeerDependencies: + - ts-node + dev: true + + /postcss-colormin@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-convert-values@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-discard-comments@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + dev: true + + /postcss-discard-duplicates@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + dev: true + + /postcss-discard-empty@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + dev: true + + /postcss-discard-overridden@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + dev: true + + /postcss-import@15.1.0(postcss@8.4.25): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.2 + dev: true + + /postcss-js@4.0.1(postcss@8.4.25): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.25 + dev: true + + /postcss-load-config@4.0.1(postcss@8.4.25): + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.25 + yaml: 2.3.1 + dev: true + + /postcss-merge-longhand@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + stylehacks: 6.0.0(postcss@8.4.25) + dev: true + + /postcss-merge-rules@6.0.1(postcss@8.4.25): + resolution: {integrity: sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + caniuse-api: 3.0.0 + cssnano-utils: 4.0.0(postcss@8.4.25) + postcss: 8.4.25 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-minify-font-values@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-gradients@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + colord: 2.9.3 + cssnano-utils: 4.0.0(postcss@8.4.25) + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-params@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + cssnano-utils: 4.0.0(postcss@8.4.25) + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-selectors@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.25): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.25 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-normalize-charset@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + dev: true + + /postcss-normalize-display-values@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-positions@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-repeat-style@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-string@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-timing-functions@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-unicode@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-url@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-whitespace@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-ordered-values@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 4.0.0(postcss@8.4.25) + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-reduce-initial@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + caniuse-api: 3.0.0 + postcss: 8.4.25 + dev: true + + /postcss-reduce-transforms@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-reporter@7.0.5(postcss@8.4.25): + resolution: {integrity: sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.1.0 + dependencies: + picocolors: 1.0.0 + postcss: 8.4.25 + thenby: 1.3.4 + dev: true + + /postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-svgo@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==} + engines: {node: ^14 || ^16 || >= 18} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-value-parser: 4.2.0 + svgo: 3.0.2 + dev: true + + /postcss-unique-selectors@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.25 + postcss-selector-parser: 6.0.13 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.25: + resolution: {integrity: sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prettier-plugin-go-template@0.0.13(prettier@2.8.8): + resolution: {integrity: sha512-gG/xT5kd+kCzoMaTchXvdfBdsunyRCV6G8cgdPGPd2V5JGGKXUG7SjzBKU7jaGh2RTeblcAdBb/E+S/duOAMsA==} + engines: {node: '>=14.0.0'} + peerDependencies: + prettier: ^2.0.0 + dependencies: + prettier: 2.8.8 + ulid: 2.3.0 + dev: true + + /prettier-plugin-tailwindcss@0.3.0(prettier@2.8.8): + resolution: {integrity: sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==} + engines: {node: '>=12.17.0'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@shufo/prettier-plugin-blade': '*' + '@trivago/prettier-plugin-sort-imports': '*' + prettier: '>=2.2.0' + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + prettier-plugin-twig-melody: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@shufo/prettier-plugin-blade': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + prettier-plugin-twig-melody: + optional: true + dependencies: + prettier: 2.8.8 + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-hrtime@1.0.3: + resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} + engines: {node: '>= 0.8'} + dev: true + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve@1.22.2: + resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + hasBin: true + dependencies: + is-core-module: 2.12.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.0 + dev: true + + /semver@5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + + /semver@7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + dev: true + + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: true + + /simple-update-notifier@1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + dependencies: + semver: 7.0.0 + dev: true + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /stylehacks@6.0.0(postcss@8.4.25): + resolution: {integrity: sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.9 + postcss: 8.4.25 + postcss-selector-parser: 6.0.13 + dev: true + + /sucrase@3.32.0: + resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svgo@3.0.2: + resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + csso: 5.0.5 + picocolors: 1.0.0 + dev: true + + /tailwindcss@3.3.2: + resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.2.12 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.18.2 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.25 + postcss-import: 15.1.0(postcss@8.4.25) + postcss-js: 4.0.1(postcss@8.4.25) + postcss-load-config: 4.0.1(postcss@8.4.25) + postcss-nested: 6.0.1(postcss@8.4.25) + postcss-selector-parser: 6.0.13 + postcss-value-parser: 4.2.0 + resolve: 1.22.2 + sucrase: 3.32.0 + transitivePeerDependencies: + - ts-node + dev: true + + /thenby@1.3.4: + resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /tslib@2.6.0: + resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} + dev: true + + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + dev: true + + /ulid@2.3.0: + resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + hasBin: true + dev: true + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /update-browserslist-db@1.0.11(browserslist@4.21.9): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.9 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yaml@2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..643684a --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + autoprefixer: {}, + cssnano: {}, + tailwindcss: {} + } +}; diff --git a/scripts/minify.js b/scripts/minify.js new file mode 100644 index 0000000..e90884d --- /dev/null +++ b/scripts/minify.js @@ -0,0 +1,28 @@ +// @ts-check +const Uglify = require("uglify-js"); +const FS = require("node:fs"); + +const base = "./internal/web/static/js/"; + +const nameCache = {}; + +const files = FS.readdirSync(base) + .filter((f) => f.endsWith(".js")) + .map((f) => base + f); + +for (const f of files) { + const raw = FS.readFileSync(f, "utf8"); + + const res = Uglify.minify(raw, { + mangle: true, + compress: {}, + module: true, + toplevel: true, + sourceMap: false, + nameCache + }); + + if (res.error) throw new Error(`err: an error occurred during minification of ${f}: ${res.error}`); + + FS.writeFileSync(f, res.code); +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..e0f1141 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,17 @@ +import plugin from "tailwindcss/plugin"; + +/** @type {import('tailwindcss').Config} */ +const config = { + content: ["internal/web/layouts/*.html", "internal/web/views/*.html"], + plugins: [ + plugin(({ addVariant }) => { + addVariant("hocus", ["&:hover", "&:focus"]); + }) + ], + theme: { + extend: {} + }, + safelist: ["bg-gray-700"] +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ceb8995 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "ESNext"], + "module": "ESNext", + "baseUrl": "./internal/web/static", + "rootDir": "./internal/web/static", + "allowJs": true, + "checkJs": true, + "allowSyntheticDefaultImports": true, + "declaration": false, + "sourceMap": false, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "skipLibCheck": true + }, + "include": ["internal/web/static"] +}