Kaynağa Gözat

Merge pull request #159 from fosrl/dev

1.0.0-beta12
Milo Schwartz 5 ay önce
ebeveyn
işleme
9e5d5e8990
38 değiştirilmiş dosya ile 1364 ekleme ve 306 silme
  1. 2 7
      .github/workflows/cicd.yml
  2. 5 1
      README.md
  3. 6 1
      docker-compose.example.yml
  4. 6 1
      install/fs/docker-compose.yml
  5. 267 0
      internationalization/de.md
  6. 20 0
      internationalization/pl.md
  7. 1 1
      package.json
  8. 2 1
      server/db/schema.ts
  9. 12 10
      server/lib/config.ts
  10. 3 1
      server/lib/consts.ts
  11. 0 16
      server/lib/loadAppVersion.ts
  12. 7 0
      server/routers/external.ts
  13. 2 14
      server/routers/gerbil/getConfig.ts
  14. 4 4
      server/routers/newt/targets.ts
  15. 51 14
      server/routers/resource/createResource.ts
  16. 2 17
      server/routers/resource/deleteResource.ts
  17. 1 0
      server/routers/resource/index.ts
  18. 192 0
      server/routers/resource/transferResource.ts
  19. 114 13
      server/routers/resource/updateResource.ts
  20. 1 1
      server/routers/target/createTarget.ts
  21. 2 17
      server/routers/target/deleteTarget.ts
  22. 18 0
      server/routers/target/helpers.ts
  23. 1 1
      server/routers/target/updateTarget.ts
  24. 5 2
      server/routers/traefik/getTraefikConfig.ts
  25. 6 1
      server/setup/copyInConfig.ts
  26. 40 10
      server/setup/migrations.ts
  27. 62 0
      server/setup/scripts/1.0.0-beta12.ts
  28. 117 45
      src/app/[orgId]/settings/resources/CreateResourceForm.tsx
  29. 26 26
      src/app/[orgId]/settings/resources/ResourcesTable.tsx
  30. 10 9
      src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx
  31. 1 2
      src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
  32. 309 52
      src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
  33. 8 5
      src/app/[orgId]/settings/resources/page.tsx
  34. 1 1
      src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx
  35. 2 7
      src/app/[orgId]/settings/sites/CreateSiteForm.tsx
  36. 46 23
      src/components/ui/checkbox.tsx
  37. 10 3
      src/lib/pullEnv.ts
  38. 2 0
      src/lib/types/env.ts

+ 2 - 7
.github/workflows/cicd.yml

@@ -35,13 +35,8 @@ jobs:
             - name: Update version in package.json
             - name: Update version in package.json
               run: |
               run: |
                   TAG=${{ env.TAG }}
                   TAG=${{ env.TAG }}
-                  if [ -f package.json ]; then
-                    jq --arg version "$TAG" '.version = $version' package.json > package.tmp.json && mv package.tmp.json package.json
-                    echo "Updated package.json with version $TAG"
-                  else
-                    echo "package.json not found"
-                  fi
-                  cat package.json
+                  sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
+                  cat server/lib/
 
 
             - name: Pull latest Gerbil version
             - name: Pull latest Gerbil version
               id: get-gerbil-tag
               id: get-gerbil-tag

+ 5 - 1
README.md

@@ -6,7 +6,7 @@
 [![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
 [![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
 [![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
 [![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
 
 
-Pangolin is a self-hosted tunneled reverse proxy management server with identity and access management, designed to securely expose private resources through use with the Traefik reverse proxy and WireGuard tunnel clients like Newt. With Pangolin, you retain full control over your infrastructure while providing a user-friendly and feature-rich solution for managing proxies, authentication, and access, and simplifying complex network setups, all with a clean and simple UI.
+Pangolin is a self-hosted tunneled reverse proxy management server with identity and access control, designed to securely expose private resources through use with the Traefik reverse proxy and WireGuard tunnel clients like Newt. With Pangolin, you retain full control over your infrastructure while providing a user-friendly and feature-rich solution for managing proxies, authentication, and access, and simplifying complex network setups, all with a clean and simple UI.
 
 
 ### Installation and Documentation
 ### Installation and Documentation
 
 
@@ -129,6 +129,10 @@ Pangolin was inspired by several existing projects and concepts:
 -   **Authentik and Authelia**:  
 -   **Authentik and Authelia**:  
     These projects inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
     These projects inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
 
 
+## Project Development / Roadmap
+
+Pangolin is under active development, and we are continuously adding new features and improvements. View the [project board](https://github.com/orgs/fosrl/projects/1) for more detailed info.
+
 ## Licensing
 ## Licensing
 
 
 Pangolin is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
 Pangolin is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.

+ 6 - 1
docker-compose.example.yml

@@ -37,7 +37,7 @@ services:
       - 80:80 # Port for traefik because of the network_mode
       - 80:80 # Port for traefik because of the network_mode
 
 
   traefik:
   traefik:
-    image: traefik:v3.1
+    image: traefik:v3.3.3
     container_name: traefik
     container_name: traefik
     restart: unless-stopped
     restart: unless-stopped
     network_mode: service:gerbil # Ports appear on the gerbil service
     network_mode: service:gerbil # Ports appear on the gerbil service
@@ -49,3 +49,8 @@ services:
     volumes:
     volumes:
       - ./traefik:/etc/traefik:ro # Volume to store the Traefik configuration
       - ./traefik:/etc/traefik:ro # Volume to store the Traefik configuration
       - ./letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
       - ./letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
+
+networks:
+  default:
+    driver: bridge
+    name: pangolin

+ 6 - 1
install/fs/docker-compose.yml

@@ -36,7 +36,7 @@ services:
 {{end}}
 {{end}}
 
 
   traefik:
   traefik:
-    image: traefik:v3.1
+    image: traefik:v3.3.3
     container_name: traefik
     container_name: traefik
     restart: unless-stopped
     restart: unless-stopped
 {{if .InstallGerbil}}
 {{if .InstallGerbil}}
@@ -55,3 +55,8 @@ services:
     volumes:
     volumes:
       - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
       - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
       - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
       - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
+
+networks:
+  default:
+    driver: bridge
+    name: pangolin

+ 267 - 0
internationalization/de.md

@@ -0,0 +1,267 @@
+## Login site
+
+| EN                    | DE                                 | Notes       |
+| --------------------- | ---------------------------------- | ----------- |
+| Welcome to Pangolin   | Willkommen bei Pangolin            |             |
+| Log in to get started | Melden Sie sich an, um zu beginnen |             |
+| Email                 | E-Mail                             |             |
+| Enter your email      | Geben Sie Ihre E-Mail-Adresse ein  | placeholder |
+| Password              | Passwort                           |             |
+| Enter your password   | Geben Sie Ihr Passwort ein         | placeholder |
+| Forgot your password? | Passwort vergessen?                |             |
+| Log in                | Anmelden                           |             |
+
+# Ogranization site after successful login
+
+| EN                                        | DE                                           | Notes |
+| ----------------------------------------- | -------------------------------------------- | ----- |
+| Welcome to Pangolin                       | Willkommen bei Pangolin                      |       |
+| You're a member of {number} organization. | Sie sind Mitglied von {number} Organisation. |       |
+
+## Shared Header, Navbar and Footer
+##### Header
+
+| EN                  | DE                  | Notes |
+| ------------------- | ------------------- | ----- |
+| Documentation       | Dokumentation       |       |
+| Support             | Support             |       |
+| Organization {name} | Organisation {name} |       |
+##### Organization selector
+
+| EN               | DE                | Notes |
+| ---------------- | ----------------- | ----- |
+| Search…          | Suchen…           |       |
+| Create           | Erstellen         |       |
+| New Organization | Neue Organisation |       |
+| Organizations    | Organisationen    |       |
+
+##### Navbar
+
+| EN              | DE                | Notes |
+| --------------- | ----------------- | ----- |
+| Sites           | Websites          |       |
+| Resources       | Ressourcen        |       |
+| User & Roles    | Benutzer & Rollen |       |
+| Shareable Links | Teilbare Links    |       |
+| General         | Allgemein         |       |
+##### Footer
+| EN                        | DE                          |                     |
+| ------------------------- | --------------------------- | ------------------- |
+| Page {number} of {number} | Seite {number} von {number} |                     |
+| Rows per page             | Zeilen pro Seite            |                     |
+| Pangolin                  | Pangolin                    | unten auf der Seite |
+| Built by Fossorial        | Erstellt von Fossorial      | unten auf der Seite |
+| Open Source               | Open Source                 | unten auf der Seite |
+| Documentation             | Dokumentation               | unten auf der Seite |
+| {version}                 | {version}                   | unten auf der Seite |
+
+## Main “Sites”
+##### “Hero” section
+
+| EN                                                           | DE                                                           | Notes |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
+| Newt (Recommended)                                           | Newt (empfohlen)                                             |       |
+| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Für das beste Benutzererlebnis verwenden Sie Newt. Es nutzt WireGuard im Hintergrund und ermöglicht es Ihnen, auf Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk direkt aus dem Pangolin-Dashboard zuzugreifen. |       |
+| Runs in Docker                                               | Läuft in Docker                                              |       |
+| Runs in shell on macOS, Linux, and Windows                   | Läuft in der Shell auf macOS, Linux und Windows              |       |
+| Install Newt                                                 | Newt installieren                                            |       |
+| Basic WireGuard<br>                                          | Verwenden Sie einen beliebigen WireGuard-Client, um eine Verbindung herzustellen. Sie müssen auf Ihre internen Ressourcen über die Peer-IP-Adresse zugreifen. |       |
+| Compatible with all WireGuard clients<br>                    | Kompatibel mit allen WireGuard-Clients<br>                   |       |
+| Manual configuration required                                | Manuelle Konfiguration erforderlich<br>                      |       |
+##### Content
+
+| EN                                                        | DE                                                           | Notes                            |
+| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
+| Manage Sites                                              | Seiten verwalten                                             |                                  |
+| Allow connectivity to your network through secure tunnels | Ermöglichen Sie die Verbindung zu Ihrem Netzwerk über ein sicheren Tunnel |                                  |
+| Search sites                                              | Seiten suchen                                                | placeholder                      |
+| Add Site                                                  | Seite hinzufügen                                             |                                  |
+| Name                                                      | Name                                                         | table header                     |
+| Online                                                    | Status                                                       | table header                     |
+| Site                                                      | Seite                                                        | table header                     |
+| Data In                                                   | Eingehende Daten                                             | table header                     |
+| Data Out                                                  | Ausgehende Daten                                             | table header                     |
+| Connection Type                                           | Verbindungstyp                                               | table header                     |
+| Online                                                    | Online                                                       | site state                       |
+| Offline                                                   | Offline                                                      | site state                       |
+| Edit →                                                    | Bearbeiten →                                                 |                                  |
+| View settings                                             | Einstellungen anzeigen                                       | Popup after clicking “…” on site |
+| Delete                                                    | Löschen                                                      | Popup after clicking “…” on site |
+##### Add Site Popup
+
+| EN                                                     | DE                                                          | Notes       |
+| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- |
+| Create Site                                            | Seite erstellen                                             |             |
+| Create a new site to start connection for this site    | Erstellen Sie eine neue Seite, um die Verbindung zu starten |             |
+| Name                                                   | Name                                                        |             |
+| Site name                                              | Seiten-Name                                                 | placeholder |
+| This is the name that will be displayed for this site. | So wird Ihre Seite angezeigt                                | desc        |
+| Method                                                 | Methode                                                     |             |
+| Local                                                  | Lokal                                                       |             |
+| Newt                                                   | Newt                                                        |             |
+| WireGuard                                              | WireGuard                                                   |             |
+| This is how you will expose connections.               | So werden Verbindungen freigegeben.                         |             |
+| You will only be able to see the configuration once.   | Diese Konfiguration können Sie nur einmal sehen.            |             |
+| Learn how to install Newt on your system               | Erfahren Sie, wie Sie Newt auf Ihrem System installieren    |             |
+| I have copied the config                               | Ich habe die Konfiguration kopiert                          |             |
+| Create Site                                            | Website erstellen                                           |             |
+| Close                                                  | Schließen                                                   |             |
+
+## Main “Resources”
+
+##### “Hero” section
+
+| EN                                                           | DE                                                           | Notes |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
+| Resources                                                    | Ressourcen                                                   |       |
+| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. |       |
+| Secure connectivity with WireGuard encryption                | Sichere Verbindung mit WireGuard-Verschlüsselung             |       |
+| Configure multiple authentication methods                    | Konfigurieren Sie mehrere Authentifizierungsmethoden         |       |
+| User and role-based access control                           | Benutzer- und rollenbasierte Zugriffskontrolle               |       |
+##### Content
+
+| EN                                                 | DE                                                         | Notes                |
+| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- |
+| Manage Resources                                   | Ressourcen verwalten                                       |                      |
+| Create secure proxies to your private applications | Erstellen Sie sichere Proxys für Ihre privaten Anwendungen |                      |
+| Search resources                                   | Ressourcen durchsuchen                                     | placeholder          |
+| Name                                               | Name                                                       |                      |
+| Site                                               | Website                                                    |                      |
+| Full URL                                           | Vollständige URL                                           |                      |
+| Authentication                                     | Authentifizierung                                          |                      |
+| Not Protected                                      | Nicht geschützt                                            | authentication state |
+| Protected                                          | Geschützt                                                  | authentication state |
+| Edit →                                             | Bearbeiten →                                               |                      |
+| Add Resource                                       | Ressource hinzufügen                                       |                      |
+##### Add Resource Popup
+
+| EN                                                           | DE                                                           | Notes               |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- |
+| Create Resource                                              | Ressource erstellen                                          |                     |
+| Create a new resource to proxy request to your app           | Erstellen Sie eine neue Ressource, um Anfragen an Ihre App zu proxen |                     |
+| Name                                                         | Name                                                         |                     |
+| My Resource                                                  | Neue Ressource                                               | name placeholder    |
+| This is the name that will be displayed for this resource.   | Dies ist der Name, der für diese Ressource angezeigt wird    |                     |
+| Subdomain                                                    | Subdomain                                                    |                     |
+| Enter subdomain                                              | Subdomain eingeben                                           |                     |
+| This is the fully qualified domain name that will be used to access the resource. | Dies ist der vollständige Domainname, der für den Zugriff auf die Ressource verwendet wird. |                     |
+| Site                                                         | Website                                                      |                     |
+| Search site…                                                 | Website suchen…                                              | Site selector popup |
+| This is the site that will be used in the dashboard.         | Dies ist die Website, die im Dashboard verwendet wird.       |                     |
+| Create Resource                                              | Ressource erstellen                                          |                     |
+| Close                                                        | Schließen                                                    |                     |
+
+
+## Main “User & Roles”
+##### Content
+
+| EN                                                           | DE                                                           | Notes                         |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
+| Manage User & Roles                                          | Benutzer & Rollen verwalten                                  |                               |
+| Invite users and add them to roles to manage access to your organization | Laden Sie Benutzer ein und weisen Sie ihnen Rollen zu, um den Zugriff auf Ihre Organisation zu verwalten |                               |
+| Users                                                        | Benutzer                                                     | sidebar item                  |
+| Roles                                                        | Rollen                                                       | sidebar item                  |
+| **User tab**                                                 |                                                              |                               |
+| Search users                                                 | Benutzer suchen                                              | placeholder                   |
+| Invite User                                                  | Benutzer einladen                                            | addbutton                     |
+| Email                                                        | E-Mail                                                       | table header                  |
+| Status                                                       | Status                                                       | table header                  |
+| Role                                                         | Rolle                                                        | table header                  |
+| Confirmed                                                    | Bestätigt                                                    | account status                |
+| Not confirmed (?)                                            | Nicht bestätigt (?)                                          | unknown for me account status |
+| Owner                                                        | Besitzer                                                     | role                          |
+| Admin                                                        | Administrator                                                | role                          |
+| Member                                                       | Mitglied                                                     | role                          |
+| **Roles Tab**                                                |                                                              |                               |
+| Search roles                                                 | Rollen suchen                                                | placeholder                   |
+| Add Role                                                     | Rolle hinzufügen                                             | addbutton                     |
+| Name                                                         | Name                                                         | table header                  |
+| Description                                                  | Beschreibung                                                 | table header                  |
+| Admin                                                        | Administrator                                                | role                          |
+| Member                                                       | Mitglied                                                     | role                          |
+| Admin role with the most permissions                         | Administratorrolle mit den meisten Berechtigungen            | admin role desc               |
+| Members can only view resources                              | Mitglieder können nur Ressourcen anzeigen                    | member role desc              |
+
+##### Invite User popup
+
+| EN                | DE                                                      | Notes       |
+| ----------------- | ------------------------------------------------------- | ----------- |
+| Invite User       | Geben Sie neuen Benutzern Zugriff auf Ihre Organisation |             |
+| Email             | E-Mail                                                  |             |
+| Enter an email    | E-Mail eingeben                                         | placeholder |
+| Role              | Rolle                                                   |             |
+| Select role       | Rolle auswählen                                         | placeholder |
+| Gültig für        | Gültig bis                                              |             |
+| 1 day             | Tag                                                     |             |
+| 2 days            | 2 Tage                                                  |             |
+| 3 days            | 3 Tage                                                  |             |
+| 4 days            | 4 Tage                                                  |             |
+| 5 days            | 5 Tage                                                  |             |
+| 6 days            | 6 Tage                                                  |             |
+| 7 days            | 7 Tage                                                  |             |
+| Create Invitation | Einladung erstellen                                     |             |
+| Close             | Schließen                                               |             |
+
+
+## Main “Shareable Links”
+##### “Hero” section
+
+| EN                                                           | DE                                                           | Notes |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
+| Shareable Links                                              | Teilbare Links                                               |       |
+| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Erstellen Sie teilbare Links zu Ihren Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Gültigkeitsdauer des Links beim Erstellen konfigurieren. |       |
+| Easy to create and share                                     | Einfach zu erstellen und zu teilen                           |       |
+| Configurable expiration duration                             | Konfigurierbare Gültigkeitsdauer                             |       |
+| Secure and revocable                                         | Sicher und widerrufbar                                       |       |
+##### Content
+
+| EN                                                           | DE                                                           | Notes             |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
+| Manage Shareable Links                                       | Teilbare Links verwalten                                     |                   |
+| Create shareable links to grant temporary or permanent access to your resources | Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren |                   |
+| Search links                                                 | Links suchen                                                 | placeholder       |
+| Create Share Link                                            | Neuen Link erstellen                                         | addbutton         |
+| Resource                                                     | Ressource                                                    | table header      |
+| Title                                                        | Titel                                                        | table header      |
+| Created                                                      | Erstellt                                                     | table header      |
+| Expires                                                      | Gültig bis                                                   | table header      |
+| No links. Create one to get started.                         | Keine Links. Erstellen Sie einen, um zu beginnen.            | table placeholder |
+
+##### Create Shareable Link popup
+
+| EN                                                           | DE                                                           | Notes                   |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- |
+| Create Shareable Link                                        | Teilbaren Link erstellen                                     |                         |
+| Anyone with this link can access the resource                | Jeder mit diesem Link kann auf die Ressource zugreifen       |                         |
+| Resource                                                     | Ressource                                                    |                         |
+| Select resource                                              | Ressource auswählen                                          |                         |
+| Search resources…                                            | Ressourcen suchen…                                           | resource selector popup |
+| Title (optional)                                             | Titel (optional)                                             |                         |
+| Enter title                                                  | Titel eingeben                                               | placeholder             |
+| Expire in                                                    | Gültig bis                                                   |                         |
+| Minutes                                                      | Minuten                                                      |                         |
+| Hours                                                        | Stunden                                                      |                         |
+| Days                                                         | Tage                                                         |                         |
+| Months                                                       | Monate                                                       |                         |
+| Years                                                        | Jahre                                                        |                         |
+| Never expire                                                 | Nie ablaufen                                                 |                         |
+| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Die Gültigkeitsdauer bestimmt, wie lange der Link nutzbar ist und Zugriff auf die Ressource bietet. Nach Ablauf dieser Zeit funktioniert der Link nicht mehr, und Benutzer, die diesen Link verwendet haben, verlieren den Zugriff auf die Ressource. |                         |
+| Create Link                                                  | Link erstellen                                               |                         |
+| Close                                                        | Schließen                                                    |                         |
+
+
+## Main “General”
+
+| EN                                                           | DE                                                           | Notes        |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ |
+| General                                                      | Allgemein                                                    |              |
+| Configure your organization’s general settings               | Konfigurieren Sie die allgemeinen Einstellungen Ihrer Organisation |              |
+| General                                                      | Allgemein                                                    | sidebar item |
+| Organization Settings                                        | Organisationseinstellungen                                   |              |
+| Manage your organization details and configuration           | Verwalten Sie die Details und Konfiguration Ihrer Organisation |              |
+| Name                                                         | Name                                                         |              |
+| This is the display name of the org                          | Dies ist der Anzeigename Ihrer Organisation                  |              |
+| Save Settings                                                | Einstellungen speichern                                      |              |
+| Danger Zone                                                  | Gefahrenzone                                                 |              |
+| Once you delete this org, there is no going back. Please be certain. | Wenn Sie diese Organisation löschen, gibt es kein Zurück. Bitte seien Sie sicher. |              |
+| Delete Organization Data                                     | Organisationsdaten löschen                                   |              |

+ 20 - 0
internationalization/pl.md

@@ -1,3 +1,23 @@
+## Authentication Site
+
+
+| EN                                                       | PL                                                           | Notes      |
+| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- |
+| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Zasilane przez [Pangolin](https://github.com/fosrl/pangolin) |            |
+| Authentication Required                                  | Wymagane uwierzytelnienie                                    |            |
+| Choose your preferred method to access {resource}        | Wybierz preferowaną metodę dostępu do {resource}             |            |
+| PIN                                                      | PIN                                                          |            |
+| User                                                     | Zaloguj                                                      |            |
+| 6-digit PIN Code                                         | 6-cyfrowy kod PIN                                            | pin login  |
+| Login in with PIN                                        | Zaloguj się PIN’em                                           | pin login  |
+| Email                                                    | Email                                                        | user login |
+| Enter your email                                         | Wprowadź swój email                                          | user login |
+| Password                                                 | Hasło                                                        | user login |
+| Enter your password                                      | Wprowadź swoje hasło                                         | user login |
+| Forgot your password?                                    | Zapomniałeś hasła?                                           | user login |
+| Log in                                                   | Zaloguj                                                      | user login |
+
+
 ## Login site
 ## Login site
 
 
 | EN                    | PL                             | Notes       |
 | EN                    | PL                             | Notes       |

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@fosrl/pangolin",
     "name": "@fosrl/pangolin",
-    "version": "1.0.0-beta.10",
+    "version": "0.0.0",
     "private": true,
     "private": true,
     "type": "module",
     "type": "module",
     "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
     "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",

+ 2 - 1
server/db/schema.ts

@@ -53,7 +53,8 @@ export const resources = sqliteTable("resources", {
     proxyPort: integer("proxyPort"),
     proxyPort: integer("proxyPort"),
     emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
     emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
         .notNull()
         .notNull()
-        .default(false)
+        .default(false),
+    isBaseDomain: integer("isBaseDomain", { mode: "boolean" })
 });
 });
 
 
 export const targets = sqliteTable("targets", {
 export const targets = sqliteTable("targets", {

+ 12 - 10
server/lib/config.ts

@@ -6,10 +6,10 @@ import { fromError } from "zod-validation-error";
 import {
 import {
     __DIRNAME,
     __DIRNAME,
     APP_PATH,
     APP_PATH,
+    APP_VERSION,
     configFilePath1,
     configFilePath1,
     configFilePath2
     configFilePath2
 } from "@server/lib/consts";
 } from "@server/lib/consts";
-import { loadAppVersion } from "@server/lib/loadAppVersion";
 import { passwordSchema } from "@server/auth/passwordSchema";
 import { passwordSchema } from "@server/auth/passwordSchema";
 import stoi from "./stoi";
 import stoi from "./stoi";
 
 
@@ -151,7 +151,8 @@ const configSchema = z.object({
             require_email_verification: z.boolean().optional(),
             require_email_verification: z.boolean().optional(),
             disable_signup_without_invite: z.boolean().optional(),
             disable_signup_without_invite: z.boolean().optional(),
             disable_user_create_org: z.boolean().optional(),
             disable_user_create_org: z.boolean().optional(),
-            allow_raw_resources: z.boolean().optional()
+            allow_raw_resources: z.boolean().optional(),
+            allow_base_domain_resources: z.boolean().optional()
         })
         })
         .optional()
         .optional()
 });
 });
@@ -239,11 +240,7 @@ export class Config {
             throw new Error(`Invalid configuration file: ${errors}`);
             throw new Error(`Invalid configuration file: ${errors}`);
         }
         }
 
 
-        const appVersion = loadAppVersion();
-        if (!appVersion) {
-            throw new Error("Could not load the application version");
-        }
-        process.env.APP_VERSION = appVersion;
+        process.env.APP_VERSION = APP_VERSION;
 
 
         process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
         process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
         process.env.SERVER_EXTERNAL_PORT =
         process.env.SERVER_EXTERNAL_PORT =
@@ -255,9 +252,9 @@ export class Config {
             ? "true"
             ? "true"
             : "false";
             : "false";
         process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
         process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
-        ?.allow_raw_resources
-        ? "true"
-        : "false";
+            ?.allow_raw_resources
+            ? "true"
+            : "false";
         process.env.SESSION_COOKIE_NAME =
         process.env.SESSION_COOKIE_NAME =
             parsedConfig.data.server.session_cookie_name;
             parsedConfig.data.server.session_cookie_name;
         process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
         process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
@@ -273,6 +270,11 @@ export class Config {
             parsedConfig.data.server.resource_access_token_param;
             parsedConfig.data.server.resource_access_token_param;
         process.env.RESOURCE_SESSION_REQUEST_PARAM =
         process.env.RESOURCE_SESSION_REQUEST_PARAM =
             parsedConfig.data.server.resource_session_request_param;
             parsedConfig.data.server.resource_session_request_param;
+        process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
+            ?.allow_base_domain_resources
+            ? "true"
+            : "false";
+        process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
 
 
         this.rawConfig = parsedConfig.data;
         this.rawConfig = parsedConfig.data;
     }
     }

+ 3 - 1
server/lib/consts.ts

@@ -1,6 +1,8 @@
 import path from "path";
 import path from "path";
 import { fileURLToPath } from "url";
 import { fileURLToPath } from "url";
-import { existsSync } from "fs";
+
+// This is a placeholder value replaced by the build process
+export const APP_VERSION = "1.0.0-beta.12";
 
 
 export const __FILENAME = fileURLToPath(import.meta.url);
 export const __FILENAME = fileURLToPath(import.meta.url);
 export const __DIRNAME = path.dirname(__FILENAME);
 export const __DIRNAME = path.dirname(__FILENAME);

+ 0 - 16
server/lib/loadAppVersion.ts

@@ -1,16 +0,0 @@
-import path from "path";
-import { __DIRNAME } from "@server/lib/consts";
-import fs from "fs";
-
-export function loadAppVersion() {
-    const packageJsonPath = path.join("package.json");
-    let packageJson: any;
-    if (fs.existsSync && fs.existsSync(packageJsonPath)) {
-        const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
-        packageJson = JSON.parse(packageJsonContent);
-
-        if (packageJson.version) {
-            return packageJson.version;
-        }
-    }
-}

+ 7 - 0
server/routers/external.ts

@@ -308,6 +308,13 @@ authenticated.get(
     resource.getResourceWhitelist
     resource.getResourceWhitelist
 );
 );
 
 
+authenticated.post(
+    `/resource/:resourceId/transfer`,
+    verifyResourceAccess,
+    verifyUserHasAction(ActionsEnum.updateResource),
+    resource.transferResource
+);
+
 authenticated.post(
 authenticated.post(
     `/resource/:resourceId/access-token`,
     `/resource/:resourceId/access-token`,
     verifyResourceAccess,
     verifyResourceAccess,

+ 2 - 14
server/routers/gerbil/getConfig.ts

@@ -11,6 +11,7 @@ import config from "@server/lib/config";
 import { getUniqueExitNodeEndpointName } from '@server/db/names';
 import { getUniqueExitNodeEndpointName } from '@server/db/names';
 import { findNextAvailableCidr } from "@server/lib/ip";
 import { findNextAvailableCidr } from "@server/lib/ip";
 import { fromError } from 'zod-validation-error';
 import { fromError } from 'zod-validation-error';
+import { getAllowedIps } from '../target/helpers';
 // Define Zod schema for request validation
 // Define Zod schema for request validation
 const getConfigSchema = z.object({
 const getConfigSchema = z.object({
     publicKey: z.string(),
     publicKey: z.string(),
@@ -83,22 +84,9 @@ export async function getConfig(req: Request, res: Response, next: NextFunction)
         });
         });
 
 
         const peers = await Promise.all(sitesRes.map(async (site) => {
         const peers = await Promise.all(sitesRes.map(async (site) => {
-            // Fetch resources for this site
-            const resourcesRes = await db.query.resources.findMany({
-                where: eq(resources.siteId, site.siteId),
-            });
-
-            // Fetch targets for all resources of this site
-            const targetIps = await Promise.all(resourcesRes.map(async (resource) => {
-                const targetsRes = await db.query.targets.findMany({
-                    where: eq(targets.resourceId, resource.resourceId),
-                });
-                return targetsRes.map(target => `${target.ip}/32`);
-            }));
-
             return {
             return {
                 publicKey: site.pubKey,
                 publicKey: site.pubKey,
-                allowedIps: targetIps.flat(),
+                allowedIps: await getAllowedIps(site.siteId)
             };
             };
         }));
         }));
 
 

+ 4 - 4
server/routers/newt/targets.ts

@@ -1,11 +1,11 @@
 import { Target } from "@server/db/schema";
 import { Target } from "@server/db/schema";
 import { sendToClient } from "../ws";
 import { sendToClient } from "../ws";
 
 
-export async function addTargets(
+export function addTargets(
     newtId: string,
     newtId: string,
     targets: Target[],
     targets: Target[],
     protocol: string
     protocol: string
-): Promise<void> {
+) {
     //create a list of udp and tcp targets
     //create a list of udp and tcp targets
     const payloadTargets = targets.map((target) => {
     const payloadTargets = targets.map((target) => {
         return `${target.internalPort ? target.internalPort + ":" : ""}${
         return `${target.internalPort ? target.internalPort + ":" : ""}${
@@ -22,11 +22,11 @@ export async function addTargets(
     sendToClient(newtId, payload);
     sendToClient(newtId, payload);
 }
 }
 
 
-export async function removeTargets(
+export function removeTargets(
     newtId: string,
     newtId: string,
     targets: Target[],
     targets: Target[],
     protocol: string
     protocol: string
-): Promise<void> {
+) {
     //create a list of udp and tcp targets
     //create a list of udp and tcp targets
     const payloadTargets = targets.map((target) => {
     const payloadTargets = targets.map((target) => {
         return `${target.internalPort ? target.internalPort + ":" : ""}${
         return `${target.internalPort ? target.internalPort + ":" : ""}${

+ 51 - 14
server/routers/resource/createResource.ts

@@ -18,6 +18,7 @@ import stoi from "@server/lib/stoi";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
+import config from "@server/lib/config";
 
 
 const createResourceParamsSchema = z
 const createResourceParamsSchema = z
     .object({
     .object({
@@ -33,7 +34,8 @@ const createResourceSchema = z
         siteId: z.number(),
         siteId: z.number(),
         http: z.boolean(),
         http: z.boolean(),
         protocol: z.string(),
         protocol: z.string(),
-        proxyPort: z.number().optional()
+        proxyPort: z.number().optional(),
+        isBaseDomain: z.boolean().optional()
     })
     })
     .refine(
     .refine(
         (data) => {
         (data) => {
@@ -54,7 +56,7 @@ const createResourceSchema = z
     )
     )
     .refine(
     .refine(
         (data) => {
         (data) => {
-            if (data.http) {
+            if (data.http && !data.isBaseDomain) {
                 return subdomainSchema.safeParse(data.subdomain).success;
                 return subdomainSchema.safeParse(data.subdomain).success;
             }
             }
             return true;
             return true;
@@ -63,6 +65,43 @@ const createResourceSchema = z
             message: "Invalid subdomain",
             message: "Invalid subdomain",
             path: ["subdomain"]
             path: ["subdomain"]
         }
         }
+    )
+    .refine(
+        (data) => {
+            if (!config.getRawConfig().flags?.allow_raw_resources) {
+                if (data.proxyPort !== undefined) {
+                    return false;
+                }
+            }
+            return true;
+        },
+        {
+            message: "Proxy port cannot be set"
+        }
+    )
+    // .refine(
+    //     (data) => {
+    //         if (data.proxyPort === 443 || data.proxyPort === 80) {
+    //             return false;
+    //         }
+    //         return true;
+    //     },
+    //     {
+    //         message: "Port 80 and 443 are reserved for http and https resources"
+    //     }
+    // )
+    .refine(
+        (data) => {
+            if (!config.getRawConfig().flags?.allow_base_domain_resources) {
+                if (data.isBaseDomain) {
+                    return false;
+                }
+            }
+            return true;
+        },
+        {
+            message: "Base domain resources are not allowed"
+        }
     );
     );
 
 
 export type CreateResourceResponse = Resource;
 export type CreateResourceResponse = Resource;
@@ -83,7 +122,7 @@ export async function createResource(
             );
             );
         }
         }
 
 
-        let { name, subdomain, protocol, proxyPort, http } = parsedBody.data;
+        let { name, subdomain, protocol, proxyPort, http, isBaseDomain } = parsedBody.data;
 
 
         // Validate request params
         // Validate request params
         const parsedParams = createResourceParamsSchema.safeParse(req.params);
         const parsedParams = createResourceParamsSchema.safeParse(req.params);
@@ -120,7 +159,13 @@ export async function createResource(
             );
             );
         }
         }
 
 
-        const fullDomain = `${subdomain}.${org[0].domain}`;
+        let fullDomain = "";
+        if (isBaseDomain) {
+            fullDomain = org[0].domain;
+        } else {
+            fullDomain = `${subdomain}.${org[0].domain}`;
+        }
+
         // if http is false check to see if there is already a resource with the same port and protocol
         // if http is false check to see if there is already a resource with the same port and protocol
         if (!http) {
         if (!http) {
             const existingResource = await db
             const existingResource = await db
@@ -142,15 +187,6 @@ export async function createResource(
                 );
                 );
             }
             }
         } else {
         } else {
-            if (proxyPort === 443 || proxyPort === 80) {
-                return next(
-                    createHttpError(
-                        HttpCode.BAD_REQUEST,
-                        "Port 80 and 443 are reserved for https resources"
-                    )
-                );
-            }
-
             // make sure the full domain is unique
             // make sure the full domain is unique
             const existingResource = await db
             const existingResource = await db
                 .select()
                 .select()
@@ -179,7 +215,8 @@ export async function createResource(
                     http,
                     http,
                     protocol,
                     protocol,
                     proxyPort,
                     proxyPort,
-                    ssl: true
+                    ssl: true,
+                    isBaseDomain
                 })
                 })
                 .returning();
                 .returning();
 
 

+ 2 - 17
server/routers/resource/deleteResource.ts

@@ -10,6 +10,7 @@ import logger from "@server/logger";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import { addPeer } from "../gerbil/peers";
 import { addPeer } from "../gerbil/peers";
 import { removeTargets } from "../newt/targets";
 import { removeTargets } from "../newt/targets";
+import { getAllowedIps } from "../target/helpers";
 
 
 // Define Zod schema for request parameters validation
 // Define Zod schema for request parameters validation
 const deleteResourceSchema = z
 const deleteResourceSchema = z
@@ -75,25 +76,9 @@ export async function deleteResource(
 
 
         if (site.pubKey) {
         if (site.pubKey) {
             if (site.type == "wireguard") {
             if (site.type == "wireguard") {
-                // TODO: is this all inefficient?
-                // Fetch resources for this site
-                const resourcesRes = await db.query.resources.findMany({
-                    where: eq(resources.siteId, site.siteId)
-                });
-
-                // Fetch targets for all resources of this site
-                const targetIps = await Promise.all(
-                    resourcesRes.map(async (resource) => {
-                        const targetsRes = await db.query.targets.findMany({
-                            where: eq(targets.resourceId, resource.resourceId)
-                        });
-                        return targetsRes.map((target) => `${target.ip}/32`);
-                    })
-                );
-
                 await addPeer(site.exitNodeId!, {
                 await addPeer(site.exitNodeId!, {
                     publicKey: site.pubKey,
                     publicKey: site.pubKey,
-                    allowedIps: targetIps.flat()
+                    allowedIps: await getAllowedIps(site.siteId)
                 });
                 });
             } else if (site.type == "newt") {
             } else if (site.type == "newt") {
                 // get the newt on the site by querying the newt table for siteId
                 // get the newt on the site by querying the newt table for siteId

+ 1 - 0
server/routers/resource/index.ts

@@ -16,4 +16,5 @@ export * from "./setResourceWhitelist";
 export * from "./getResourceWhitelist";
 export * from "./getResourceWhitelist";
 export * from "./authWithWhitelist";
 export * from "./authWithWhitelist";
 export * from "./authWithAccessToken";
 export * from "./authWithAccessToken";
+export * from "./transferResource";
 export * from "./getExchangeToken";
 export * from "./getExchangeToken";

+ 192 - 0
server/routers/resource/transferResource.ts

@@ -0,0 +1,192 @@
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { newts, resources, sites, targets } from "@server/db/schema";
+import { eq } from "drizzle-orm";
+import response from "@server/lib/response";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { addPeer } from "../gerbil/peers";
+import { addTargets, removeTargets } from "../newt/targets";
+import { getAllowedIps } from "../target/helpers";
+
+const transferResourceParamsSchema = z
+    .object({
+        resourceId: z
+            .string()
+            .transform(Number)
+            .pipe(z.number().int().positive())
+    })
+    .strict();
+
+const transferResourceBodySchema = z
+    .object({
+        siteId: z.number().int().positive()
+    })
+    .strict();
+
+export async function transferResource(
+    req: Request,
+    res: Response,
+    next: NextFunction
+): Promise<any> {
+    try {
+        const parsedParams = transferResourceParamsSchema.safeParse(req.params);
+        if (!parsedParams.success) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    fromError(parsedParams.error).toString()
+                )
+            );
+        }
+
+        const parsedBody = transferResourceBodySchema.safeParse(req.body);
+        if (!parsedBody.success) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    fromError(parsedBody.error).toString()
+                )
+            );
+        }
+
+        const { resourceId } = parsedParams.data;
+        const { siteId } = parsedBody.data;
+
+        const [oldResource] = await db
+            .select()
+            .from(resources)
+            .where(eq(resources.resourceId, resourceId))
+            .limit(1);
+
+        if (!oldResource) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `Resource with ID ${resourceId} not found`
+                )
+            );
+        }
+
+        if (oldResource.siteId === siteId) {
+            return next(
+                createHttpError(
+                    HttpCode.BAD_REQUEST,
+                    `Resource is already assigned to this site`
+                )
+            );
+        }
+
+        const [newSite] = await db
+            .select()
+            .from(sites)
+            .where(eq(sites.siteId, siteId))
+            .limit(1);
+
+        if (!newSite) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `Site with ID ${siteId} not found`
+                )
+            );
+        }
+
+        const [oldSite] = await db
+            .select()
+            .from(sites)
+            .where(eq(sites.siteId, oldResource.siteId))
+            .limit(1);
+
+        if (!oldSite) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `Site with ID ${oldResource.siteId} not found`
+                )
+            );
+        }
+
+        const [updatedResource] = await db
+            .update(resources)
+            .set({ siteId })
+            .where(eq(resources.resourceId, resourceId))
+            .returning();
+
+        if (!updatedResource) {
+            return next(
+                createHttpError(
+                    HttpCode.NOT_FOUND,
+                    `Resource with ID ${resourceId} not found`
+                )
+            );
+        }
+
+        const resourceTargets = await db
+            .select()
+            .from(targets)
+            .where(eq(targets.resourceId, resourceId));
+
+        if (resourceTargets.length > 0) {
+            ////// REMOVE THE TARGETS FROM THE OLD SITE //////
+            if (oldSite.pubKey) {
+                if (oldSite.type == "wireguard") {
+                    await addPeer(oldSite.exitNodeId!, {
+                        publicKey: oldSite.pubKey,
+                        allowedIps: await getAllowedIps(oldSite.siteId)
+                    });
+                } else if (oldSite.type == "newt") {
+                    const [newt] = await db
+                        .select()
+                        .from(newts)
+                        .where(eq(newts.siteId, oldSite.siteId))
+                        .limit(1);
+
+                    removeTargets(
+                        newt.newtId,
+                        resourceTargets,
+                        updatedResource.protocol
+                    );
+                }
+            }
+
+            ////// ADD THE TARGETS TO THE NEW SITE //////
+            if (newSite.pubKey) {
+                if (newSite.type == "wireguard") {
+                    await addPeer(newSite.exitNodeId!, {
+                        publicKey: newSite.pubKey,
+                        allowedIps: await getAllowedIps(newSite.siteId)
+                    });
+                } else if (newSite.type == "newt") {
+                    const [newt] = await db
+                        .select()
+                        .from(newts)
+                        .where(eq(newts.siteId, newSite.siteId))
+                        .limit(1);
+
+                    addTargets(
+                        newt.newtId,
+                        resourceTargets,
+                        updatedResource.protocol
+                    );
+                }
+            }
+        }
+
+        return response(res, {
+            data: updatedResource,
+            success: true,
+            error: false,
+            message: "Resource transferred successfully",
+            status: HttpCode.OK
+        });
+    } catch (error) {
+        logger.error(error);
+        return next(
+            createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+        );
+    }
+}

+ 114 - 13
server/routers/resource/updateResource.ts

@@ -2,13 +2,14 @@ import { Request, Response, NextFunction } from "express";
 import { z } from "zod";
 import { z } from "zod";
 import { db } from "@server/db";
 import { db } from "@server/db";
 import { orgs, resources, sites } from "@server/db/schema";
 import { orgs, resources, sites } from "@server/db/schema";
-import { eq, or } from "drizzle-orm";
+import { eq, or, and } from "drizzle-orm";
 import response from "@server/lib/response";
 import response from "@server/lib/response";
 import HttpCode from "@server/types/HttpCode";
 import HttpCode from "@server/types/HttpCode";
 import createHttpError from "http-errors";
 import createHttpError from "http-errors";
 import logger from "@server/logger";
 import logger from "@server/logger";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
+import config from "@server/lib/config";
 
 
 const updateResourceParamsSchema = z
 const updateResourceParamsSchema = z
     .object({
     .object({
@@ -27,12 +28,48 @@ const updateResourceBodySchema = z
         sso: z.boolean().optional(),
         sso: z.boolean().optional(),
         blockAccess: z.boolean().optional(),
         blockAccess: z.boolean().optional(),
         proxyPort: z.number().int().min(1).max(65535).optional(),
         proxyPort: z.number().int().min(1).max(65535).optional(),
-        emailWhitelistEnabled: z.boolean().optional()
+        emailWhitelistEnabled: z.boolean().optional(),
+        isBaseDomain: z.boolean().optional()
     })
     })
     .strict()
     .strict()
     .refine((data) => Object.keys(data).length > 0, {
     .refine((data) => Object.keys(data).length > 0, {
         message: "At least one field must be provided for update"
         message: "At least one field must be provided for update"
-    });
+    })
+    .refine(
+        (data) => {
+            if (!config.getRawConfig().flags?.allow_raw_resources) {
+                if (data.proxyPort !== undefined) {
+                    return false;
+                }
+            }
+            return true;
+        },
+        { message: "Cannot update proxyPort" }
+    )
+    // .refine(
+    //     (data) => {
+    //         if (data.proxyPort === 443 || data.proxyPort === 80) {
+    //             return false;
+    //         }
+    //         return true;
+    //     },
+    //     {
+    //         message: "Port 80 and 443 are reserved for http and https resources"
+    //     }
+    // )
+    .refine(
+        (data) => {
+            if (!config.getRawConfig().flags?.allow_base_domain_resources) {
+                if (data.isBaseDomain) {
+                    return false;
+                }
+            }
+            return true;
+        },
+        {
+            message: "Base domain resources are not allowed"
+        }
+    );
 
 
 export async function updateResource(
 export async function updateResource(
     req: Request,
     req: Request,
@@ -63,13 +100,16 @@ export async function updateResource(
         const { resourceId } = parsedParams.data;
         const { resourceId } = parsedParams.data;
         const updateData = parsedBody.data;
         const updateData = parsedBody.data;
 
 
-        const resource = await db
+        const [result] = await db
             .select()
             .select()
             .from(resources)
             .from(resources)
             .where(eq(resources.resourceId, resourceId))
             .where(eq(resources.resourceId, resourceId))
             .leftJoin(orgs, eq(resources.orgId, orgs.orgId));
             .leftJoin(orgs, eq(resources.orgId, orgs.orgId));
 
 
-        if (resource.length === 0) {
+        const resource = result.resources;
+        const org = result.orgs;
+
+        if (!resource || !org) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.NOT_FOUND,
                     HttpCode.NOT_FOUND,
@@ -78,7 +118,55 @@ export async function updateResource(
             );
             );
         }
         }
 
 
-        if (!resource[0].orgs?.domain) {
+        if (updateData.subdomain) {
+            if (!resource.http) {
+                return next(
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        "Cannot update subdomain for non-http resource"
+                    )
+                );
+            }
+
+            const valid = subdomainSchema.safeParse(
+                updateData.subdomain
+            ).success;
+            if (!valid) {
+                return next(
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        "Invalid subdomain provided"
+                    )
+                );
+            }
+        }
+
+        if (updateData.proxyPort) {
+            const proxyPort = updateData.proxyPort;
+            const existingResource = await db
+                .select()
+                .from(resources)
+                .where(
+                    and(
+                        eq(resources.protocol, resource.protocol),
+                        eq(resources.proxyPort, proxyPort!)
+                    )
+                );
+
+            if (
+                existingResource.length > 0 &&
+                existingResource[0].resourceId !== resourceId
+            ) {
+                return next(
+                    createHttpError(
+                        HttpCode.CONFLICT,
+                        "Resource with that protocol and port already exists"
+                    )
+                );
+            }
+        }
+
+        if (!org?.domain) {
             return next(
             return next(
                 createHttpError(
                 createHttpError(
                     HttpCode.BAD_REQUEST,
                     HttpCode.BAD_REQUEST,
@@ -87,15 +175,32 @@ export async function updateResource(
             );
             );
         }
         }
 
 
-        const fullDomain = updateData.subdomain
-            ? `${updateData.subdomain}.${resource[0].orgs.domain}`
-            : undefined;
+        let fullDomain = "";
+        if (updateData.isBaseDomain) {
+            fullDomain = org.domain;
+        } else {
+            fullDomain = `${updateData.subdomain}.${org.domain}`;
+        }
 
 
         const updatePayload = {
         const updatePayload = {
             ...updateData,
             ...updateData,
             ...(fullDomain && { fullDomain })
             ...(fullDomain && { fullDomain })
         };
         };
 
 
+        const [existingDomain] = await db
+            .select()
+            .from(resources)
+            .where(eq(resources.fullDomain, fullDomain));
+
+        if (existingDomain && existingDomain.resourceId !== resourceId) {
+            return next(
+                createHttpError(
+                    HttpCode.CONFLICT,
+                    "Resource with that domain already exists"
+                )
+            );
+        }
+
         const updatedResource = await db
         const updatedResource = await db
             .update(resources)
             .update(resources)
             .set(updatePayload)
             .set(updatePayload)
@@ -111,10 +216,6 @@ export async function updateResource(
             );
             );
         }
         }
 
 
-        if (resource[0].resources.ssl !== updatedResource[0].ssl) {
-            // invalidate all sessions?
-        }
-
         return response(res, {
         return response(res, {
             data: updatedResource[0],
             data: updatedResource[0],
             success: true,
             success: true,

+ 1 - 1
server/routers/target/createTarget.ts

@@ -11,7 +11,7 @@ import { isIpInCidr } from "@server/lib/ip";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import { addTargets } from "../newt/targets";
 import { addTargets } from "../newt/targets";
 import { eq } from "drizzle-orm";
 import { eq } from "drizzle-orm";
-import { pickPort } from "./ports";
+import { pickPort } from "./helpers";
 
 
 // Regular expressions for validation
 // Regular expressions for validation
 const DOMAIN_REGEX =
 const DOMAIN_REGEX =

+ 2 - 17
server/routers/target/deleteTarget.ts

@@ -10,6 +10,7 @@ import logger from "@server/logger";
 import { addPeer } from "../gerbil/peers";
 import { addPeer } from "../gerbil/peers";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import { removeTargets } from "../newt/targets";
 import { removeTargets } from "../newt/targets";
+import { getAllowedIps } from "./helpers";
 
 
 const deleteTargetSchema = z
 const deleteTargetSchema = z
     .object({
     .object({
@@ -80,25 +81,9 @@ export async function deleteTarget(
 
 
         if (site.pubKey) {
         if (site.pubKey) {
             if (site.type == "wireguard") {
             if (site.type == "wireguard") {
-                // TODO: is this all inefficient?
-                // Fetch resources for this site
-                const resourcesRes = await db.query.resources.findMany({
-                    where: eq(resources.siteId, site.siteId)
-                });
-
-                // Fetch targets for all resources of this site
-                const targetIps = await Promise.all(
-                    resourcesRes.map(async (resource) => {
-                        const targetsRes = await db.query.targets.findMany({
-                            where: eq(targets.resourceId, resource.resourceId)
-                        });
-                        return targetsRes.map((target) => `${target.ip}/32`);
-                    })
-                );
-
                 await addPeer(site.exitNodeId!, {
                 await addPeer(site.exitNodeId!, {
                     publicKey: site.pubKey,
                     publicKey: site.pubKey,
-                    allowedIps: targetIps.flat()
+                    allowedIps: await getAllowedIps(site.siteId)
                 });
                 });
             } else if (site.type == "newt") {
             } else if (site.type == "newt") {
                 // get the newt on the site by querying the newt table for siteId
                 // get the newt on the site by querying the newt table for siteId

+ 18 - 0
server/routers/target/ports.ts → server/routers/target/helpers.ts

@@ -46,3 +46,21 @@ export async function pickPort(siteId: number): Promise<{
 
 
     return { internalPort, targetIps };
     return { internalPort, targetIps };
 }
 }
+
+export async function getAllowedIps(siteId: number) {
+    // TODO: is this all inefficient?
+    const resourcesRes = await db.query.resources.findMany({
+        where: eq(resources.siteId, siteId)
+    });
+
+    // Fetch targets for all resources of this site
+    const targetIps = await Promise.all(
+        resourcesRes.map(async (resource) => {
+            const targetsRes = await db.query.targets.findMany({
+                where: eq(targets.resourceId, resource.resourceId)
+            });
+            return targetsRes.map((target) => `${target.ip}/32`);
+        })
+    );
+    return targetIps.flat();
+}

+ 1 - 1
server/routers/target/updateTarget.ts

@@ -10,7 +10,7 @@ import logger from "@server/logger";
 import { fromError } from "zod-validation-error";
 import { fromError } from "zod-validation-error";
 import { addPeer } from "../gerbil/peers";
 import { addPeer } from "../gerbil/peers";
 import { addTargets } from "../newt/targets";
 import { addTargets } from "../newt/targets";
-import { pickPort } from "./ports";
+import { pickPort } from "./helpers";
 
 
 // Regular expressions for validation
 // Regular expressions for validation
 const DOMAIN_REGEX =
 const DOMAIN_REGEX =

+ 5 - 2
server/routers/traefik/getTraefikConfig.ts

@@ -25,6 +25,7 @@ export async function traefikConfigProvider(
                 http: resources.http,
                 http: resources.http,
                 proxyPort: resources.proxyPort,
                 proxyPort: resources.proxyPort,
                 protocol: resources.protocol,
                 protocol: resources.protocol,
+                isBaseDomain: resources.isBaseDomain,
                 // Site fields
                 // Site fields
                 site: {
                 site: {
                     siteId: sites.siteId,
                     siteId: sites.siteId,
@@ -110,11 +111,11 @@ export async function traefikConfigProvider(
 
 
             const routerName = `${resource.resourceId}-router`;
             const routerName = `${resource.resourceId}-router`;
             const serviceName = `${resource.resourceId}-service`;
             const serviceName = `${resource.resourceId}-service`;
-            const fullDomain = `${resource.subdomain}.${org.domain}`;
+            const fullDomain = `${resource.fullDomain}`;
 
 
             if (resource.http) {
             if (resource.http) {
                 // HTTP configuration remains the same
                 // HTTP configuration remains the same
-                if (!resource.subdomain) {
+                if (!resource.subdomain && !resource.isBaseDomain) {
                     continue;
                     continue;
                 }
                 }
 
 
@@ -148,6 +149,8 @@ export async function traefikConfigProvider(
                         : {})
                         : {})
                 };
                 };
 
 
+                logger.debug(config.getRawConfig().traefik.prefer_wildcard_cert)
+
                 const additionalMiddlewares =
                 const additionalMiddlewares =
                     config.getRawConfig().traefik.additional_middlewares || [];
                     config.getRawConfig().traefik.additional_middlewares || [];
 
 

+ 6 - 1
server/setup/copyInConfig.ts

@@ -23,7 +23,12 @@ export async function copyInConfig() {
         const allResources = await trx.select().from(resources);
         const allResources = await trx.select().from(resources);
 
 
         for (const resource of allResources) {
         for (const resource of allResources) {
-            const fullDomain = `${resource.subdomain}.${domain}`;
+            let fullDomain = "";
+            if (resource.isBaseDomain) {
+                fullDomain = domain;
+            } else {
+                fullDomain = `${resource.subdomain}.${domain}`;
+            }
             await trx
             await trx
                 .update(resources)
                 .update(resources)
                 .set({ fullDomain })
                 .set({ fullDomain })

+ 40 - 10
server/setup/migrations.ts

@@ -3,9 +3,9 @@ import db, { exists } from "@server/db";
 import path from "path";
 import path from "path";
 import semver from "semver";
 import semver from "semver";
 import { versionMigrations } from "@server/db/schema";
 import { versionMigrations } from "@server/db/schema";
-import { __DIRNAME } from "@server/lib/consts";
-import { loadAppVersion } from "@server/lib/loadAppVersion";
+import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
 import { SqliteError } from "better-sqlite3";
 import { SqliteError } from "better-sqlite3";
+import fs from "fs";
 import m1 from "./scripts/1.0.0-beta1";
 import m1 from "./scripts/1.0.0-beta1";
 import m2 from "./scripts/1.0.0-beta2";
 import m2 from "./scripts/1.0.0-beta2";
 import m3 from "./scripts/1.0.0-beta3";
 import m3 from "./scripts/1.0.0-beta3";
@@ -13,6 +13,7 @@ import m4 from "./scripts/1.0.0-beta5";
 import m5 from "./scripts/1.0.0-beta6";
 import m5 from "./scripts/1.0.0-beta6";
 import m6 from "./scripts/1.0.0-beta9";
 import m6 from "./scripts/1.0.0-beta9";
 import m7 from "./scripts/1.0.0-beta10";
 import m7 from "./scripts/1.0.0-beta10";
+import m8 from "./scripts/1.0.0-beta12";
 
 
 // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
 // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
 // EXCEPT FOR THE DATABASE AND THE SCHEMA
 // EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -25,19 +26,45 @@ const migrations = [
     { version: "1.0.0-beta.5", run: m4 },
     { version: "1.0.0-beta.5", run: m4 },
     { version: "1.0.0-beta.6", run: m5 },
     { version: "1.0.0-beta.6", run: m5 },
     { version: "1.0.0-beta.9", run: m6 },
     { version: "1.0.0-beta.9", run: m6 },
-    { version: "1.0.0-beta.10", run: m7 }
+    { version: "1.0.0-beta.10", run: m7 },
+    { version: "1.0.0-beta.12", run: m8 }
     // Add new migrations here as they are created
     // Add new migrations here as they are created
 ] as const;
 ] as const;
 
 
-// Run the migrations
-await runMigrations();
+await run();
+
+async function run() {
+    // backup the database
+    backupDb();
+
+    // run the migrations
+    await runMigrations();
+}
+
+function backupDb() {
+    // make dir config/db/backups
+    const appPath = APP_PATH;
+    const dbDir = path.join(appPath, "db");
+
+    const backupsDir = path.join(dbDir, "backups");
+
+    // check if the backups directory exists and create it if it doesn't
+    if (!fs.existsSync(backupsDir)) {
+        fs.mkdirSync(backupsDir, { recursive: true });
+    }
+
+    // copy the db.sqlite file to backups
+    // add the date to the filename
+    const date = new Date();
+    const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
+    const dbPath = path.join(dbDir, "db.sqlite");
+    const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
+    fs.copyFileSync(dbPath, backupPath);
+}
 
 
 export async function runMigrations() {
 export async function runMigrations() {
     try {
     try {
-        const appVersion = loadAppVersion();
-        if (!appVersion) {
-            throw new Error("APP_VERSION is not set in the environment");
-        }
+        const appVersion = APP_VERSION;
 
 
         if (exists) {
         if (exists) {
             await executeScripts();
             await executeScripts();
@@ -109,7 +136,10 @@ async function executeScripts() {
                     `Successfully completed migration ${migration.version}`
                     `Successfully completed migration ${migration.version}`
                 );
                 );
             } catch (e) {
             } catch (e) {
-                if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
+                if (
+                    e instanceof SqliteError &&
+                    e.code === "SQLITE_CONSTRAINT_UNIQUE"
+                ) {
                     console.error("Migration has already run! Skipping...");
                     console.error("Migration has already run! Skipping...");
                     continue;
                     continue;
                 }
                 }

+ 62 - 0
server/setup/scripts/1.0.0-beta12.ts

@@ -0,0 +1,62 @@
+import db from "@server/db";
+import { configFilePath1, configFilePath2 } from "@server/lib/consts";
+import { sql } from "drizzle-orm";
+import fs from "fs";
+import yaml from "js-yaml";
+
+export default async function migration() {
+    console.log("Running setup script 1.0.0-beta.12...");
+
+    try {
+        // Determine which config file exists
+        const filePaths = [configFilePath1, configFilePath2];
+        let filePath = "";
+        for (const path of filePaths) {
+            if (fs.existsSync(path)) {
+                filePath = path;
+                break;
+            }
+        }
+
+        if (!filePath) {
+            throw new Error(
+                `No config file found (expected config.yml or config.yaml).`
+            );
+        }
+
+        // Read and parse the YAML file
+        let rawConfig: any;
+        const fileContents = fs.readFileSync(filePath, "utf8");
+        rawConfig = yaml.load(fileContents);
+
+        if (!rawConfig.flags) {
+            rawConfig.flags = {};
+        }
+
+        rawConfig.flags.allow_base_domain_resources = true;
+
+        // Write the updated YAML back to the file
+        const updatedYaml = yaml.dump(rawConfig);
+        fs.writeFileSync(filePath, updatedYaml, "utf8");
+
+        console.log(`Added new config option: allow_base_domain_resources`);
+    } catch (e) {
+        console.log(
+            `Unable to add new config option: allow_base_domain_resources. This is not critical.`
+        );
+        console.error(e);
+    }
+
+    try {
+        db.transaction((trx) => {
+            trx.run(sql`ALTER TABLE 'resources' ADD 'isBaseDomain' integer;`);
+        });
+
+        console.log(`Added new column: isBaseDomain`);
+    } catch (e) {
+        console.log("Unable to add new column: isBaseDomain");
+        throw e;
+    }
+
+    console.log("Done.");
+}

+ 117 - 45
src/app/[orgId]/settings/resources/CreateResourceForm.tsx

@@ -63,6 +63,8 @@ import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import Link from "next/link";
 import Link from "next/link";
 import { SquareArrowOutUpRight } from "lucide-react";
 import { SquareArrowOutUpRight } from "lucide-react";
 import CopyTextBox from "@app/components/CopyTextBox";
 import CopyTextBox from "@app/components/CopyTextBox";
+import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
+import { Label } from "@app/components/ui/label";
 
 
 const createResourceFormSchema = z
 const createResourceFormSchema = z
     .object({
     .object({
@@ -71,7 +73,8 @@ const createResourceFormSchema = z
         siteId: z.number(),
         siteId: z.number(),
         http: z.boolean(),
         http: z.boolean(),
         protocol: z.string(),
         protocol: z.string(),
-        proxyPort: z.number().optional()
+        proxyPort: z.number().optional(),
+        isBaseDomain: z.boolean().optional()
     })
     })
     .refine(
     .refine(
         (data) => {
         (data) => {
@@ -92,7 +95,7 @@ const createResourceFormSchema = z
     )
     )
     .refine(
     .refine(
         (data) => {
         (data) => {
-            if (data.http) {
+            if (data.http && !data.isBaseDomain) {
                 return subdomainSchema.safeParse(data.subdomain).success;
                 return subdomainSchema.safeParse(data.subdomain).success;
             }
             }
             return true;
             return true;
@@ -129,26 +132,36 @@ export default function CreateResourceForm({
 
 
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
     const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
     const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
-
     const [showSnippets, setShowSnippets] = useState(false);
     const [showSnippets, setShowSnippets] = useState(false);
-
     const [resourceId, setResourceId] = useState<number | null>(null);
     const [resourceId, setResourceId] = useState<number | null>(null);
+    const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
+        "subdomain"
+    );
 
 
     const form = useForm<CreateResourceFormValues>({
     const form = useForm<CreateResourceFormValues>({
         resolver: zodResolver(createResourceFormSchema),
         resolver: zodResolver(createResourceFormSchema),
         defaultValues: {
         defaultValues: {
             subdomain: "",
             subdomain: "",
-            name: "My Resource",
+            name: "",
             http: true,
             http: true,
             protocol: "tcp"
             protocol: "tcp"
         }
         }
     });
     });
 
 
+    function reset() {
+        form.reset();
+        setSites([]);
+        setShowSnippets(false);
+        setResourceId(null);
+    }
+
     useEffect(() => {
     useEffect(() => {
         if (!open) {
         if (!open) {
             return;
             return;
         }
         }
 
 
+        reset();
+
         const fetchSites = async () => {
         const fetchSites = async () => {
             const res = await api.get<AxiosResponse<ListSitesResponse>>(
             const res = await api.get<AxiosResponse<ListSitesResponse>>(
                 `/org/${orgId}/sites/`
                 `/org/${orgId}/sites/`
@@ -173,7 +186,8 @@ export default function CreateResourceForm({
                     http: data.http,
                     http: data.http,
                     protocol: data.protocol,
                     protocol: data.protocol,
                     proxyPort: data.http ? undefined : data.proxyPort,
                     proxyPort: data.http ? undefined : data.proxyPort,
-                    siteId: data.siteId
+                    siteId: data.siteId,
+                    isBaseDomain: data.isBaseDomain
                 }
                 }
             )
             )
             .catch((e) => {
             .catch((e) => {
@@ -239,7 +253,7 @@ export default function CreateResourceForm({
                                                 <FormLabel>Name</FormLabel>
                                                 <FormLabel>Name</FormLabel>
                                                 <FormControl>
                                                 <FormControl>
                                                     <Input
                                                     <Input
-                                                        placeholder="Your name"
+                                                        placeholder="Resource name"
                                                         {...field}
                                                         {...field}
                                                     />
                                                     />
                                                 </FormControl>
                                                 </FormControl>
@@ -284,33 +298,89 @@ export default function CreateResourceForm({
                                         />
                                         />
                                     )}
                                     )}
 
 
+                                    {form.watch("http") &&
+                                        env.flags.allowBaseDomainResources && (
+                                            <div>
+                                                <RadioGroup
+                                                    className="flex space-x-4"
+                                                    defaultValue={domainType}
+                                                    onValueChange={(val) => {
+                                                        setDomainType(
+                                                            val as any
+                                                        );
+                                                        form.setValue(
+                                                            "isBaseDomain",
+                                                            val === "basedomain"
+                                                        );
+                                                    }}
+                                                >
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="subdomain"
+                                                            id="r1"
+                                                        />
+                                                        <Label htmlFor="r1">
+                                                            Subdomain
+                                                        </Label>
+                                                    </div>
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="basedomain"
+                                                            id="r2"
+                                                        />
+                                                        <Label htmlFor="r2">
+                                                            Base Domain
+                                                        </Label>
+                                                    </div>
+                                                </RadioGroup>
+                                            </div>
+                                        )}
+
                                     {form.watch("http") && (
                                     {form.watch("http") && (
                                         <FormField
                                         <FormField
                                             control={form.control}
                                             control={form.control}
                                             name="subdomain"
                                             name="subdomain"
                                             render={({ field }) => (
                                             render={({ field }) => (
                                                 <FormItem>
                                                 <FormItem>
-                                                    <FormLabel>
-                                                        Subdomain
-                                                    </FormLabel>
-                                                    <FormControl>
-                                                        <CustomDomainInput
-                                                            value={
-                                                                field.value ??
-                                                                ""
-                                                            }
-                                                            domainSuffix={
-                                                                domainSuffix
-                                                            }
-                                                            placeholder="Enter subdomain"
-                                                            onChange={(value) =>
-                                                                form.setValue(
-                                                                    "subdomain",
+                                                    {!env.flags
+                                                        .allowBaseDomainResources && (
+                                                        <FormLabel>
+                                                            Subdomain
+                                                        </FormLabel>
+                                                    )}
+                                                    {domainType ===
+                                                    "subdomain" ? (
+                                                        <FormControl>
+                                                            <CustomDomainInput
+                                                                value={
+                                                                    field.value ??
+                                                                    ""
+                                                                }
+                                                                domainSuffix={
+                                                                    domainSuffix
+                                                                }
+                                                                placeholder="Subdomain"
+                                                                onChange={(
                                                                     value
                                                                     value
-                                                                )
-                                                            }
-                                                        />
-                                                    </FormControl>
+                                                                ) =>
+                                                                    form.setValue(
+                                                                        "subdomain",
+                                                                        value
+                                                                    )
+                                                                }
+                                                            />
+                                                        </FormControl>
+                                                    ) : (
+                                                        <FormControl>
+                                                            <Input
+                                                                value={
+                                                                    domainSuffix
+                                                                }
+                                                                readOnly
+                                                                disabled
+                                                            />
+                                                        </FormControl>
+                                                    )}
                                                     <FormDescription>
                                                     <FormDescription>
                                                         This is the fully
                                                         This is the fully
                                                         qualified domain name
                                                         qualified domain name
@@ -464,9 +534,7 @@ export default function CreateResourceForm({
                                                                             site
                                                                             site
                                                                         ) => (
                                                                         ) => (
                                                                             <CommandItem
                                                                             <CommandItem
-                                                                                value={
-                                                                                    site.niceId
-                                                                                }
+                                                                                value={`${site.siteId}:${site.name}:${site.niceId}`}
                                                                                 key={
                                                                                 key={
                                                                                     site.siteId
                                                                                     site.siteId
                                                                                 }
                                                                                 }
@@ -560,21 +628,25 @@ export default function CreateResourceForm({
                         )}
                         )}
                     </CredenzaBody>
                     </CredenzaBody>
                     <CredenzaFooter>
                     <CredenzaFooter>
-                        {!showSnippets && <Button
-                            type="submit"
-                            form="create-resource-form"
-                            loading={loading}
-                            disabled={loading}
-                        >
-                            Create Resource
-                        </Button>}
-
-                        {showSnippets && <Button
-                            loading={loading}
-                            onClick={() => goToResource()}
-                        >
-                            Go to Resource
-                        </Button>}
+                        {!showSnippets && (
+                            <Button
+                                type="submit"
+                                form="create-resource-form"
+                                loading={loading}
+                                disabled={loading}
+                            >
+                                Create Resource
+                            </Button>
+                        )}
+
+                        {showSnippets && (
+                            <Button
+                                loading={loading}
+                                onClick={() => goToResource()}
+                            >
+                                Go to Resource
+                            </Button>
+                        )}
 
 
                         <CredenzaClose asChild>
                         <CredenzaClose asChild>
                             <Button variant="outline">Close</Button>
                             <Button variant="outline">Close</Button>

+ 26 - 26
src/app/[orgId]/settings/resources/ResourcesTable.tsx

@@ -38,7 +38,7 @@ export type ResourceRow = {
     domain: string;
     domain: string;
     site: string;
     site: string;
     siteId: string;
     siteId: string;
-    hasAuth: boolean;
+    authState: string;
     http: boolean;
     http: boolean;
     protocol: string;
     protocol: string;
     proxyPort: number | null;
     proxyPort: number | null;
@@ -165,9 +165,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
             header: "Protocol",
             header: "Protocol",
             cell: ({ row }) => {
             cell: ({ row }) => {
                 const resourceRow = row.original;
                 const resourceRow = row.original;
-                return (
-                    <span>{resourceRow.protocol.toUpperCase()}</span>
-                );
+                return <span>{resourceRow.protocol.toUpperCase()}</span>;
             }
             }
         },
         },
         {
         {
@@ -177,17 +175,23 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                 const resourceRow = row.original;
                 const resourceRow = row.original;
                 return (
                 return (
                     <div>
                     <div>
-                    {!resourceRow.http ? (
-                                            <CopyToClipboard text={resourceRow.proxyPort!.toString()} isLink={false} />
-                    ) : (
-                    <CopyToClipboard text={resourceRow.domain} isLink={true} />
-                    )}
+                        {!resourceRow.http ? (
+                            <CopyToClipboard
+                                text={resourceRow.proxyPort!.toString()}
+                                isLink={false}
+                            />
+                        ) : (
+                            <CopyToClipboard
+                                text={resourceRow.domain}
+                                isLink={true}
+                            />
+                        )}
                     </div>
                     </div>
                 );
                 );
             }
             }
         },
         },
         {
         {
-            accessorKey: "hasAuth",
+            accessorKey: "authState",
             header: ({ column }) => {
             header: ({ column }) => {
                 return (
                 return (
                     <Button
                     <Button
@@ -205,23 +209,19 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                 const resourceRow = row.original;
                 const resourceRow = row.original;
                 return (
                 return (
                     <div>
                     <div>
-                        
-
-                        {!resourceRow.http ? (
+                        {resourceRow.authState === "protected" ? (
+                            <span className="text-green-500 flex items-center space-x-2">
+                                <ShieldCheck className="w-4 h-4" />
+                                <span>Protected</span>
+                            </span>
+                        ) : resourceRow.authState === "not_protected" ? (
+                            <span className="text-yellow-500 flex items-center space-x-2">
+                                <ShieldOff className="w-4 h-4" />
+                                <span>Not Protected</span>
+                            </span>
+                        ) : (
                             <span>--</span>
                             <span>--</span>
-                        ) : 
-                            resourceRow.hasAuth ? (
-                                <span className="text-green-500 flex items-center space-x-2">
-                                    <ShieldCheck className="w-4 h-4" />
-                                    <span>Protected</span>
-                                </span>
-                            ) : (
-                                <span className="text-yellow-500 flex items-center space-x-2">
-                                    <ShieldOff className="w-4 h-4" />
-                                    <span>Not Protected</span>
-                                </span>
-                            )
-                        }
+                        )}
                     </div>
                     </div>
                 );
                 );
             }
             }

+ 10 - 9
src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx

@@ -2,11 +2,7 @@
 
 
 import { useState } from "react";
 import { useState } from "react";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import {
-    InfoIcon,
-    ShieldCheck,
-    ShieldOff
-} from "lucide-react";
+import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { Separator } from "@app/components/ui/separator";
 import { Separator } from "@app/components/ui/separator";
@@ -26,9 +22,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
     const { org } = useOrgContext();
     const { org } = useOrgContext();
     const { resource, authInfo } = useResourceContext();
     const { resource, authInfo } = useResourceContext();
 
 
-    const fullUrl = `${resource.ssl ? "https" : "http"}://${
-        resource.subdomain
-    }.${org.org.domain}`;
+    let fullUrl = `${resource.ssl ? "https" : "http"}://`;
+    if (resource.isBaseDomain) {
+        fullUrl = fullUrl + org.org.domain;
+    } else {
+        fullUrl = fullUrl + `${resource.subdomain}.${org.org.domain}`;
+    }
 
 
     return (
     return (
         <Alert>
         <Alert>
@@ -82,7 +81,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
                             <InfoSection>
                             <InfoSection>
                                 <InfoSectionTitle>Protocol</InfoSectionTitle>
                                 <InfoSectionTitle>Protocol</InfoSectionTitle>
                                 <InfoSectionContent>
                                 <InfoSectionContent>
-                                    <span>{resource.protocol.toUpperCase()}</span>
+                                    <span>
+                                        {resource.protocol.toUpperCase()}
+                                    </span>
                                 </InfoSectionContent>
                                 </InfoSectionContent>
                             </InfoSection>
                             </InfoSection>
                             <Separator orientation="vertical" />
                             <Separator orientation="vertical" />

+ 1 - 2
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -132,9 +132,8 @@ export default function ReverseProxyTargets(props: {
         defaultValues: {
         defaultValues: {
             ip: "",
             ip: "",
             method: resource.http ? "http" : null,
             method: resource.http ? "http" : null,
-            port: resource.http ? 80 : resource.proxyPort || 1234
             // protocol: "TCP",
             // protocol: "TCP",
-        }
+        } as z.infer<typeof addTargetSchema>
     });
     });
 
 
     useEffect(() => {
     useEffect(() => {

+ 309 - 52
src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx

@@ -14,6 +14,19 @@ import {
     FormMessage
     FormMessage
 } from "@/components/ui/form";
 } from "@/components/ui/form";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
+import {
+    Command,
+    CommandEmpty,
+    CommandGroup,
+    CommandInput,
+    CommandItem
+} from "@/components/ui/command";
+import { cn } from "@app/lib/cn";
+import {
+    Popover,
+    PopoverContent,
+    PopoverTrigger
+} from "@/components/ui/popover";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { ListSitesResponse } from "@server/routers/site";
 import { ListSitesResponse } from "@server/routers/site";
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
@@ -37,13 +50,18 @@ import CustomDomainInput from "../CustomDomainInput";
 import { createApiClient } from "@app/lib/api";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
+import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
+import { pullEnv } from "@app/lib/pullEnv";
+import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
+import { Label } from "@app/components/ui/label";
 
 
 const GeneralFormSchema = z
 const GeneralFormSchema = z
     .object({
     .object({
         subdomain: z.string().optional(),
         subdomain: z.string().optional(),
         name: z.string().min(1).max(255),
         name: z.string().min(1).max(255),
         proxyPort: z.number().optional(),
         proxyPort: z.number().optional(),
-        http: z.boolean()
+        http: z.boolean(),
+        isBaseDomain: z.boolean().optional()
     })
     })
     .refine(
     .refine(
         (data) => {
         (data) => {
@@ -64,7 +82,7 @@ const GeneralFormSchema = z
     )
     )
     .refine(
     .refine(
         (data) => {
         (data) => {
-            if (data.http) {
+            if (data.http && !data.isBaseDomain) {
                 return subdomainSchema.safeParse(data.subdomain).success;
                 return subdomainSchema.safeParse(data.subdomain).success;
             }
             }
             return true;
             return true;
@@ -75,7 +93,12 @@ const GeneralFormSchema = z
         }
         }
     );
     );
 
 
+const TransferFormSchema = z.object({
+    siteId: z.number()
+});
+
 type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
 type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
+type TransferFormValues = z.infer<typeof TransferFormSchema>;
 
 
 export default function GeneralForm() {
 export default function GeneralForm() {
     const params = useParams();
     const params = useParams();
@@ -84,13 +107,21 @@ export default function GeneralForm() {
     const { org } = useOrgContext();
     const { org } = useOrgContext();
     const router = useRouter();
     const router = useRouter();
 
 
+    const { env } = useEnvContext();
+
     const orgId = params.orgId;
     const orgId = params.orgId;
 
 
-    const api = createApiClient(useEnvContext());
+    const api = createApiClient({ env });
 
 
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
     const [saveLoading, setSaveLoading] = useState(false);
     const [saveLoading, setSaveLoading] = useState(false);
     const [domainSuffix, setDomainSuffix] = useState(org.org.domain);
     const [domainSuffix, setDomainSuffix] = useState(org.org.domain);
+    const [transferLoading, setTransferLoading] = useState(false);
+    const [open, setOpen] = useState(false);
+
+    const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
+        resource.isBaseDomain ? "basedomain" : "subdomain"
+    );
 
 
     const form = useForm<GeneralFormValues>({
     const form = useForm<GeneralFormValues>({
         resolver: zodResolver(GeneralFormSchema),
         resolver: zodResolver(GeneralFormSchema),
@@ -98,11 +129,19 @@ export default function GeneralForm() {
             name: resource.name,
             name: resource.name,
             subdomain: resource.subdomain ? resource.subdomain : undefined,
             subdomain: resource.subdomain ? resource.subdomain : undefined,
             proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
             proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
-            http: resource.http
+            http: resource.http,
+            isBaseDomain: resource.isBaseDomain ? true : false
         },
         },
         mode: "onChange"
         mode: "onChange"
     });
     });
 
 
+    const transferForm = useForm<TransferFormValues>({
+        resolver: zodResolver(TransferFormSchema),
+        defaultValues: {
+            siteId: resource.siteId ? Number(resource.siteId) : undefined
+        }
+    });
+
     useEffect(() => {
     useEffect(() => {
         const fetchSites = async () => {
         const fetchSites = async () => {
             const res = await api.get<AxiosResponse<ListSitesResponse>>(
             const res = await api.get<AxiosResponse<ListSitesResponse>>(
@@ -116,14 +155,13 @@ export default function GeneralForm() {
     async function onSubmit(data: GeneralFormValues) {
     async function onSubmit(data: GeneralFormValues) {
         setSaveLoading(true);
         setSaveLoading(true);
 
 
-        api.post<AxiosResponse<GetResourceAuthInfoResponse>>(
-            `resource/${resource?.resourceId}`,
-            {
+        const res = await api
+            .post(`resource/${resource?.resourceId}`, {
                 name: data.name,
                 name: data.name,
-                subdomain: data.subdomain
-                // siteId: data.siteId,
-            }
-        )
+                subdomain: data.subdomain,
+                proxyPort: data.proxyPort,
+                isBaseDomain: data.isBaseDomain
+            })
             .catch((e) => {
             .catch((e) => {
                 toast({
                 toast({
                     variant: "destructive",
                     variant: "destructive",
@@ -133,18 +171,50 @@ export default function GeneralForm() {
                         "An error occurred while updating the resource"
                         "An error occurred while updating the resource"
                     )
                     )
                 });
                 });
+            });
+
+        if (res && res.status === 200) {
+            toast({
+                title: "Resource updated",
+                description: "The resource has been updated successfully"
+            });
+
+            updateResource({
+                name: data.name,
+                subdomain: data.subdomain,
+                proxyPort: data.proxyPort,
+                isBaseDomain: data.isBaseDomain
+            });
+        }
+        setSaveLoading(false);
+    }
+
+    async function onTransfer(data: TransferFormValues) {
+        setTransferLoading(true);
+
+        const res = await api
+            .post(`resource/${resource?.resourceId}/transfer`, {
+                siteId: data.siteId
             })
             })
-            .then(() => {
+            .catch((e) => {
                 toast({
                 toast({
-                    title: "Resource updated",
-                    description: "The resource has been updated successfully"
+                    variant: "destructive",
+                    title: "Failed to transfer resource",
+                    description: formatAxiosError(
+                        e,
+                        "An error occurred while transferring the resource"
+                    )
                 });
                 });
+            });
 
 
-                updateResource({ name: data.name, subdomain: data.subdomain });
-
-                router.refresh();
-            })
-            .finally(() => setSaveLoading(false));
+        if (res && res.status === 200) {
+            toast({
+                title: "Resource transferred",
+                description: "The resource has been transferred successfully"
+            });
+            router.refresh();
+        }
+        setTransferLoading(false);
     }
     }
 
 
     return (
     return (
@@ -185,40 +255,103 @@ export default function GeneralForm() {
                                     )}
                                     )}
                                 />
                                 />
 
 
-                                {resource.http ? (
-                                    <FormField
-                                        control={form.control}
-                                        name="subdomain"
-                                        render={({ field }) => (
-                                            <FormItem>
-                                                <FormLabel>Subdomain</FormLabel>
-                                                <FormControl>
-                                                    <CustomDomainInput
-                                                        value={
-                                                            field.value || ""
-                                                        }
-                                                        domainSuffix={
-                                                            domainSuffix
-                                                        }
-                                                        placeholder="Enter subdomain"
-                                                        onChange={(value) =>
-                                                            form.setValue(
-                                                                "subdomain",
-                                                                value
-                                                            )
-                                                        }
-                                                    />
-                                                </FormControl>
-                                                <FormDescription>
-                                                    This is the subdomain that
-                                                    will be used to access the
-                                                    resource.
-                                                </FormDescription>
-                                                <FormMessage />
-                                            </FormItem>
+                                {resource.http && (
+                                    <>
+                                        {env.flags.allowBaseDomainResources && (
+                                            <div>
+                                                <RadioGroup
+                                                    className="flex space-x-4"
+                                                    defaultValue={domainType}
+                                                    onValueChange={(val) => {
+                                                        setDomainType(
+                                                            val as any
+                                                        );
+                                                        form.setValue(
+                                                            "isBaseDomain",
+                                                            val === "basedomain"
+                                                        );
+                                                    }}
+                                                >
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="subdomain"
+                                                            id="r1"
+                                                        />
+                                                        <Label htmlFor="r1">
+                                                            Subdomain
+                                                        </Label>
+                                                    </div>
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="basedomain"
+                                                            id="r2"
+                                                        />
+                                                        <Label htmlFor="r2">
+                                                            Base Domain
+                                                        </Label>
+                                                    </div>
+                                                </RadioGroup>
+                                            </div>
                                         )}
                                         )}
-                                    />
-                                ) : (
+
+                                        <FormField
+                                            control={form.control}
+                                            name="subdomain"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    {!env.flags
+                                                        .allowBaseDomainResources && (
+                                                        <FormLabel>
+                                                            Subdomain
+                                                        </FormLabel>
+                                                    )}
+
+                                                    {domainType ===
+                                                    "subdomain" ? (
+                                                        <FormControl>
+                                                            <CustomDomainInput
+                                                                value={
+                                                                    field.value ||
+                                                                    ""
+                                                                }
+                                                                domainSuffix={
+                                                                    domainSuffix
+                                                                }
+                                                                placeholder="Enter subdomain"
+                                                                onChange={(
+                                                                    value
+                                                                ) =>
+                                                                    form.setValue(
+                                                                        "subdomain",
+                                                                        value
+                                                                    )
+                                                                }
+                                                            />
+                                                        </FormControl>
+                                                    ) : (
+                                                        <FormControl>
+                                                            <Input
+                                                                value={
+                                                                    domainSuffix
+                                                                }
+                                                                readOnly
+                                                                disabled
+                                                            />
+                                                        </FormControl>
+                                                    )}
+                                                    <FormDescription>
+                                                        This is the subdomain
+                                                        that will be used to
+                                                        access the resource.
+                                                    </FormDescription>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+                                    </>
+                                )}
+
+                                {!resource.http && (
                                     <FormField
                                     <FormField
                                         control={form.control}
                                         control={form.control}
                                         name="proxyPort"
                                         name="proxyPort"
@@ -273,6 +406,130 @@ export default function GeneralForm() {
                     </Button>
                     </Button>
                 </SettingsSectionFooter>
                 </SettingsSectionFooter>
             </SettingsSection>
             </SettingsSection>
+
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        Transfer Resource
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Transfer this resource to a different site
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+
+                <SettingsSectionBody>
+                    <SettingsSectionForm>
+                        <Form {...transferForm}>
+                            <form
+                                onSubmit={transferForm.handleSubmit(onTransfer)}
+                                className="space-y-4"
+                                id="transfer-form"
+                            >
+                                <FormField
+                                    control={transferForm.control}
+                                    name="siteId"
+                                    render={({ field }) => (
+                                        <FormItem className="flex flex-col">
+                                            <FormLabel>
+                                                Destination Site
+                                            </FormLabel>
+                                            <Popover
+                                                open={open}
+                                                onOpenChange={setOpen}
+                                            >
+                                                <PopoverTrigger asChild>
+                                                    <FormControl>
+                                                        <Button
+                                                            variant="outline"
+                                                            role="combobox"
+                                                            className={cn(
+                                                                "w-full justify-between",
+                                                                !field.value &&
+                                                                    "text-muted-foreground"
+                                                            )}
+                                                        >
+                                                            {field.value
+                                                                ? sites.find(
+                                                                      (site) =>
+                                                                          site.siteId ===
+                                                                          field.value
+                                                                  )?.name
+                                                                : "Select site"}
+                                                            <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                                                        </Button>
+                                                    </FormControl>
+                                                </PopoverTrigger>
+                                                <PopoverContent className="w-full p-0">
+                                                    <Command>
+                                                        <CommandInput
+                                                            placeholder="Search sites..."
+                                                            className="h-9"
+                                                        />
+                                                        <CommandEmpty>
+                                                            No sites found.
+                                                        </CommandEmpty>
+                                                        <CommandGroup>
+                                                            {sites.map(
+                                                                (site) => (
+                                                                    <CommandItem
+                                                                        value={`${site.name}:${site.siteId}`}
+                                                                        key={
+                                                                            site.siteId
+                                                                        }
+                                                                        onSelect={() => {
+                                                                            transferForm.setValue(
+                                                                                "siteId",
+                                                                                site.siteId
+                                                                            );
+                                                                            setOpen(
+                                                                                false
+                                                                            );
+                                                                        }}
+                                                                    >
+                                                                        {
+                                                                            site.name
+                                                                        }
+                                                                        <CheckIcon
+                                                                            className={cn(
+                                                                                "ml-auto h-4 w-4",
+                                                                                site.siteId ===
+                                                                                    field.value
+                                                                                    ? "opacity-100"
+                                                                                    : "opacity-0"
+                                                                            )}
+                                                                        />
+                                                                    </CommandItem>
+                                                                )
+                                                            )}
+                                                        </CommandGroup>
+                                                    </Command>
+                                                </PopoverContent>
+                                            </Popover>
+                                            <FormDescription>
+                                                Select the new site to transfer
+                                                this resource to.
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </SettingsSectionForm>
+                </SettingsSectionBody>
+
+                <SettingsSectionFooter>
+                    <Button
+                        type="submit"
+                        loading={transferLoading}
+                        disabled={transferLoading}
+                        form="transfer-form"
+                        variant="destructive"
+                    >
+                        Transfer Resource
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
         </SettingsContainer>
         </SettingsContainer>
     );
     );
 }
 }

+ 8 - 5
src/app/[orgId]/settings/resources/page.tsx

@@ -56,11 +56,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
             protocol: resource.protocol,
             protocol: resource.protocol,
             proxyPort: resource.proxyPort,
             proxyPort: resource.proxyPort,
             http: resource.http,
             http: resource.http,
-            hasAuth:
-                resource.sso ||
-                resource.pincodeId !== null ||
-                resource.pincodeId !== null ||
-                resource.whitelist
+            authState: !resource.http
+                ? "none"
+                : resource.sso ||
+                    resource.pincodeId !== null ||
+                    resource.pincodeId !== null ||
+                    resource.whitelist
+                  ? "protected"
+                  : "not_protected"
         };
         };
     });
     });
 
 

+ 1 - 1
src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx

@@ -320,7 +320,7 @@ export default function CreateShareLinkForm({
                                                                             ) => (
                                                                             ) => (
                                                                                 <CommandItem
                                                                                 <CommandItem
                                                                                     value={
                                                                                     value={
-                                                                                        r.resourceId.toString()
+                                                                                        `${r.name}:${r.resourceId}`
                                                                                     }
                                                                                     }
                                                                                     key={
                                                                                     key={
                                                                                         r.resourceId
                                                                                         r.resourceId

+ 2 - 7
src/app/[orgId]/settings/sites/CreateSiteForm.tsx

@@ -75,6 +75,7 @@ export default function CreateSiteForm({
     const { toast } = useToast();
     const { toast } = useToast();
 
 
     const api = createApiClient(useEnvContext());
     const api = createApiClient(useEnvContext());
+    const { env } = useEnvContext();
 
 
     const [isLoading, setIsLoading] = useState(false);
     const [isLoading, setIsLoading] = useState(false);
     const [isChecked, setIsChecked] = useState(false);
     const [isChecked, setIsChecked] = useState(false);
@@ -234,13 +235,7 @@ Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort}
 PersistentKeepalive = 5`
 PersistentKeepalive = 5`
             : "";
             : "";
 
 
-    // am I at http or https?
-    let proto = "https:";
-    // if (typeof window !== "undefined") {
-    //     proto = window.location.protocol;
-    // }
-
-    const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
+    const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
 
 
     return (
     return (
         <div className="space-y-4">
         <div className="space-y-4">

+ 46 - 23
src/components/ui/checkbox.tsx

@@ -1,30 +1,53 @@
-"use client"
+"use client";
 
 
-import * as React from "react"
-import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
-import { Check } from "lucide-react"
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { Check } from "lucide-react";
 
 
-import { cn } from "@app/lib/cn"
+import { cn } from "@app/lib/cn";
 
 
 const Checkbox = React.forwardRef<
 const Checkbox = React.forwardRef<
-  React.ElementRef<typeof CheckboxPrimitive.Root>,
-  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
+    React.ElementRef<typeof CheckboxPrimitive.Root>,
+    React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
 >(({ className, ...props }, ref) => (
 >(({ className, ...props }, ref) => (
-  <CheckboxPrimitive.Root
-    ref={ref}
-    className={cn(
-      "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
-      className
-    )}
-    {...props}
-  >
-    <CheckboxPrimitive.Indicator
-      className={cn("flex items-center justify-center text-current")}
+    <CheckboxPrimitive.Root
+        ref={ref}
+        className={cn(
+            "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
+            className
+        )}
+        {...props}
     >
     >
-      <Check className="h-4 w-4" />
-    </CheckboxPrimitive.Indicator>
-  </CheckboxPrimitive.Root>
-))
-Checkbox.displayName = CheckboxPrimitive.Root.displayName
+        <CheckboxPrimitive.Indicator
+            className={cn("flex items-center justify-center text-current")}
+        >
+            <Check className="h-4 w-4" />
+        </CheckboxPrimitive.Indicator>
+    </CheckboxPrimitive.Root>
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
 
 
-export { Checkbox }
+interface CheckboxWithLabelProps
+    extends React.ComponentPropsWithoutRef<typeof Checkbox> {
+    label: string;
+}
+
+const CheckboxWithLabel = React.forwardRef<
+    React.ElementRef<typeof Checkbox>,
+    CheckboxWithLabelProps
+>(({ className, label, id, ...props }, ref) => {
+    return (
+        <div className={cn("flex items-center space-x-2", className)}>
+            <Checkbox id={id} ref={ref} {...props} />
+            <label
+                htmlFor={id}
+                className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+            >
+                {label}
+            </label>
+        </div>
+    );
+});
+CheckboxWithLabel.displayName = "CheckboxWithLabel";
+
+export { Checkbox, CheckboxWithLabel };

+ 10 - 3
src/lib/pullEnv.ts

@@ -6,12 +6,15 @@ export function pullEnv(): Env {
             nextPort: process.env.NEXT_PORT as string,
             nextPort: process.env.NEXT_PORT as string,
             externalPort: process.env.SERVER_EXTERNAL_PORT as string,
             externalPort: process.env.SERVER_EXTERNAL_PORT as string,
             sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
             sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
-            resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
-            resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
+            resourceAccessTokenParam: process.env
+                .RESOURCE_ACCESS_TOKEN_PARAM as string,
+            resourceSessionRequestParam: process.env
+                .RESOURCE_SESSION_REQUEST_PARAM as string
         },
         },
         app: {
         app: {
             environment: process.env.ENVIRONMENT as string,
             environment: process.env.ENVIRONMENT as string,
-            version: process.env.APP_VERSION as string
+            version: process.env.APP_VERSION as string,
+            dashboardUrl: process.env.DASHBOARD_URL as string,
         },
         },
         email: {
         email: {
             emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
             emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
@@ -29,6 +32,10 @@ export function pullEnv(): Env {
                     : false,
                     : false,
             allowRawResources:
             allowRawResources:
                 process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
                 process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
+            allowBaseDomainResources:
+                process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES === "true"
+                    ? true
+                    : false
         }
         }
     };
     };
 }
 }

+ 2 - 0
src/lib/types/env.ts

@@ -2,6 +2,7 @@ export type Env = {
     app: {
     app: {
         environment: string;
         environment: string;
         version: string;
         version: string;
+        dashboardUrl: string;
     },
     },
     server: {
     server: {
         externalPort: string;
         externalPort: string;
@@ -18,5 +19,6 @@ export type Env = {
         disableUserCreateOrg: boolean;
         disableUserCreateOrg: boolean;
         emailVerificationRequired: boolean;
         emailVerificationRequired: boolean;
         allowRawResources: boolean;
         allowRawResources: boolean;
+        allowBaseDomainResources: boolean;
     }
     }
 };
 };