├── .dockerignore ├── .env.template ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── contributing.md └── workflows │ ├── portr-admin.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── .pre-commit-config.yaml ├── .well-known └── funding-manifest-urls ├── LICENSE ├── Makefile ├── README.md ├── admin ├── .dockerignore ├── .env.template ├── .gitignore ├── .python-version ├── Dockerfile ├── Makefile ├── README.md ├── apis │ ├── __init__.py │ ├── pagination.py │ ├── security.py │ └── v1 │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── config.py │ │ ├── connection.py │ │ ├── instance_settings.py │ │ ├── team.py │ │ └── user.py ├── config │ ├── __init__.py │ ├── beats.py │ ├── database.py │ ├── enums.py │ └── settings.py ├── conftest.py ├── main.py ├── migrations │ └── models │ │ ├── 0_20240326150919_init.py.py │ │ └── 1_20240813141115_add-password-to-user.py ├── models │ ├── __init__.py │ ├── auth.py │ ├── connection.py │ ├── settings.py │ └── user.py ├── pyproject.toml ├── requirements-dev.lock ├── requirements.lock ├── schemas │ ├── __init__.py │ ├── auth.py │ ├── connection.py │ ├── settings.py │ ├── team.py │ └── user.py ├── scripts │ ├── generate-encryption-key.sh │ ├── pre-deploy.py │ └── start.sh ├── services │ ├── __init__.py │ ├── auth.py │ ├── connection.py │ ├── settings.py │ ├── team.py │ └── user.py ├── static │ ├── Geist-Regular.woff2 │ ├── Manrope-VariableFont_wght.ttf │ ├── favicon.ico │ ├── favicon.svg │ ├── geist-mono-latin-400-normal.woff2 │ └── icon.svg ├── templates │ └── index.html ├── tests │ ├── __init__.py │ ├── api_tests │ │ ├── __init__.py │ │ ├── test_auth.py │ │ ├── test_config.py │ │ ├── test_connection.py │ │ ├── test_instance_settings.py │ │ ├── test_team.py │ │ └── test_user.py │ ├── factories.py │ ├── service_tests │ │ ├── __init__.py │ │ ├── test_connection_service.py │ │ └── test_user_service.py │ ├── test_pages.py │ └── test_utils.py ├── utils │ ├── __init__.py │ ├── exception.py │ ├── github_auth.py │ ├── smtp.py │ ├── template_renderer.py │ ├── token.py │ └── vite.py └── web │ ├── .gitignore │ ├── components.json │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.cjs │ ├── src │ ├── App.svelte │ ├── app.pcss │ ├── lib │ │ ├── components │ │ │ ├── ApiError.svelte │ │ │ ├── ConnectionStatus.svelte │ │ │ ├── ConnectionType.svelte │ │ │ ├── DateField.svelte │ │ │ ├── ErrorText.svelte │ │ │ ├── Pagination.svelte │ │ │ ├── copyToClipboard.svelte │ │ │ ├── data-table-skeleton.svelte │ │ │ ├── data-table.svelte │ │ │ ├── error.svelte │ │ │ ├── goback.svelte │ │ │ ├── issue-link.svelte │ │ │ ├── new-team-dialog.svelte │ │ │ ├── sidebarlink.svelte │ │ │ ├── team-selector.svelte │ │ │ ├── ui │ │ │ │ ├── alert-dialog │ │ │ │ │ ├── alert-dialog-action.svelte │ │ │ │ │ ├── alert-dialog-cancel.svelte │ │ │ │ │ ├── alert-dialog-content.svelte │ │ │ │ │ ├── alert-dialog-description.svelte │ │ │ │ │ ├── alert-dialog-footer.svelte │ │ │ │ │ ├── alert-dialog-header.svelte │ │ │ │ │ ├── alert-dialog-overlay.svelte │ │ │ │ │ ├── alert-dialog-portal.svelte │ │ │ │ │ ├── alert-dialog-title.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── alert │ │ │ │ │ ├── alert-description.svelte │ │ │ │ │ ├── alert-title.svelte │ │ │ │ │ ├── alert.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── avatar │ │ │ │ │ ├── avatar-fallback.svelte │ │ │ │ │ ├── avatar-image.svelte │ │ │ │ │ ├── avatar.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── badge │ │ │ │ │ ├── badge.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── button │ │ │ │ │ ├── button.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── card │ │ │ │ │ ├── card-content.svelte │ │ │ │ │ ├── card-description.svelte │ │ │ │ │ ├── card-footer.svelte │ │ │ │ │ ├── card-header.svelte │ │ │ │ │ ├── card-title.svelte │ │ │ │ │ ├── card.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── checkbox │ │ │ │ │ ├── checkbox.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── dialog │ │ │ │ │ ├── dialog-content.svelte │ │ │ │ │ ├── dialog-description.svelte │ │ │ │ │ ├── dialog-footer.svelte │ │ │ │ │ ├── dialog-header.svelte │ │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ │ ├── dialog-portal.svelte │ │ │ │ │ ├── dialog-title.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── dropdown-menu │ │ │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ │ ├── dropdown-menu-radio-group.svelte │ │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── input │ │ │ │ │ ├── index.ts │ │ │ │ │ └── input.svelte │ │ │ │ ├── label │ │ │ │ │ ├── index.ts │ │ │ │ │ └── label.svelte │ │ │ │ ├── pagination │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── pagination-content.svelte │ │ │ │ │ ├── pagination-ellipsis.svelte │ │ │ │ │ ├── pagination-item.svelte │ │ │ │ │ ├── pagination-link.svelte │ │ │ │ │ ├── pagination-next-button.svelte │ │ │ │ │ ├── pagination-prev-button.svelte │ │ │ │ │ └── pagination.svelte │ │ │ │ ├── select │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── select-content.svelte │ │ │ │ │ ├── select-item.svelte │ │ │ │ │ ├── select-label.svelte │ │ │ │ │ ├── select-separator.svelte │ │ │ │ │ └── select-trigger.svelte │ │ │ │ ├── separator │ │ │ │ │ ├── index.ts │ │ │ │ │ └── separator.svelte │ │ │ │ ├── skeleton │ │ │ │ │ ├── index.ts │ │ │ │ │ └── skeleton.svelte │ │ │ │ ├── switch │ │ │ │ │ ├── index.ts │ │ │ │ │ └── switch.svelte │ │ │ │ ├── table │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── table-body.svelte │ │ │ │ │ ├── table-caption.svelte │ │ │ │ │ ├── table-cell.svelte │ │ │ │ │ ├── table-footer.svelte │ │ │ │ │ ├── table-head.svelte │ │ │ │ │ ├── table-header.svelte │ │ │ │ │ ├── table-row.svelte │ │ │ │ │ └── table.svelte │ │ │ │ ├── textarea │ │ │ │ │ ├── index.ts │ │ │ │ │ └── textarea.svelte │ │ │ │ └── tooltip │ │ │ │ │ ├── index.ts │ │ │ │ │ └── tooltip-content.svelte │ │ │ └── users │ │ │ │ ├── avatar.svelte │ │ │ │ ├── delete.svelte │ │ │ │ ├── invite-user.svelte │ │ │ │ ├── members.svelte │ │ │ │ └── user-email.svelte │ │ ├── humanize.ts │ │ ├── services │ │ │ └── user.ts │ │ ├── store.ts │ │ ├── types.d.ts │ │ └── utils.ts │ ├── main.ts │ ├── pages │ │ ├── app-layout.svelte │ │ ├── app │ │ │ ├── app.svelte │ │ │ ├── connections.svelte │ │ │ ├── email-settings.svelte │ │ │ ├── myaccount.svelte │ │ │ ├── overview.svelte │ │ │ └── users.svelte │ │ ├── home.svelte │ │ ├── instance-settings │ │ │ ├── emailSettings.svelte │ │ │ ├── index.svelte │ │ │ ├── newteam.svelte │ │ │ └── page.svelte │ │ └── notfound.svelte │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── docker-compose.dev.yaml ├── docker-compose.yaml ├── docs ├── .gitignore ├── .nvmrc ├── .vscode │ ├── extensions.json │ └── launch.json ├── astro.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public │ ├── favicon.ico │ ├── funding.json │ ├── icon.svg │ └── og.png ├── src │ ├── assets │ │ ├── cloudflare.png │ │ ├── icon.png │ │ └── icon.svg │ ├── components │ │ └── Head.astro │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── client │ │ │ ├── http-tunnel.md │ │ │ ├── installation.mdx │ │ │ ├── tcp-tunnel.md │ │ │ ├── templates.md │ │ │ └── websocket-tunnel.md │ │ │ ├── getting-started.mdx │ │ │ ├── index.mdx │ │ │ ├── local-development │ │ │ ├── admin.md │ │ │ ├── portr-client.md │ │ │ └── tunnel-server.md │ │ │ ├── resources │ │ │ └── route53.mdx │ │ │ └── server │ │ │ ├── cloudflare-api-token.mdx │ │ │ ├── github-oauth-app.mdx │ │ │ ├── index.md │ │ │ └── start-the-tunnel-server.mdx │ ├── env.d.ts │ ├── fonts │ │ ├── Geist-Regular.woff2 │ │ └── font-face.css │ ├── styles │ │ └── custom.css │ └── tailwind.css ├── tailwind.config.mjs └── tsconfig.json ├── e2e └── setup_test_data.py ├── install.sh └── tunnel ├── .air.toml ├── .dockerignore ├── .env.template ├── .gitignore ├── Dockerfile ├── Makefile ├── client.dev.yaml ├── cmd ├── portr │ ├── auth.go │ ├── config.go │ ├── http.go │ ├── main.go │ ├── start.go │ ├── tcp.go │ └── updates.go └── portrd │ └── main.go ├── go.mod ├── go.sum └── internal ├── client ├── client │ └── client.go ├── config │ └── config.go ├── dashboard │ ├── dashboard.go │ ├── handler │ │ ├── handler.go │ │ └── tunnels.go │ ├── service │ │ ├── service.go │ │ └── tunnels.go │ ├── templates │ │ ├── index.html │ │ └── templates.go │ └── ui │ │ ├── .gitignore │ │ ├── .vscode │ │ └── extensions.json │ │ ├── components.json │ │ ├── dist │ │ └── dist.go │ │ ├── index.html │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── postcss.config.cjs │ │ ├── public │ │ ├── Geist-Regular.woff2 │ │ └── favicon.svg │ │ ├── src │ │ ├── App.svelte │ │ ├── app.pcss │ │ ├── assets │ │ │ └── svelte.svg │ │ ├── lib │ │ │ ├── components │ │ │ │ ├── HttpBadge.svelte │ │ │ │ ├── InspectorIcon.svelte │ │ │ │ ├── RenderContent.svelte │ │ │ │ ├── RenderFormUrlEncoded.svelte │ │ │ │ ├── RenderMultipartFormData.svelte │ │ │ │ └── ui │ │ │ │ │ ├── button │ │ │ │ │ ├── button.svelte │ │ │ │ │ └── index.ts │ │ │ │ │ ├── card │ │ │ │ │ ├── card-content.svelte │ │ │ │ │ ├── card-description.svelte │ │ │ │ │ ├── card-footer.svelte │ │ │ │ │ ├── card-header.svelte │ │ │ │ │ ├── card-title.svelte │ │ │ │ │ ├── card.svelte │ │ │ │ │ └── index.ts │ │ │ │ │ ├── input │ │ │ │ │ ├── index.ts │ │ │ │ │ └── input.svelte │ │ │ │ │ ├── label │ │ │ │ │ ├── index.ts │ │ │ │ │ └── label.svelte │ │ │ │ │ ├── table │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── table-body.svelte │ │ │ │ │ ├── table-caption.svelte │ │ │ │ │ ├── table-cell.svelte │ │ │ │ │ ├── table-footer.svelte │ │ │ │ │ ├── table-head.svelte │ │ │ │ │ ├── table-header.svelte │ │ │ │ │ ├── table-row.svelte │ │ │ │ │ └── table.svelte │ │ │ │ │ └── tabs │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tabs-content.svelte │ │ │ │ │ ├── tabs-list.svelte │ │ │ │ │ ├── tabs-trigger.svelte │ │ │ │ │ └── tabs.svelte │ │ │ ├── store.ts │ │ │ ├── types.d.ts │ │ │ └── utils.ts │ │ ├── main.ts │ │ ├── pages │ │ │ ├── Home.svelte │ │ │ ├── Inspect.svelte │ │ │ └── RequestDetails.svelte │ │ └── vite-env.d.ts │ │ ├── svelte.config.js │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts ├── db │ └── db.go ├── ssh │ └── ssh.go ├── tui │ └── tui.go └── vite │ └── vite.go ├── constants └── constants.go ├── server ├── config │ └── config.go ├── cron │ ├── cron.go │ ├── ping.go │ └── tasks.go ├── db │ ├── db.go │ └── models.go ├── proxy │ └── proxy.go ├── service │ └── service.go └── ssh │ └── sshd.go └── utils ├── dir.go ├── dir_test.go ├── error-templates ├── connection-lost.html ├── local-server-not-online.html └── unregistered-subdomain.html ├── error.go ├── http.go ├── id.go ├── port.go ├── random.go ├── request.go ├── string.go ├── subdomain.go └── subdomain_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | PORTR_ADMIN_GITHUB_CLIENT_ID= 2 | PORTR_ADMIN_GITHUB_CLIENT_SECRET= 3 | 4 | PORTR_DOMAIN=example.com 5 | PORTR_DB_URL=postgres://postgres:postgres@localhost:5432/postgres 6 | 7 | PORTR_SERVER_URL=example.com 8 | PORTR_SSH_URL=example.com:2222 9 | 10 | CLOUDFLARE_API_TOKEN= 11 | 12 | POSTGRES_USER=postgres 13 | POSTGRES_PASSWORD=postgres 14 | POSTGRES_DB=postgres 15 | 16 | PORTR_ADMIN_ENCRYPTION_KEY= 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | **/*.svelte -linguist-detectable -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [amalshaji] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: amalshaji 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/portr-admin.yml: -------------------------------------------------------------------------------- 1 | name: Portr admin 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | paths: 7 | - "admin/**" 8 | pull_request: 9 | branches: ["main"] 10 | paths: 11 | - "admin/**" 12 | 13 | permissions: 14 | contents: read 15 | 16 | env: 17 | PORTR_DOMAIN: localhost:8000 18 | PORTR_ADMIN_GITHUB_CLIENT_ID: "" 19 | PORTR_ADMIN_GITHUB_CLIENT_SECRET: "" 20 | PORTR_SERVER_URL: localhost:8000 21 | PORTR_SSH_URL: localhost:2222 22 | PORTR_ADMIN_USE_VITE: true 23 | PORTR_ADMIN_ENCRYPTION_KEY: "mj-qoeMhLQp_cHnMU9nsLfCMnNkZ6XBcFefy4VxzOe8=" 24 | PORTR_DB_URL: "" 25 | 26 | jobs: 27 | test: 28 | runs-on: ubuntu-latest 29 | defaults: 30 | run: 31 | working-directory: ./admin 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Set up Python 3.12 35 | uses: actions/setup-python@v3 36 | with: 37 | python-version: "3.12" 38 | cache: "pip" 39 | cache-dependency-path: admin/requirements*.lock 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | sed '/-e/d' requirements.lock > requirements.txt && python -m pip install -r requirements.txt 44 | sed '/-e/d' requirements-dev.lock > requirements-dev.txt && python -m pip install -r requirements-dev.txt 45 | - name: Run tests 46 | run: | 47 | pytest 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | keys 2 | .DS_Store 3 | db.sqlite 4 | .env 5 | *.fiber.gz 6 | tmp 7 | portr 8 | !**/portr/ 9 | postgres-data 10 | node_modules 11 | .mypy_cache 12 | data/ 13 | dist 14 | !tunnel/internal/client/dashboard/dist -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: "portr" 3 | main: ./cmd/portr 4 | binary: "portr" 5 | dir: tunnel 6 | env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | goarch: 13 | - amd64 14 | - arm64 15 | ldflags: 16 | - -s -w -X main.version={{.Version}} 17 | 18 | archives: 19 | - format: zip 20 | name_template: >- 21 | {{ .Binary }}_ 22 | {{- .Version }}_ 23 | {{- title .Os }}_ 24 | {{- if eq .Arch "amd64" }}x86_64 25 | {{- else if eq .Arch "386" }}i386 26 | {{- else }}{{ .Arch }}{{ end }} 27 | {{- if .Arm }}v{{ .Arm }}{{ end }} 28 | id: portr 29 | 30 | checksum: 31 | name_template: "checksums.txt" 32 | 33 | snapshot: 34 | name_template: "{{ incpatch .Version }}-next" 35 | 36 | release: 37 | name_template: "v{{ .Version }}" 38 | draft: true 39 | 40 | brews: 41 | - name: portr 42 | ids: 43 | - portr 44 | homepage: https://github.com/amalshaji/portr 45 | repository: 46 | owner: amalshaji 47 | name: homebrew-taps 48 | token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" 49 | commit_author: 50 | name: Amal Shaji 51 | email: amalshajid@gmail.com 52 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/charliermarsh/ruff-pre-commit 3 | rev: "v0.2.2" 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --exit-non-zero-on-fix] 7 | exclude: "(^.*/migrations/|^client/)" 8 | - id: ruff-format 9 | exclude: "(^.*/migrations/|^client/)" 10 | 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v4.5.0 13 | hooks: 14 | - id: trailing-whitespace 15 | exclude: "(^.*/migrations/|^client/)" 16 | - id: check-merge-conflict 17 | - id: debug-statements 18 | - id: check-added-large-files 19 | 20 | - repo: https://github.com/pre-commit/mirrors-mypy 21 | rev: v1.8.0 22 | hooks: 23 | - id: mypy 24 | args: 25 | - --follow-imports=skip 26 | - --ignore-missing-imports 27 | - --show-column-numbers 28 | - --no-pretty 29 | - --check-untyped-defs 30 | exclude: '(^.*/migrations/|^client/|_tests\.py$)' 31 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://portr.dev/funding.json -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | load-env: 2 | export $(cat .env | grep -v '^#' | xargs) 3 | -------------------------------------------------------------------------------- /admin/.dockerignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | .pytest_cache 3 | .venv 4 | tmp 5 | .env 6 | **/__pycache__ 7 | **/node_modules 8 | -------------------------------------------------------------------------------- /admin/.env.template: -------------------------------------------------------------------------------- 1 | PORTR_DB_URL="postgres://postgres:postgres@localhost:5432/postgres" 2 | PORTR_DOMAIN=localhost:8000 3 | 4 | PORTR_ADMIN_DEBUG=True 5 | PORTR_ADMIN_USE_VITE=True 6 | PORTR_ADMIN_ENCRYPTION_KEY= 7 | PORTR_ADMIN_GITHUB_CLIENT_ID= 8 | PORTR_ADMIN_GITHUB_CLIENT_SECRET= 9 | 10 | PORTR_SERVER_URL=localhost:8000 11 | PORTR_SSH_URL=localhost:2222 12 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .mypy_cache 3 | .pytest_cache 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /admin/.python-version: -------------------------------------------------------------------------------- 1 | 3.12.1 2 | -------------------------------------------------------------------------------- /admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS frontend-builder 2 | 3 | WORKDIR /app 4 | 5 | COPY web/package.json web/pnpm-lock.yaml ./ 6 | 7 | RUN npm i -g pnpm@9.7.0 && pnpm install --frozen-lockfile 8 | 9 | COPY web . 10 | 11 | RUN pnpm build 12 | 13 | FROM python:3.12 AS builder 14 | 15 | ENV PATH="/app/.venv/bin:$PATH" 16 | 17 | WORKDIR /app 18 | 19 | COPY pyproject.toml requirements.lock README.md ./ 20 | 21 | RUN pip install --no-cache-dir uv==0.5.2 22 | 23 | RUN uv venv && \ 24 | uv pip install --no-cache -r requirements.lock 25 | 26 | FROM python:3.12-slim AS final 27 | 28 | LABEL maintainer="Amal Shaji" \ 29 | org.opencontainers.image.title="Portr Admin" \ 30 | org.opencontainers.image.description="Admin server for Portr" \ 31 | org.opencontainers.image.source="https://github.com/amalshaji/portr" 32 | 33 | ENV PATH="/app/.venv/bin:$PATH" \ 34 | PYTHONPATH="/app/:$PYTHONPATH" 35 | 36 | RUN apt-get update && apt-get install -y --no-install-recommends \ 37 | curl \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | WORKDIR /app 41 | 42 | COPY --from=builder /app/.venv/ /app/.venv/ 43 | COPY --from=frontend-builder /app/dist /app/web/dist 44 | COPY . . 45 | 46 | ENTRYPOINT ["sh", "scripts/start.sh"] 47 | -------------------------------------------------------------------------------- /admin/Makefile: -------------------------------------------------------------------------------- 1 | init-migrations: 2 | aerich init -t db.TORTOISE_ORM 3 | 4 | init-db: 5 | aerich init-db 6 | 7 | create-migrations: 8 | aerich migrate --name $(name) 9 | 10 | run-migrations: 11 | aerich upgrade 12 | 13 | runserver: 14 | make run-migrations 15 | uvicorn main:app --reload 16 | 17 | installclient: 18 | pnpm --dir web install 19 | 20 | runclient: 21 | pnpm --dir web dev -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # Portr admin 2 | 3 | Admin dashboard for portr. 4 | -------------------------------------------------------------------------------- /admin/apis/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from apis.v1 import api as api_v1 3 | 4 | api = APIRouter(prefix="/api") 5 | api.include_router(api_v1) 6 | -------------------------------------------------------------------------------- /admin/apis/pagination.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | from pydantic import BaseModel, ConfigDict 3 | from tortoise.queryset import QuerySet 4 | from tortoise.models import Model 5 | 6 | T = TypeVar("T") 7 | Qs_T = TypeVar("Qs_T", bound=Model) 8 | 9 | 10 | class PaginatedResponse(BaseModel, Generic[T]): 11 | count: int 12 | data: list[T] 13 | 14 | model_config = ConfigDict(arbitrary_types_allowed=True) 15 | 16 | @classmethod 17 | async def generate_response_for_page( 18 | self, 19 | qs: QuerySet[Qs_T], 20 | page: int, 21 | page_size: int = 10, 22 | ): 23 | if page < 1: 24 | page = 1 25 | 26 | self.count = await qs.count() 27 | self.data = await qs.limit(page_size).offset((page - 1) * page_size) 28 | return PaginatedResponse[T](count=self.count, data=self.data) # type: ignore 29 | -------------------------------------------------------------------------------- /admin/apis/security.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from fastapi import Cookie, Depends, Header 3 | 4 | from models.auth import Session 5 | from models.user import Role, TeamUser, User 6 | 7 | from utils.exception import PermissionDenied 8 | 9 | 10 | class NotAuthenticated(Exception): 11 | pass 12 | 13 | 14 | async def get_current_user( 15 | portr_session: Annotated[str | None, Cookie()] = None, 16 | ) -> User: 17 | if portr_session is None: 18 | raise NotAuthenticated 19 | 20 | session = await Session.filter(token=portr_session).select_related("user").first() 21 | if session is None: 22 | raise NotAuthenticated 23 | 24 | return session.user 25 | 26 | 27 | async def get_current_team_user( 28 | user: User = Depends(get_current_user), 29 | x_team_slug: str | None = Header(), 30 | ) -> TeamUser: 31 | if x_team_slug is None: 32 | raise NotAuthenticated 33 | 34 | team_user = ( 35 | await TeamUser.filter(user=user, team__slug=x_team_slug) 36 | .select_related("team", "user", "user__github_user") 37 | .first() 38 | ) 39 | if team_user is None: 40 | raise NotAuthenticated 41 | 42 | return team_user 43 | 44 | 45 | async def requires_superuser(user: User = Depends(get_current_user)) -> User: 46 | if not user.is_superuser: 47 | raise PermissionDenied("Only superuser can perform this action") 48 | 49 | return user 50 | 51 | 52 | async def requires_admin( 53 | team_user: TeamUser = Depends(get_current_team_user), 54 | ) -> TeamUser: 55 | if team_user.role != Role.admin: 56 | raise PermissionDenied("Only admin can perform this action") 57 | 58 | return team_user 59 | -------------------------------------------------------------------------------- /admin/apis/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from apis.v1.auth import api as api_v1_auth 3 | from apis.v1.team import api as api_v1_team 4 | from apis.v1.user import api as api_v1_user 5 | from apis.v1.connection import api as api_v1_connection 6 | from apis.v1.instance_settings import api as api_v1_instance_settings 7 | from apis.v1.config import api as api_v1_config 8 | 9 | api = APIRouter(prefix="/v1") 10 | api.include_router(api_v1_auth) 11 | api.include_router(api_v1_team) 12 | api.include_router(api_v1_user) 13 | api.include_router(api_v1_connection) 14 | api.include_router(api_v1_instance_settings) 15 | api.include_router(api_v1_config) 16 | 17 | 18 | @api.get("/healthcheck", tags=["healthcheck"]) 19 | async def healthcheck(): 20 | return {"status": "ok"} 21 | -------------------------------------------------------------------------------- /admin/apis/v1/instance_settings.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from apis import security 3 | from models.user import User 4 | from services import settings as settings_service 5 | from schemas.settings import SettingsResponseSchema, SettingsUpdateSchema 6 | 7 | api = APIRouter(prefix="/instance-settings", tags=["instance-settings"]) 8 | 9 | 10 | @api.get("/", response_model=SettingsResponseSchema) 11 | async def get_settings(_=Depends(security.requires_superuser)): 12 | return await settings_service.get_instance_settings() 13 | 14 | 15 | @api.patch("/", response_model=SettingsResponseSchema) 16 | async def update_settings( 17 | data: SettingsUpdateSchema, user: User = Depends(security.requires_superuser) 18 | ): 19 | settings = await settings_service.get_instance_settings() 20 | 21 | if data.smtp_enabled is False: 22 | settings.smtp_enabled = False 23 | await settings.save() 24 | return settings 25 | 26 | for k, v in data.model_dump().items(): 27 | setattr(settings, k, v) 28 | 29 | settings.updated_by = user # type: ignore 30 | 31 | await settings.save() 32 | 33 | return settings 34 | -------------------------------------------------------------------------------- /admin/apis/v1/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | from apis import security 3 | 4 | from models.user import Team, TeamUser, User 5 | from schemas.team import TeamSchema 6 | from schemas.user import ( 7 | ChangePasswordSchema, 8 | TeamUserSchemaForCurrentUser, 9 | UserSchema, 10 | UserUpdateSchema, 11 | ) 12 | from utils.token import generate_secret_key 13 | 14 | api = APIRouter(prefix="/user", tags=["user"]) 15 | 16 | 17 | @api.get("/me", response_model=TeamUserSchemaForCurrentUser) 18 | async def current_team_user( 19 | team_user: TeamUser = Depends(security.get_current_team_user), 20 | ): 21 | return team_user 22 | 23 | 24 | @api.get("/me/teams", response_model=list[TeamSchema]) 25 | async def current_user_teams( 26 | user: TeamUser = Depends(security.get_current_user), 27 | ): 28 | return await Team.filter(team_users__user=user).all() 29 | 30 | 31 | @api.patch("/me/update", response_model=UserSchema) 32 | async def update_user( 33 | data: UserUpdateSchema, user: User = Depends(security.get_current_user) 34 | ): 35 | for k, v in data.model_dump().items(): 36 | if v is not None: 37 | setattr(user, k, v) 38 | await user.save() 39 | return user 40 | 41 | 42 | @api.patch("/me/change-password", response_model=UserSchema) 43 | async def change_password( 44 | data: ChangePasswordSchema, user: User = Depends(security.get_current_user) 45 | ): 46 | user.set_password(data.password) 47 | await user.save() 48 | return user 49 | 50 | 51 | @api.patch("/me/rotate-secret-key") 52 | async def rotate_secret_key(user: TeamUser = Depends(security.get_current_team_user)): 53 | user.secret_key = generate_secret_key() 54 | await user.save() 55 | return {"secret_key": user.secret_key} 56 | -------------------------------------------------------------------------------- /admin/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .settings import settings 2 | 3 | __all__ = ["settings"] 4 | -------------------------------------------------------------------------------- /admin/config/beats.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, UTC 2 | from models.auth import Session 3 | from models.connection import Connection, ConnectionStatus 4 | import logging 5 | 6 | logger = logging.getLogger("uvicorn.info") 7 | 8 | 9 | async def clear_expired_sessions(): 10 | logger.info("Clearing expired sessions") 11 | await Session.filter(expires_at__lte=datetime.now(UTC)).delete() 12 | 13 | 14 | async def clear_unclaimed_connections(): 15 | logger.info("Clearing unclaimed connections") 16 | await Connection.filter( 17 | status=ConnectionStatus.reserved.value, 18 | created_at__lte=datetime.now(UTC) - timedelta(seconds=10), 19 | ).delete() 20 | -------------------------------------------------------------------------------- /admin/config/database.py: -------------------------------------------------------------------------------- 1 | from tortoise import Tortoise, connections 2 | from config import settings 3 | 4 | TORTOISE_MODELS = [ 5 | "aerich.models", 6 | "models.auth", 7 | "models.user", 8 | "models.settings", 9 | "models.connection", 10 | ] 11 | 12 | TORTOISE_ORM = { 13 | "connections": {"default": settings.db_url}, 14 | "apps": { 15 | "models": { 16 | "models": TORTOISE_MODELS, 17 | "default_connection": "default", 18 | }, 19 | }, 20 | } 21 | 22 | 23 | async def connect_db(generate_schemas: bool = False): 24 | await Tortoise.init( 25 | db_url=settings.db_url, 26 | modules={"models": TORTOISE_MODELS}, 27 | ) 28 | if generate_schemas: 29 | await Tortoise.generate_schemas() 30 | 31 | 32 | async def disconnect_db(): 33 | await connections.close_all() 34 | -------------------------------------------------------------------------------- /admin/config/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum as BaseEnum 2 | from typing import Any 3 | 4 | 5 | class Enum(BaseEnum): 6 | @classmethod 7 | def choices(self) -> list[tuple[str, Any]]: 8 | return [(e.name, e.value) for e in self] 9 | -------------------------------------------------------------------------------- /admin/config/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings, SettingsConfigDict 3 | from pydantic import model_validator 4 | 5 | 6 | class Settings(BaseSettings): 7 | debug: bool = Field(default=False, alias="PORTR_ADMIN_DEBUG") 8 | db_url: str = Field(alias="PORTR_DB_URL") 9 | domain: str 10 | use_vite: bool = Field(default=False, alias="PORTR_ADMIN_USE_VITE") 11 | encryption_key: str = Field(alias="PORTR_ADMIN_ENCRYPTION_KEY") 12 | 13 | github_client_id: str | None = Field(None, alias="PORTR_ADMIN_GITHUB_CLIENT_ID") 14 | github_client_secret: str | None = Field( 15 | None, alias="PORTR_ADMIN_GITHUB_CLIENT_SECRET" 16 | ) 17 | 18 | server_url: str 19 | ssh_url: str 20 | 21 | model_config = SettingsConfigDict(env_file=".env", env_prefix="PORTR_") 22 | 23 | def domain_address(self): 24 | if "localhost:" in self.domain: 25 | return f"http://{self.domain}" 26 | return f"https://{self.domain}" 27 | 28 | @model_validator(mode="after") 29 | def validate_github_auth_credentials(self): 30 | if self.github_client_id is not None and self.github_client_secret is None: 31 | raise ValueError("Github client secret is required") 32 | return self 33 | 34 | 35 | settings = Settings() # type: ignore 36 | -------------------------------------------------------------------------------- /admin/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from tortoise.contrib.test import finalizer, initializer 4 | 5 | from config.database import TORTOISE_MODELS 6 | 7 | 8 | @pytest.fixture(scope="session", autouse=True) 9 | def initialize_tests(request): 10 | db_url = os.environ.get("TORTOISE_TEST_DB", "sqlite://:memory:") 11 | initializer(TORTOISE_MODELS, db_url=db_url, app_label="models") 12 | request.addfinalizer(finalizer) 13 | -------------------------------------------------------------------------------- /admin/migrations/models/1_20240813141115_add-password-to-user.py: -------------------------------------------------------------------------------- 1 | from tortoise import BaseDBAsyncClient 2 | 3 | 4 | async def upgrade(db: BaseDBAsyncClient) -> str: 5 | return """ 6 | ALTER TABLE "user" ADD "password" VARCHAR(255);""" 7 | 8 | 9 | async def downgrade(db: BaseDBAsyncClient) -> str: 10 | return """ 11 | ALTER TABLE "user" DROP COLUMN "password";""" 12 | -------------------------------------------------------------------------------- /admin/models/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from tortoise import Model, fields 3 | 4 | from models import PkModelMixin, TimestampModelMixin 5 | from models.user import User 6 | from utils.token import generate_session_token 7 | 8 | 9 | class Session(PkModelMixin, TimestampModelMixin, Model): # type: ignore 10 | user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField( 11 | "models.User", related_name="sessions" 12 | ) 13 | token = fields.CharField( 14 | max_length=255, unique=True, default=generate_session_token 15 | ) 16 | expires_at = fields.DatetimeField( 17 | index=True, 18 | default=lambda: datetime.datetime.now(datetime.UTC) 19 | + datetime.timedelta(days=7), 20 | ) 21 | -------------------------------------------------------------------------------- /admin/models/connection.py: -------------------------------------------------------------------------------- 1 | from tortoise import Model, fields 2 | from config.enums import Enum 3 | 4 | from models import TimestampModelMixin 5 | 6 | from models.user import Team, TeamUser 7 | from utils.token import generate_connection_id 8 | 9 | 10 | class ConnectionType(str, Enum): 11 | http = "http" 12 | tcp = "tcp" 13 | 14 | 15 | class ConnectionStatus(str, Enum): 16 | reserved = "reserved" 17 | active = "active" 18 | closed = "closed" 19 | 20 | 21 | class Connection(TimestampModelMixin, Model): 22 | id = fields.CharField(max_length=26, pk=True, default=generate_connection_id) 23 | type = fields.CharField(max_length=255, choices=ConnectionType.choices()) 24 | subdomain = fields.CharField(max_length=255, null=True) 25 | port = fields.IntField(null=True) 26 | status = fields.CharField( 27 | max_length=255, 28 | choices=ConnectionStatus.choices(), 29 | default=ConnectionStatus.reserved.value, # type: ignore 30 | index=True, 31 | ) 32 | created_by: fields.ForeignKeyRelation[TeamUser] = fields.ForeignKeyField( 33 | "models.TeamUser", related_name="connections" 34 | ) 35 | started_at = fields.DatetimeField(null=True) 36 | closed_at = fields.DatetimeField(null=True) 37 | team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField( 38 | "models.Team", related_name="connections" 39 | ) 40 | -------------------------------------------------------------------------------- /admin/models/settings.py: -------------------------------------------------------------------------------- 1 | from tortoise import Model, fields 2 | 3 | from models import PkModelMixin, TimestampModelMixin, EncryptedField 4 | from models.user import User 5 | 6 | 7 | class InstanceSettings(PkModelMixin, TimestampModelMixin, Model): # type: ignore 8 | smtp_enabled = fields.BooleanField(default=False) 9 | smtp_host = fields.CharField(max_length=255, null=True) 10 | smtp_port = fields.IntField(null=True) 11 | smtp_username = fields.CharField(max_length=255, null=True) 12 | smtp_password = EncryptedField(max_length=255, null=True) 13 | from_address = fields.CharField(max_length=255, null=True) 14 | add_user_email_subject = fields.CharField(max_length=255, null=True) 15 | add_user_email_body = fields.TextField(null=True) 16 | 17 | updated_by: fields.ForeignKeyRelation[User] | None = fields.ForeignKeyField( 18 | "models.User", 19 | null=True, 20 | on_delete=fields.SET_NULL, 21 | ) 22 | -------------------------------------------------------------------------------- /admin/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "portr-admin" 3 | version = "0.0.29-beta" 4 | description = "Add your description here" 5 | authors = [{ name = "amalshaji", email = "amalshajid@gmail.com" }] 6 | dependencies = [ 7 | "nanoid>=2.0.0", 8 | "fastapi>=0.109.2", 9 | "uvicorn>=0.27.1", 10 | "tortoise-orm[asyncpg]>=0.20.0", 11 | "pydantic-settings>=2.2.0", 12 | "httpx>=0.26.0", 13 | "jinja2>=3.1.3", 14 | "python-slugify[unidecode]>=8.0.4", 15 | "python-ulid>=2.2.0", 16 | "apscheduler>=3.10.4", 17 | "pydantic>=2.6.1", 18 | "email-validator>=2.1.0.post1", 19 | "aiosmtplib>=3.0.1", 20 | "cryptography>=42.0.5", 21 | "aerich>=0.7.2", 22 | "passlib>=1.7.4", 23 | "argon2-cffi>=23.1.0", 24 | ] 25 | readme = "README.md" 26 | requires-python = ">= 3.8" 27 | 28 | [project.scripts] 29 | hello = "portr_admin:hello" 30 | 31 | [build-system] 32 | requires = ["hatchling"] 33 | build-backend = "hatchling.build" 34 | 35 | [tool.rye] 36 | managed = true 37 | dev-dependencies = [ 38 | "pre-commit>=3.5.0", 39 | "pytest>=8.0.1", 40 | "factory-boy>=3.3.0", 41 | "pytest-asyncio>=0.23.5", 42 | "asgi-lifespan>=2.1.0", 43 | "async-factory-boy>=1.0.1", 44 | "mimesis>=14.0.0", 45 | "ipdb>=0.13.13", 46 | ] 47 | 48 | [tool.hatch.metadata] 49 | allow-direct-references = true 50 | 51 | [tool.hatch.build.targets.wheel] 52 | packages = ["."] 53 | 54 | [tool.aerich] 55 | tortoise_orm = "config.database.TORTOISE_ORM" 56 | location = "./migrations" 57 | src_folder = "./." 58 | -------------------------------------------------------------------------------- /admin/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/schemas/__init__.py -------------------------------------------------------------------------------- /admin/schemas/auth.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | 3 | 4 | class LoginSchema(BaseModel): 5 | email: EmailStr 6 | password: str 7 | -------------------------------------------------------------------------------- /admin/schemas/connection.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pydantic import BaseModel 3 | 4 | from schemas.user import TeamUserSchemaForConnection 5 | 6 | 7 | class ConnectionSchema(BaseModel): 8 | id: str 9 | type: str 10 | subdomain: str | None 11 | port: int | None 12 | status: str 13 | created_at: datetime.datetime 14 | started_at: datetime.datetime | None 15 | closed_at: datetime.datetime | None 16 | 17 | created_by: TeamUserSchemaForConnection 18 | 19 | 20 | class ConnectionCreateSchema(BaseModel): 21 | connection_type: str 22 | secret_key: str 23 | subdomain: str | None 24 | -------------------------------------------------------------------------------- /admin/schemas/settings.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pydantic import BaseModel 3 | 4 | from schemas.user import UserSchema 5 | 6 | 7 | class SettingsSchemaBase(BaseModel): 8 | smtp_enabled: bool 9 | smtp_host: str | None = None 10 | smtp_port: int | None = None 11 | smtp_username: str | None = None 12 | from_address: str | None = None 13 | add_user_email_subject: str | None = None 14 | add_user_email_body: str | None = None 15 | 16 | 17 | class SettingsUpdatedBySchema(BaseModel): 18 | updated_by: UserSchema | None 19 | updated_at: datetime.datetime 20 | 21 | 22 | class SettingsUpdateSchema(SettingsSchemaBase): 23 | smtp_password: str | None = None 24 | 25 | 26 | class SettingsResponseSchema(SettingsSchemaBase, SettingsUpdatedBySchema): 27 | pass 28 | -------------------------------------------------------------------------------- /admin/schemas/team.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr 2 | 3 | from models.user import Role 4 | 5 | 6 | class NewTeamSchema(BaseModel): 7 | name: str 8 | 9 | 10 | class TeamSchema(BaseModel): 11 | id: int 12 | name: str 13 | slug: str 14 | 15 | 16 | class AddUserToTeamSchema(BaseModel): 17 | email: EmailStr 18 | role: Role 19 | set_superuser: bool 20 | -------------------------------------------------------------------------------- /admin/schemas/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from models.user import Role 4 | 5 | 6 | class GithubUserSchema(BaseModel): 7 | github_avatar_url: str 8 | 9 | 10 | class UserSchema(BaseModel): 11 | email: str 12 | first_name: str | None 13 | last_name: str | None 14 | is_superuser: bool 15 | 16 | 17 | class UserSchemaForCurrentUser(UserSchema): 18 | github_user: GithubUserSchema | None 19 | 20 | 21 | class TeamUserSchemaForCurrentUser(BaseModel): 22 | id: int 23 | secret_key: str 24 | role: Role 25 | 26 | user: UserSchemaForCurrentUser 27 | 28 | 29 | class TeamUserSchemaForTeam(BaseModel): 30 | id: int 31 | role: Role 32 | 33 | user: UserSchemaForCurrentUser 34 | 35 | 36 | class AddUserToTeamResponseSchema(BaseModel): 37 | team_user: TeamUserSchemaForTeam 38 | password: str | None = None 39 | 40 | 41 | class TeamUserSchemaForConnection(BaseModel): 42 | id: int 43 | 44 | user: UserSchema 45 | 46 | 47 | class UserUpdateSchema(BaseModel): 48 | first_name: str | None = None 49 | last_name: str | None = None 50 | 51 | 52 | class ChangePasswordSchema(BaseModel): 53 | password: str 54 | -------------------------------------------------------------------------------- /admin/scripts/generate-encryption-key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python -c "import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" -------------------------------------------------------------------------------- /admin/scripts/pre-deploy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aerich import Command # type: ignore 3 | 4 | from config.database import TORTOISE_ORM, connect_db, disconnect_db 5 | from services.settings import populate_instance_settings 6 | 7 | 8 | command = Command(tortoise_config=TORTOISE_ORM) 9 | 10 | 11 | async def run_migrations(): 12 | await command.init() 13 | await command.upgrade(run_in_transaction=True) 14 | 15 | 16 | async def populate_settings(): 17 | await connect_db() 18 | await populate_instance_settings() 19 | await disconnect_db() 20 | 21 | 22 | async def main(): 23 | await run_migrations() 24 | await populate_settings() 25 | 26 | 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /admin/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python scripts/pre-deploy.py 4 | python main.py 5 | -------------------------------------------------------------------------------- /admin/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/services/__init__.py -------------------------------------------------------------------------------- /admin/services/auth.py: -------------------------------------------------------------------------------- 1 | from models.auth import Session 2 | from models.user import User 3 | 4 | 5 | async def login_user(user: User) -> str: 6 | session = await Session.create(user=user) 7 | return session.token 8 | -------------------------------------------------------------------------------- /admin/services/connection.py: -------------------------------------------------------------------------------- 1 | from models.connection import Connection, ConnectionStatus, ConnectionType 2 | from models.user import TeamUser 3 | from utils.exception import ServiceError 4 | 5 | 6 | async def create_new_connection( 7 | type: ConnectionType, 8 | created_by: TeamUser, 9 | subdomain: str | None = None, 10 | port: int | None = None, 11 | ) -> Connection: 12 | if type == ConnectionType.http and not subdomain: 13 | raise ServiceError("subdomain is required for http connections") 14 | 15 | if type == ConnectionType.http: 16 | active_connection = await Connection.filter( 17 | subdomain=subdomain, status=ConnectionStatus.active.value 18 | ).first() 19 | if active_connection: 20 | raise ServiceError("Subdomain already in use") 21 | 22 | return await Connection.create( 23 | type=type, 24 | subdomain=subdomain if type == ConnectionType.http else None, 25 | port=port if type == ConnectionType.tcp else None, 26 | created_by=created_by, 27 | team=created_by.team, 28 | ) 29 | -------------------------------------------------------------------------------- /admin/services/settings.py: -------------------------------------------------------------------------------- 1 | from models.settings import InstanceSettings 2 | import logging 3 | 4 | 5 | DEFAULT_SMTP_ENABLED = False 6 | DEFAULT_ADD_USER_EMAIL_SUBJECT = """ 7 | You've been added to team {{teamName}} on Portr! 8 | """.strip() 9 | DEFAULT_ADD_USER_EMAIL_BODY = """ 10 | Hello {{email}} 11 | 12 | You've been added to team "{{teamName}}" on Portr. 13 | 14 | Get started by signing in with your github account at {{dashboardUrl}} 15 | """.strip() 16 | 17 | 18 | async def populate_instance_settings(): 19 | logger = logging.getLogger() 20 | settings = await InstanceSettings.first() 21 | if not settings: 22 | logger.info("Creating default instance settings") 23 | settings = await InstanceSettings.create( 24 | smtp_enabled=DEFAULT_SMTP_ENABLED, 25 | add_user_email_subject=DEFAULT_ADD_USER_EMAIL_SUBJECT, 26 | add_user_email_body=DEFAULT_ADD_USER_EMAIL_BODY, 27 | ) 28 | return settings 29 | 30 | 31 | async def get_instance_settings() -> InstanceSettings: 32 | settings = await InstanceSettings.filter().select_related("updated_by").first() 33 | if not settings: 34 | raise Exception("Instance settings not found") 35 | return settings 36 | -------------------------------------------------------------------------------- /admin/static/Geist-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/static/Geist-Regular.woff2 -------------------------------------------------------------------------------- /admin/static/Manrope-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/static/Manrope-VariableFont_wght.ttf -------------------------------------------------------------------------------- /admin/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/static/favicon.ico -------------------------------------------------------------------------------- /admin/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/static/geist-mono-latin-400-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/static/geist-mono-latin-400-normal.woff2 -------------------------------------------------------------------------------- /admin/static/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /admin/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Portr - Expose local http, tcp or websocket connections to the public 9 | internet 10 | 11 | 17 | 18 | 19 |
20 | {% if use_vite %} 21 | 25 | 29 | {% else %}{{ vite_tags | safe }}{% endif %} 30 | 31 | 32 | -------------------------------------------------------------------------------- /admin/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient as BaseTestClient 2 | from main import app 3 | from models.user import TeamUser, User 4 | from tests.factories import SessionFactory 5 | 6 | 7 | class TestClient: 8 | @classmethod 9 | async def get_client(cls): 10 | # async so that the signature matches the other method 11 | return BaseTestClient(app) 12 | 13 | @classmethod 14 | async def get_logged_in_client(cls, auth_user: User | TeamUser): 15 | # Separate into two methods? 16 | if isinstance(auth_user, User): 17 | user = auth_user 18 | team_user = None 19 | else: 20 | user = auth_user.user 21 | team_user = auth_user 22 | 23 | client = await cls.get_client() 24 | 25 | session = await SessionFactory.create(user=user) 26 | client.cookies["portr_session"] = session.token 27 | if team_user: 28 | client.headers["x-team-slug"] = team_user.team.slug 29 | 30 | return client 31 | -------------------------------------------------------------------------------- /admin/tests/api_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/tests/api_tests/__init__.py -------------------------------------------------------------------------------- /admin/tests/factories.py: -------------------------------------------------------------------------------- 1 | from models.auth import Session 2 | from models.connection import Connection, ConnectionStatus, ConnectionType 3 | from models.user import Team, TeamUser, User 4 | from factory import SubFactory, Sequence, LazyAttribute # type: ignore 5 | from async_factory_boy.factory.tortoise import AsyncTortoiseFactory # type: ignore 6 | import mimesis 7 | 8 | 9 | class UserFactory(AsyncTortoiseFactory): 10 | class Meta: 11 | model = User 12 | 13 | email = LazyAttribute(lambda _: mimesis.Person().email()) 14 | 15 | 16 | class SessionFactory(AsyncTortoiseFactory): 17 | class Meta: 18 | model = Session 19 | 20 | user = SubFactory(UserFactory) 21 | 22 | 23 | class TeamFactory(AsyncTortoiseFactory): 24 | class Meta: 25 | model = Team 26 | 27 | name = Sequence(lambda n: f"test team-{n}") 28 | slug = Sequence(lambda n: f"test-team-{n}") 29 | 30 | 31 | class TeamUserFactory(AsyncTortoiseFactory): 32 | class Meta: 33 | model = TeamUser 34 | 35 | user = SubFactory(UserFactory) 36 | team = SubFactory(TeamFactory) 37 | role = "admin" 38 | 39 | 40 | class ConnectionFactory(AsyncTortoiseFactory): 41 | class Meta: 42 | model = Connection 43 | 44 | type = ConnectionType.http 45 | subdomain = LazyAttribute(lambda _: mimesis.Person().username()) 46 | status = ConnectionStatus.reserved 47 | created_by = SubFactory(TeamUserFactory) 48 | -------------------------------------------------------------------------------- /admin/tests/service_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/tests/service_tests/__init__.py -------------------------------------------------------------------------------- /admin/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from utils.template_renderer import render_template 3 | 4 | 5 | @pytest.fixture 6 | def context(): 7 | return {"name": "John", "age": 30} 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "test_input,expected", 12 | [ 13 | ( 14 | "Hello, my name is {{ name }} and I am {{ age }} years old.", 15 | "Hello, my name is John and I am 30 years old.", 16 | ), 17 | ( 18 | "Hello, my name is {{ name }} and I am {{ age years old.", 19 | "Hello, my name is John and I am {{ age years old.", 20 | ), 21 | ], 22 | ) 23 | def test_render_template(context, test_input, expected): 24 | assert render_template(test_input, context) == expected 25 | -------------------------------------------------------------------------------- /admin/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amalshaji/portr/4b5e8ffd95140acbcbea12b300f1cb6435df27ab/admin/utils/__init__.py -------------------------------------------------------------------------------- /admin/utils/exception.py: -------------------------------------------------------------------------------- 1 | class PortrError(Exception): 2 | def __init__(self, message: str | None = None) -> None: 3 | self.message = message 4 | 5 | 6 | class ServiceError(PortrError): 7 | pass 8 | 9 | 10 | class PermissionDenied(PortrError): 11 | pass 12 | -------------------------------------------------------------------------------- /admin/utils/smtp.py: -------------------------------------------------------------------------------- 1 | import aiosmtplib 2 | from email.message import EmailMessage 3 | from services import settings as settings_service 4 | 5 | 6 | async def send_mail(to: str, subject: str, body: str): 7 | settings = await settings_service.get_instance_settings() 8 | 9 | message = EmailMessage() 10 | message["From"] = settings.from_address # type: ignore 11 | message["To"] = to 12 | message["Subject"] = subject 13 | message.set_content(body) 14 | 15 | await aiosmtplib.send( 16 | message, 17 | hostname=settings.smtp_host, # type: ignore 18 | port=settings.smtp_port, # type: ignore 19 | username=settings.smtp_username, # type: ignore 20 | password=settings.smtp_password, # type: ignore 21 | use_tls=True, 22 | ) 23 | -------------------------------------------------------------------------------- /admin/utils/template_renderer.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def render_template(template, variables): 5 | pattern = r"{{\s*([^{}]*)\s*}}" 6 | 7 | def replace(match): 8 | var_name = match.group(1).strip() 9 | return str(variables.get(var_name, match.group(0))) 10 | 11 | rendered_template = re.sub(pattern, replace, template) 12 | return rendered_template 13 | -------------------------------------------------------------------------------- /admin/utils/token.py: -------------------------------------------------------------------------------- 1 | import string 2 | import nanoid # type: ignore 3 | from ulid import ULID 4 | 5 | NANOID_ALPHABETS = string.ascii_letters + string.digits 6 | 7 | 8 | def generate_secret_key() -> str: 9 | return f"portr_{nanoid.generate(size=36, alphabet=NANOID_ALPHABETS)}" 10 | 11 | 12 | def generate_oauth_state() -> str: 13 | return nanoid.generate(size=26) 14 | 15 | 16 | def generate_session_token() -> str: 17 | return nanoid.generate(size=32) 18 | 19 | 20 | def generate_connection_id() -> str: 21 | return str(ULID()) 22 | 23 | 24 | def generate_random_password() -> str: 25 | return nanoid.generate(size=16) 26 | -------------------------------------------------------------------------------- /admin/utils/vite.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | import json 3 | 4 | from pathlib import Path 5 | 6 | MANIFEST_PATH = Path(__file__).parent.parent / "web/dist/static/.vite/manifest.json" 7 | 8 | 9 | @cache 10 | def generate_vite_tags() -> str: 11 | if not MANIFEST_PATH.exists(): 12 | raise FileNotFoundError("manifest.json not found") 13 | 14 | manifest_json = json.loads(MANIFEST_PATH.read_text()) 15 | 16 | tag = "" 17 | 18 | for style in manifest_json["index.html"]["css"]: 19 | tag += f'' 20 | 21 | if manifest_json["index.html"]["file"]: 22 | tag += f'' 23 | 24 | return tag.strip() 25 | -------------------------------------------------------------------------------- /admin/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /admin/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "style": "new-york", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/app.pcss", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils" 12 | } 13 | } -------------------------------------------------------------------------------- /admin/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Portr 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portr-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json" 11 | }, 12 | "devDependencies": { 13 | "@dicebear/collection": "^8.0.1", 14 | "@dicebear/core": "^8.0.1", 15 | "@sveltejs/vite-plugin-svelte": "^2.5.3", 16 | "@tsconfig/svelte": "^5.0.4", 17 | "autoprefixer": "^10.4.19", 18 | "bits-ui": "^0.22.0", 19 | "clsx": "^2.1.0", 20 | "formsnap": "^0.4.4", 21 | "highlight.js": "^11.9.0", 22 | "postcss": "^8.4.38", 23 | "postcss-load-config": "^4.0.2", 24 | "radix-icons-svelte": "^1.2.1", 25 | "svelte": "^4.2.19", 26 | "svelte-check": "^3.6.9", 27 | "svelte-headless-table": "^0.17.7", 28 | "svelte-highlight": "^7.6.0", 29 | "svelte-routing": "^2.12.0", 30 | "tailwind-merge": "^2.2.2", 31 | "tailwind-variants": "^0.1.20", 32 | "tailwindcss": "^3.4.3", 33 | "tslib": "^2.6.2", 34 | "typescript": "^5.4.5", 35 | "vite": "^5.4.18" 36 | }, 37 | "dependencies": { 38 | "lucide-svelte": "0.352.0", 39 | "moment": "^2.30.1", 40 | "svelte-legos": "^0.2.2", 41 | "svelte-sonner": "^0.3.22", 42 | "zod": "^3.22.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /admin/web/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require("tailwindcss"); 2 | const autoprefixer = require("autoprefixer"); 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer, 10 | ], 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /admin/web/src/App.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ApiError.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | Error 11 | 12 | {#if typeof error === "string"} 13 | {error} 14 | {:else} 15 |
    16 | {#each Object.keys(error) as key} 17 | {#each Object.keys(error[key]) as message} 18 |
  • {error[key][message]}
  • 19 | {/each} 20 | {/each} 21 |
22 | {/if} 23 |
24 |
25 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ConnectionStatus.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if status === "closed"} 9 | closed 10 | {:else} 11 | active 12 | {/if} 13 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ConnectionType.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if type === "http"} 9 | HTTP 10 | {:else} 11 | TCP 12 | {/if} 13 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/DateField.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 18 | 19 | 20 |

{moment(Date).format("DD MMM YYYY, HH:mm:ss")}

21 |
22 |
23 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ErrorText.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

{error}

6 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/Pagination.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | {#each pages as page (page.key)} 21 | {#if page.type === "ellipsis"} 22 | 23 | 24 | 25 | {:else} 26 | 27 | 28 | {page.value} 29 | 30 | 31 | {/if} 32 | {/each} 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/copyToClipboard.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | {#if copied} 21 | 24 | {:else} 25 | 28 | {/if} 29 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/data-table-skeleton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/error.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
9 |
10 | 15 | 18 | 19 |
20 | 21 |
22 |
23 | Error 24 |

25 | {error} 26 |

27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/goback.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/issue-link.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/sidebarlink.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |
15 | 16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; 2 | 3 | const Root = AlertDialogPrimitive.Root; 4 | const Trigger = AlertDialogPrimitive.Trigger; 5 | 6 | import Title from "./alert-dialog-title.svelte"; 7 | import Action from "./alert-dialog-action.svelte"; 8 | import Cancel from "./alert-dialog-cancel.svelte"; 9 | import Portal from "./alert-dialog-portal.svelte"; 10 | import Footer from "./alert-dialog-footer.svelte"; 11 | import Header from "./alert-dialog-header.svelte"; 12 | import Overlay from "./alert-dialog-overlay.svelte"; 13 | import Content from "./alert-dialog-content.svelte"; 14 | import Description from "./alert-dialog-description.svelte"; 15 | 16 | export { 17 | Root, 18 | Title, 19 | Action, 20 | Cancel, 21 | Portal, 22 | Footer, 23 | Header, 24 | Trigger, 25 | Overlay, 26 | Content, 27 | Description, 28 | // 29 | Root as AlertDialog, 30 | Title as AlertDialogTitle, 31 | Action as AlertDialogAction, 32 | Cancel as AlertDialogCancel, 33 | Portal as AlertDialogPortal, 34 | Footer as AlertDialogFooter, 35 | Header as AlertDialogHeader, 36 | Trigger as AlertDialogTrigger, 37 | Overlay as AlertDialogOverlay, 38 | Content as AlertDialogContent, 39 | Description as AlertDialogDescription 40 | }; 41 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert/alert-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert/alert-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert/alert.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/alert/index.ts: -------------------------------------------------------------------------------- 1 | import { tv, type VariantProps } from "tailwind-variants"; 2 | 3 | import Root from "./alert.svelte"; 4 | import Description from "./alert-description.svelte"; 5 | import Title from "./alert-title.svelte"; 6 | 7 | export const alertVariants = tv({ 8 | base: "relative w-full rounded-sm border px-4 py-3 text-sm [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }); 20 | 21 | export type Variant = VariantProps["variant"]; 22 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 23 | 24 | export { 25 | Root, 26 | Description, 27 | Title, 28 | // 29 | Root as Alert, 30 | Description as AlertDescription, 31 | Title as AlertTitle, 32 | }; 33 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/avatar/avatar-fallback.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/avatar/avatar-image.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/avatar/avatar.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./avatar.svelte"; 2 | import Image from "./avatar-image.svelte"; 3 | import Fallback from "./avatar-fallback.svelte"; 4 | 5 | export { 6 | Root, 7 | Image, 8 | Fallback, 9 | // 10 | Root as Avatar, 11 | Image as AvatarImage, 12 | Fallback as AvatarFallback 13 | }; 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { tv, type VariantProps } from "tailwind-variants"; 2 | 3 | export { default as Badge } from "./badge.svelte"; 4 | export const badgeVariants = tv({ 5 | base: "inline-flex items-center rounded-sm border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none", 6 | variants: { 7 | variant: { 8 | default: 9 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 10 | secondary: 11 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 12 | destructive: 13 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 14 | outline: "text-foreground", 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: "default", 19 | }, 20 | }); 21 | 22 | export type Variant = VariantProps["variant"]; 23 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |

12 | 13 |

14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./card.svelte"; 2 | import Content from "./card-content.svelte"; 3 | import Description from "./card-description.svelte"; 4 | import Footer from "./card-footer.svelte"; 5 | import Header from "./card-header.svelte"; 6 | import Title from "./card-title.svelte"; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle 22 | }; 23 | 24 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 25 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 28 | {#if isIndeterminate} 29 | 30 | {:else} 31 | 32 | {/if} 33 | 34 | 35 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./checkbox.svelte"; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox 6 | }; 7 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 28 | 29 | 32 | 33 | Close 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from "bits-ui"; 2 | 3 | import Title from "./dialog-title.svelte"; 4 | import Portal from "./dialog-portal.svelte"; 5 | import Footer from "./dialog-footer.svelte"; 6 | import Header from "./dialog-header.svelte"; 7 | import Overlay from "./dialog-overlay.svelte"; 8 | import Content from "./dialog-content.svelte"; 9 | import Description from "./dialog-description.svelte"; 10 | 11 | const Root = DialogPrimitive.Root; 12 | const Trigger = DialogPrimitive.Trigger; 13 | const Close = DialogPrimitive.Close; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose, 37 | }; 38 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; 2 | import Item from "./dropdown-menu-item.svelte"; 3 | import Label from "./dropdown-menu-label.svelte"; 4 | import Content from "./dropdown-menu-content.svelte"; 5 | import Shortcut from "./dropdown-menu-shortcut.svelte"; 6 | import RadioItem from "./dropdown-menu-radio-item.svelte"; 7 | import Separator from "./dropdown-menu-separator.svelte"; 8 | import RadioGroup from "./dropdown-menu-radio-group.svelte"; 9 | import SubContent from "./dropdown-menu-sub-content.svelte"; 10 | import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; 11 | import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; 12 | 13 | const Sub = DropdownMenuPrimitive.Sub; 14 | const Root = DropdownMenuPrimitive.Root; 15 | const Trigger = DropdownMenuPrimitive.Trigger; 16 | const Group = DropdownMenuPrimitive.Group; 17 | 18 | export { 19 | Sub, 20 | Root, 21 | Item, 22 | Label, 23 | Group, 24 | Trigger, 25 | Content, 26 | Shortcut, 27 | Separator, 28 | RadioItem, 29 | SubContent, 30 | SubTrigger, 31 | RadioGroup, 32 | CheckboxItem, 33 | // 34 | Root as DropdownMenu, 35 | Sub as DropdownMenuSub, 36 | Item as DropdownMenuItem, 37 | Label as DropdownMenuLabel, 38 | Group as DropdownMenuGroup, 39 | Content as DropdownMenuContent, 40 | Trigger as DropdownMenuTrigger, 41 | Shortcut as DropdownMenuShortcut, 42 | RadioItem as DropdownMenuRadioItem, 43 | Separator as DropdownMenuSeparator, 44 | RadioGroup as DropdownMenuRadioGroup, 45 | SubContent as DropdownMenuSubContent, 46 | SubTrigger as DropdownMenuSubTrigger, 47 | CheckboxItem as DropdownMenuCheckboxItem 48 | }; 49 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | type FormInputEvent = T & { 4 | currentTarget: EventTarget & HTMLInputElement; 5 | }; 6 | export type InputEvents = { 7 | blur: FormInputEvent; 8 | change: FormInputEvent; 9 | click: FormInputEvent; 10 | focus: FormInputEvent; 11 | focusin: FormInputEvent; 12 | focusout: FormInputEvent; 13 | keydown: FormInputEvent; 14 | keypress: FormInputEvent; 15 | keyup: FormInputEvent; 16 | mouseover: FormInputEvent; 17 | mouseenter: FormInputEvent; 18 | mouseleave: FormInputEvent; 19 | paste: FormInputEvent; 20 | input: FormInputEvent; 21 | }; 22 | 23 | export { 24 | Root, 25 | // 26 | Root as Input 27 | }; 28 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./label.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label 7 | }; 8 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./pagination.svelte"; 2 | import Content from "./pagination-content.svelte"; 3 | import Item from "./pagination-item.svelte"; 4 | import Link from "./pagination-link.svelte"; 5 | import PrevButton from "./pagination-prev-button.svelte"; 6 | import NextButton from "./pagination-next-button.svelte"; 7 | import Ellipsis from "./pagination-ellipsis.svelte"; 8 | export { 9 | Root, 10 | Content, 11 | Item, 12 | Link, 13 | PrevButton, 14 | NextButton, 15 | Ellipsis, 16 | // 17 | Root as Pagination, 18 | Content as PaginationContent, 19 | Item as PaginationItem, 20 | Link as PaginationLink, 21 | PrevButton as PaginationPrevButton, 22 | NextButton as PaginationNextButton, 23 | Ellipsis as PaginationEllipsis 24 | }; 25 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination-content.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 |
14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination-ellipsis.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | More pages 19 | 20 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination-item.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
  • 12 | 13 |
  • 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination-link.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | {page.value} 34 | 35 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination-next-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 27 | 28 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 27 | 28 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/pagination/pagination.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from "bits-ui"; 2 | 3 | import Label from "./select-label.svelte"; 4 | import Item from "./select-item.svelte"; 5 | import Content from "./select-content.svelte"; 6 | import Trigger from "./select-trigger.svelte"; 7 | import Separator from "./select-separator.svelte"; 8 | 9 | const Root = SelectPrimitive.Root; 10 | const Group = SelectPrimitive.Group; 11 | const Input = SelectPrimitive.Input; 12 | const Value = SelectPrimitive.Value; 13 | 14 | export { 15 | Root, 16 | Item, 17 | Group, 18 | Input, 19 | Label, 20 | Value, 21 | Content, 22 | Trigger, 23 | Separator, 24 | // 25 | Root as Select, 26 | Item as SelectItem, 27 | Group as SelectGroup, 28 | Input as SelectInput, 29 | Label as SelectLabel, 30 | Value as SelectValue, 31 | Content as SelectContent, 32 | Trigger as SelectTrigger, 33 | Separator as SelectSeparator 34 | }; 35 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 |
    34 | 35 |
    36 |
    37 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/select/select-label.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 |
    22 | 23 |
    24 |
    25 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./separator.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator 7 | }; 8 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./skeleton.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Skeleton 7 | }; 8 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./switch.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Switch 7 | }; 8 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/switch/switch.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./table.svelte"; 2 | import Body from "./table-body.svelte"; 3 | import Caption from "./table-caption.svelte"; 4 | import Cell from "./table-cell.svelte"; 5 | import Footer from "./table-footer.svelte"; 6 | import Head from "./table-head.svelte"; 7 | import Header from "./table-header.svelte"; 8 | import Row from "./table-row.svelte"; 9 | 10 | export { 11 | Root, 12 | Body, 13 | Caption, 14 | Cell, 15 | Footer, 16 | Head, 17 | Header, 18 | Row, 19 | // 20 | Root as Table, 21 | Body as TableBody, 22 | Caption as TableCaption, 23 | Cell as TableCell, 24 | Footer as TableFooter, 25 | Head as TableHead, 26 | Header as TableHeader, 27 | Row as TableRow 28 | }; 29 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-body.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-caption.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-cell.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | [role=checkbox]]:translate-y-[2px]", 14 | className 15 | )} 16 | {...$$restProps} 17 | on:click 18 | on:keydown 19 | > 20 | 21 | 22 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-head.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | [role=checkbox]]:translate-y-[2px]", 14 | className 15 | )} 16 | {...$$restProps} 17 | > 18 | 19 | 20 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table-row.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/table/table.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/textarea/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./textarea.svelte"; 2 | 3 | type FormTextareaEvent = T & { 4 | currentTarget: EventTarget & HTMLTextAreaElement; 5 | }; 6 | 7 | type TextareaEvents = { 8 | blur: FormTextareaEvent; 9 | change: FormTextareaEvent; 10 | click: FormTextareaEvent; 11 | focus: FormTextareaEvent; 12 | keydown: FormTextareaEvent; 13 | keypress: FormTextareaEvent; 14 | keyup: FormTextareaEvent; 15 | mouseover: FormTextareaEvent; 16 | mouseenter: FormTextareaEvent; 17 | mouseleave: FormTextareaEvent; 18 | paste: FormTextareaEvent; 19 | input: FormTextareaEvent; 20 | }; 21 | 22 | export { 23 | Root, 24 | // 25 | Root as Textarea, 26 | type TextareaEvents, 27 | type FormTextareaEvent 28 | }; 29 | -------------------------------------------------------------------------------- /admin/web/src/lib/components/ui/textarea/textarea.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |