Bladeren bron

Merge branch 'main' of https://github.com/fosrl/pangolin

Owen Schwartz 5 maanden geleden
bovenliggende
commit
f7fe965fdf
91 gewijzigde bestanden met toevoegingen van 1799 en 1249 verwijderingen
  1. 2 0
      Dockerfile
  2. 4 2
      README.md
  3. 1 1
      package.json
  4. 38 0
      public/logo/pangolin_black.svg
  5. 39 0
      public/logo/pangolin_orange.svg
  6. BIN
      public/screenshots/auth.png
  7. BIN
      public/screenshots/connectivity.png
  8. BIN
      public/screenshots/preview.png
  9. BIN
      public/screenshots/share-link.png
  10. BIN
      public/screenshots/sites.png
  11. BIN
      public/screenshots/users.png
  12. 1 1
      server/db/names.ts
  13. 1 1
      server/emails/sendEmail.ts
  14. 25 31
      server/emails/templates/NotifyResetPassword.tsx
  15. 31 36
      server/emails/templates/ResetPasswordCode.tsx
  16. 29 33
      server/emails/templates/ResourceOTPCode.tsx
  17. 34 42
      server/emails/templates/SendInviteLink.tsx
  18. 28 32
      server/emails/templates/TwoFactorAuthNotification.tsx
  19. 31 36
      server/emails/templates/VerifyEmailCode.tsx
  20. 18 0
      server/emails/templates/components/ButtonLink.tsx
  21. 11 0
      server/emails/templates/components/CopyCodeBox.tsx
  22. 91 0
      server/emails/templates/components/Email.tsx
  23. 0 36
      server/emails/templates/components/LetterHead.tsx
  24. 9 0
      server/emails/templates/lib/theme.ts
  25. 3 2
      src/app/[orgId]/settings/access/roles/RolesDataTable.tsx
  26. 2 2
      src/app/[orgId]/settings/access/users/InviteUserForm.tsx
  27. 3 2
      src/app/[orgId]/settings/access/users/UsersDataTable.tsx
  28. 2 2
      src/app/[orgId]/settings/access/users/UsersTable.tsx
  29. 86 66
      src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx
  30. 82 59
      src/app/[orgId]/settings/general/page.tsx
  31. 2 2
      src/app/[orgId]/settings/layout.tsx
  32. 1 1
      src/app/[orgId]/settings/page.tsx
  33. 3 2
      src/app/[orgId]/settings/resources/ResourcesDataTable.tsx
  34. 68 0
      src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx
  35. 1 1
      src/app/[orgId]/settings/resources/ResourcesTable.tsx
  36. 1 3
      src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx
  37. 318 309
      src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx
  38. 182 227
      src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
  39. 97 156
      src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
  40. 3 0
      src/app/[orgId]/settings/resources/page.tsx
  41. 3 2
      src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx
  42. 70 0
      src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx
  43. 3 0
      src/app/[orgId]/settings/share-links/page.tsx
  44. 21 0
      src/app/[orgId]/settings/sites/CreateSiteForm.tsx
  45. 3 2
      src/app/[orgId]/settings/sites/SitesDataTable.tsx
  46. 98 0
      src/app/[orgId]/settings/sites/SitesSplashCard.tsx
  47. 1 1
      src/app/[orgId]/settings/sites/SitesTable.tsx
  48. 64 41
      src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
  49. 3 0
      src/app/[orgId]/settings/sites/page.tsx
  50. 15 4
      src/app/auth/login/DashboardLoginForm.tsx
  51. 4 1
      src/app/auth/login/page.tsx
  52. 0 2
      src/app/auth/reset-password/ResetPasswordForm.tsx
  53. 1 1
      src/app/auth/reset-password/page.tsx
  54. 8 7
      src/app/auth/resource/[resourceId]/page.tsx
  55. 29 12
      src/app/auth/signup/SignupForm.tsx
  56. 4 1
      src/app/auth/signup/page.tsx
  57. 4 1
      src/app/auth/verify-email/page.tsx
  58. BIN
      src/app/favicon.ico
  59. 6 6
      src/app/globals.css
  60. 19 17
      src/app/layout.tsx
  61. 5 2
      src/app/page.tsx
  62. 4 1
      src/app/setup/layout.tsx
  63. 2 2
      src/components/CopyTextBox.tsx
  64. 1 8
      src/components/Credenza.tsx
  65. 3 1
      src/components/Enable2FaForm.tsx
  66. 2 2
      src/components/Header.tsx
  67. 1 2
      src/components/LoginForm.tsx
  68. 3 3
      src/components/ProfileIcon.tsx
  69. 31 0
      src/components/Settings.tsx
  70. 1 1
      src/components/SettingsSectionTitle.tsx
  71. 2 2
      src/components/SidebarNav.tsx
  72. 2 2
      src/components/SidebarSettings.tsx
  73. 37 0
      src/components/SwitchInput.tsx
  74. 1 1
      src/components/TopbarNav.tsx
  75. 1 1
      src/components/ui/alert.tsx
  76. 4 5
      src/components/ui/button.tsx
  77. 2 2
      src/components/ui/command.tsx
  78. 1 1
      src/components/ui/dialog.tsx
  79. 2 2
      src/components/ui/input.tsx
  80. 10 1
      src/components/ui/popover.tsx
  81. 3 2
      src/components/ui/select.tsx
  82. 6 6
      src/components/ui/sheet.tsx
  83. 2 2
      src/components/ui/switch.tsx
  84. 4 0
      src/components/ui/table.tsx
  85. 2 2
      src/contexts/envContext.ts
  86. 4 1
      src/lib/api/cookies.ts
  87. 4 4
      src/lib/api/index.ts
  88. 4 1
      src/lib/auth/verifySession.ts
  89. 31 0
      src/lib/pullEnv.ts
  90. 19 7
      src/lib/types/env.ts
  91. 2 2
      src/providers/EnvProvider.tsx

+ 2 - 0
Dockerfile

@@ -29,4 +29,6 @@ COPY --from=builder /app/init ./dist/init
 COPY config.example.yml ./dist/config.example.yml
 COPY server/db/names.json ./dist/names.json
 
+COPY public ./public
+
 CMD ["npm", "start"]

+ 4 - 2
README.md

@@ -1,6 +1,6 @@
 # Pangolin
 
-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 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.
 
 ### Installation and Documentation
 
@@ -9,7 +9,7 @@ Pangolin is a self-hosted tunneled reverse proxy management server with identity
 
 ## Preview
 
-<img src="public/screenshots/preview.png" alt="Preview"/>
+<img src="public/screenshots/sites.png" alt="Preview"/>
 
 _Sites page of Pangolin showing multiple site-to-site tunnels connected to the central server._
 
@@ -25,6 +25,7 @@ _Sites page of Pangolin showing multiple site-to-site tunnels connected to the c
 ### Identity & Access Management
 
 -   Centralized authentication system using platform SSO. **Users will only have to manage one login.**
+-   Totp with backup codes for two-factor authentication.
 -   Create organizations, each with multiple sites, users, and roles.
 -   **Role-based access control** to manage resource access permissions.
 -   Additional authentication options include:
@@ -38,6 +39,7 @@ _Sites page of Pangolin showing multiple site-to-site tunnels connected to the c
 -   Manage sites, users, and roles with a clean and intuitive UI.
 -   Monitor site usage and connectivity.
 -   Light and dark mode options.
+-   Mobile friendly.
 
 ### Easy Deployment
 

+ 1 - 1
package.json

@@ -9,7 +9,7 @@
         "db:push": "npx tsx server/db/migrate.ts",
         "db:studio": "drizzle-kit studio",
         "build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.ts -o dist/migrations.mjs",
-        "start": "NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
+        "start": "NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
         "email": "email dev --dir server/emails/templates --port 3005"
     },
     "dependencies": {

+ 38 - 0
public/logo/pangolin_black.svg

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 399.99999 400.00002"
+   enable-background="new 0 0 419.528 419.528"
+   xml:space="preserve"
+   id="svg52"
+   sodipodi:docname="noun-pangolin-1798092.svg"
+   width="400"
+   height="400"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><defs
+     id="defs56" /><sodipodi:namedview
+     id="namedview54"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="1.9583914"
+     inkscape:cx="209.86611"
+     inkscape:cy="262.20499"
+     inkscape:window-width="3840"
+     inkscape:window-height="2136"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg52" /><path
+     d="m 62.232921,184.91974 c 0,2.431 -1.97,4.402 -4.399,4.402 -2.429,0 -4.399,-1.972 -4.399,-4.402 0,-2.429 1.97,-4.399 4.399,-4.399 2.429,-10e-4 4.399,1.97 4.399,4.399 z m 58.993999,-4.821 c -25.943999,-2.826 -38.978999,7.453 -71.181999,31.357 -27.572,20.467 -32.767,4.381 -31.748,-2.614 1.499,-10.282 25.222,-58.573 48.079,-88.461 28.273,7.34 49.869999,30.727 54.850999,59.718 z m -55.915999,4.821 c 0,-4.131 -3.349,-7.478 -7.478,-7.478 -4.129,0 -7.478,3.347 -7.478,7.478 0,4.131 3.349,7.481 7.478,7.481 4.13,0 7.478,-3.35 7.478,-7.481 z m -15.032,48.424 -0.234,14.041 20.413,22.687 -9.818,7.353 33.306,27.492 -11.759,8.124 42.631999,19.939 -10.825,9.747 48.291,8.078 -7.526,10.307 48.758,-4.531 -3.997,11.725 53.916,-18.153 -2.76,13.357 48.077,-34.345 1.479,13.562 34.087,-48.576 7.478,14.206 15.187,-58.89 10.391,8.533 -2.14,-57.884 13.814,5.13 -21.082,-51.204 13.404,0.048 -33.696,-42.131 15.312,-1.366 -47.026,-32.831002 14.255,-8.399 -54.817,-14.682 9.257,-11.695 -49.625,0.352 0.6,-13.337 -38.537,14.084 -1.597,-12.689 -29.984,21.429 -6.446,-10.852 -22.59,26.504 -7.021,-9.572 -18.923,30.294 -9.595999,-8.744 -16.754,30.138002 c 31.509999,10.197 54.979999,37.951 59.126999,71.547 0.404,0.087 -22.37,31.257 10.955,57.85 -0.576,-2.985 -6.113,-53.902 47.496,-57.61 26.668,-1.844 48.4,21.666 48.4,48.399 0,8.184 -2.05,15.883 -5.636,22.64 -15.927,29.611 -64.858,30.755 -80.429,30.596 -45.154,-0.459 -104.051999,-51.521 -104.051999,-51.521 z"
+     id="path46" /></svg>

+ 39 - 0
public/logo/pangolin_orange.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   version="1.1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 399.99999 400.00002"
+   enable-background="new 0 0 419.528 419.528"
+   xml:space="preserve"
+   id="svg52"
+   sodipodi:docname="pangolin_orange.svg"
+   width="400"
+   height="400"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><defs
+     id="defs56" /><sodipodi:namedview
+     id="namedview54"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="1.9583914"
+     inkscape:cx="127.40048"
+     inkscape:cy="262.71561"
+     inkscape:window-width="1436"
+     inkscape:window-height="1236"
+     inkscape:window-x="2208"
+     inkscape:window-y="511"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg52" /><path
+     d="m 62.232921,184.91974 c 0,2.431 -1.97,4.402 -4.399,4.402 -2.429,0 -4.399,-1.972 -4.399,-4.402 0,-2.429 1.97,-4.399 4.399,-4.399 2.429,-10e-4 4.399,1.97 4.399,4.399 z m 58.993999,-4.821 c -25.943999,-2.826 -38.978999,7.453 -71.181999,31.357 -27.572,20.467 -32.767,4.381 -31.748,-2.614 1.499,-10.282 25.222,-58.573 48.079,-88.461 28.273,7.34 49.869999,30.727 54.850999,59.718 z m -55.915999,4.821 c 0,-4.131 -3.349,-7.478 -7.478,-7.478 -4.129,0 -7.478,3.347 -7.478,7.478 0,4.131 3.349,7.481 7.478,7.481 4.13,0 7.478,-3.35 7.478,-7.481 z m -15.032,48.424 -0.234,14.041 20.413,22.687 -9.818,7.353 33.306,27.492 -11.759,8.124 42.631999,19.939 -10.825,9.747 48.291,8.078 -7.526,10.307 48.758,-4.531 -3.997,11.725 53.916,-18.153 -2.76,13.357 48.077,-34.345 1.479,13.562 34.087,-48.576 7.478,14.206 15.187,-58.89 10.391,8.533 -2.14,-57.884 13.814,5.13 -21.082,-51.204 13.404,0.048 -33.696,-42.131 15.312,-1.366 -47.026,-32.831002 14.255,-8.399 -54.817,-14.682 9.257,-11.695 -49.625,0.352 0.6,-13.337 -38.537,14.084 -1.597,-12.689 -29.984,21.429 -6.446,-10.852 -22.59,26.504 -7.021,-9.572 -18.923,30.294 -9.595999,-8.744 -16.754,30.138002 c 31.509999,10.197 54.979999,37.951 59.126999,71.547 0.404,0.087 -22.37,31.257 10.955,57.85 -0.576,-2.985 -6.113,-53.902 47.496,-57.61 26.668,-1.844 48.4,21.666 48.4,48.399 0,8.184 -2.05,15.883 -5.636,22.64 -15.927,29.611 -64.858,30.755 -80.429,30.596 -45.154,-0.459 -104.051999,-51.521 -104.051999,-51.521 z"
+     id="path46"
+     style="fill:#f97315;fill-opacity:1" /></svg>

BIN
public/screenshots/auth.png


BIN
public/screenshots/connectivity.png


BIN
public/screenshots/preview.png


BIN
public/screenshots/share-link.png


BIN
public/screenshots/sites.png


BIN
public/screenshots/users.png


+ 1 - 1
server/db/names.ts

@@ -9,7 +9,7 @@ import { __DIRNAME } from "@server/lib/consts";
 const dev = process.env.ENVIRONMENT !== "prod";
 let file;
 if (!dev) {
-    file = join("names.json");
+    file = join(__DIRNAME, "names.json");
 } else {
     file = join("server/db/names.json");
 }

+ 1 - 1
server/emails/sendEmail.ts

@@ -26,7 +26,7 @@ export async function sendEmail(
 
     await emailClient.sendMail({
         from: {
-            name: opts.name || "Pangolin Proxy",
+            name: opts.name || "Pangolin",
             address: opts.from,
         },
         to: opts.to,

+ 25 - 31
server/emails/templates/NotifyResetPassword.tsx

@@ -1,16 +1,20 @@
 import {
     Body,
-    Container,
     Head,
-    Heading,
     Html,
     Preview,
-    Section,
-    Text,
     Tailwind
 } from "@react-email/components";
 import * as React from "react";
-import LetterHead from "./components/LetterHead";
+import { themeColors } from "./lib/theme";
+import {
+    EmailContainer,
+    EmailFooter,
+    EmailGreeting,
+    EmailHeading,
+    EmailLetterHead,
+    EmailText
+} from "./components/Email";
 
 interface Props {
     email: string;
@@ -23,41 +27,31 @@ export const ConfirmPasswordReset = ({ email }: Props) => {
         <Html>
             <Head />
             <Preview>{previewText}</Preview>
-            <Tailwind
-                config={{
-                    theme: {
-                        extend: {
-                            colors: {
-                                primary: "#16A34A"
-                            }
-                        }
-                    }
-                }}
-            >
+            <Tailwind config={themeColors}>
                 <Body className="font-sans relative">
-                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <LetterHead />
+                    <EmailContainer>
+                        <EmailLetterHead />
+
+                        <EmailHeading>Password Reset Confirmation</EmailHeading>
+
+                        <EmailGreeting>Hi {email || "there"},</EmailGreeting>
 
-                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
-                            Password Reset Confirmation
-                        </Heading>
-                        <Text className="text-base text-gray-700 mt-4">
-                            Hi {email || "there"},
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        <EmailText>
                             This email confirms that your password has just been
                             reset. If you made this change, no further action is
                             required.
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        </EmailText>
+
+                        <EmailText>
                             Thank you for keeping your account secure.
-                        </Text>
-                        <Text className="text-sm text-gray-500 mt-6">
+                        </EmailText>
+
+                        <EmailFooter>
                             Best regards,
                             <br />
                             Fossorial
-                        </Text>
-                    </Container>
+                        </EmailFooter>
+                    </EmailContainer>
                 </Body>
             </Tailwind>
         </Html>

+ 31 - 36
server/emails/templates/ResetPasswordCode.tsx

@@ -1,16 +1,22 @@
 import {
     Body,
-    Container,
     Head,
-    Heading,
     Html,
     Preview,
-    Section,
-    Text,
     Tailwind
 } from "@react-email/components";
 import * as React from "react";
-import LetterHead from "./components/LetterHead";
+import { themeColors } from "./lib/theme";
+import {
+    EmailContainer,
+    EmailFooter,
+    EmailGreeting,
+    EmailHeading,
+    EmailLetterHead,
+    EmailSection,
+    EmailText
+} from "./components/Email";
+import CopyCodeBox from "./components/CopyCodeBox";
 
 interface Props {
     email: string;
@@ -25,50 +31,39 @@ export const ResetPasswordCode = ({ email, code, link }: Props) => {
         <Html>
             <Head />
             <Preview>{previewText}</Preview>
-            <Tailwind
-                config={{
-                    theme: {
-                        extend: {
-                            colors: {
-                                primary: "#F97317"
-                            }
-                        }
-                    }
-                }}
-            >
+            <Tailwind config={themeColors}>
                 <Body className="font-sans">
-                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <LetterHead />
+                    <EmailContainer>
+                        <EmailLetterHead />
+
+                        <EmailHeading>Password Reset Request</EmailHeading>
+
+                        <EmailGreeting>Hi {email || "there"},</EmailGreeting>
 
-                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
-                            Password Reset Request
-                        </Heading>
-                        <Text className="text-base text-gray-700 mt-4">
-                            Hi {email || "there"},
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        <EmailText>
                             You’ve requested to reset your password. Please{" "}
                             <a href={link} className="text-primary">
                                 click here
                             </a>{" "}
                             and follow the instructions to reset your password,
                             or manually enter the following code:
-                        </Text>
-                        <Section className="text-center">
-                            <Text className="inline-block bg-primary text-xl font-bold text-white py-2 px-4 border border-gray-300 rounded-xl">
-                                {code}
-                            </Text>
-                        </Section>
-                        <Text className="text-base text-gray-700 mt-2">
+                        </EmailText>
+
+                        <EmailSection>
+                            <CopyCodeBox text={code} />
+                        </EmailSection>
+
+                        <EmailText>
                             If you didn’t request this, you can safely ignore
                             this email.
-                        </Text>
-                        <Text className="text-sm text-gray-500 mt-6">
+                        </EmailText>
+
+                        <EmailFooter>
                             Best regards,
                             <br />
                             Fossorial
-                        </Text>
-                    </Container>
+                        </EmailFooter>
+                    </EmailContainer>
                 </Body>
             </Tailwind>
         </Html>

+ 29 - 33
server/emails/templates/ResourceOTPCode.tsx

@@ -1,16 +1,22 @@
 import {
     Body,
-    Container,
     Head,
-    Heading,
     Html,
     Preview,
-    Section,
-    Text,
     Tailwind
 } from "@react-email/components";
 import * as React from "react";
-import LetterHead from "./components/LetterHead";
+import {
+    EmailContainer,
+    EmailLetterHead,
+    EmailHeading,
+    EmailText,
+    EmailFooter,
+    EmailSection,
+    EmailGreeting
+} from "./components/Email";
+import { themeColors } from "./lib/theme";
+import CopyCodeBox from "./components/CopyCodeBox";
 
 interface ResourceOTPCodeProps {
     email?: string;
@@ -31,44 +37,34 @@ export const ResourceOTPCode = ({
         <Html>
             <Head />
             <Preview>{previewText}</Preview>
-            <Tailwind
-                config={{
-                    theme: {
-                        extend: {
-                            colors: {
-                                primary: "#F97317"
-                            }
-                        }
-                    }
-                }}
-            >
+            <Tailwind config={themeColors}>
                 <Body className="font-sans">
-                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <LetterHead />
+                    <EmailContainer>
+                        <EmailLetterHead />
 
-                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
+                        <EmailHeading>
                             Your One-Time Password for {resourceName}
-                        </Heading>
-                        <Text className="text-base text-gray-700 mt-4">
-                            Hi {email || "there"},
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        </EmailHeading>
+
+                        <EmailGreeting>Hi {email || "there"},</EmailGreeting>
+
+                        <EmailText>
                             You’ve requested a one-time password to access{" "}
                             <strong>{resourceName}</strong> in{" "}
                             <strong>{organizationName}</strong>. Use the code
                             below to complete your authentication:
-                        </Text>
-                        <Section className="text-center">
-                            <Text className="inline-block bg-primary text-xl font-bold text-white py-2 px-4 border border-gray-300 rounded-xl">
-                                {otp}
-                            </Text>
-                        </Section>
-                        <Text className="text-sm text-gray-500 mt-6">
+                        </EmailText>
+
+                        <EmailSection>
+                            <CopyCodeBox text={otp} />
+                        </EmailSection>
+
+                        <EmailFooter>
                             Best regards,
                             <br />
                             Fossorial
-                        </Text>
-                    </Container>
+                        </EmailFooter>
+                    </EmailContainer>
                 </Body>
             </Tailwind>
         </Html>

+ 34 - 42
server/emails/templates/SendInviteLink.tsx

@@ -1,17 +1,22 @@
 import {
     Body,
-    Container,
     Head,
-    Heading,
     Html,
     Preview,
-    Section,
-    Text,
     Tailwind,
-    Button
 } from "@react-email/components";
 import * as React from "react";
-import LetterHead from "./components/LetterHead";
+import { themeColors } from "./lib/theme";
+import {
+    EmailContainer,
+    EmailFooter,
+    EmailGreeting,
+    EmailHeading,
+    EmailLetterHead,
+    EmailSection,
+    EmailText
+} from "./components/Email";
+import ButtonLink from "./components/ButtonLink";
 
 interface SendInviteLinkProps {
     email: string;
@@ -34,55 +39,42 @@ export const SendInviteLink = ({
         <Html>
             <Head />
             <Preview>{previewText}</Preview>
-            <Tailwind
-                config={{
-                    theme: {
-                        extend: {
-                            colors: {
-                                primary: "#F97317"
-                            }
-                        }
-                    }
-                }}
-            >
+            <Tailwind config={themeColors}>
                 <Body className="font-sans">
-                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <LetterHead />
+                    <EmailContainer>
+                        <EmailLetterHead />
+
+                        <EmailHeading>Invited to Join {orgName}</EmailHeading>
+
+                        <EmailGreeting>Hi {email || "there"},</EmailGreeting>
 
-                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
-                            Invited to Join {orgName}
-                        </Heading>
-                        <Text className="text-base text-gray-700 mt-4">
-                            Hi {email || "there"},
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        <EmailText>
                             You’ve been invited to join the organization{" "}
-                            {orgName}
+                            <strong>{orgName}</strong>
                             {inviterName ? ` by ${inviterName}.` : "."} Please
                             access the link below to accept the invite.
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        </EmailText>
+
+                        <EmailText>
                             This invite will expire in{" "}
-                            <b>
+                            <strong>
                                 {expiresInDays}{" "}
                                 {expiresInDays === "1" ? "day" : "days"}.
-                            </b>
-                        </Text>
-                        <Section className="text-center">
-                            <Button
-                                href={inviteLink}
-                                className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer text-xl"
-                            >
+                            </strong>
+                        </EmailText>
+
+                        <EmailSection>
+                            <ButtonLink href={inviteLink}>
                                 Accept Invite to {orgName}
-                            </Button>
-                        </Section>
+                            </ButtonLink>
+                        </EmailSection>
 
-                        <Text className="text-sm text-gray-500 mt-6">
+                        <EmailFooter>
                             Best regards,
                             <br />
                             Fossorial
-                        </Text>
-                    </Container>
+                        </EmailFooter>
+                    </EmailContainer>
                 </Body>
             </Tailwind>
         </Html>

+ 28 - 32
server/emails/templates/TwoFactorAuthNotification.tsx

@@ -1,16 +1,20 @@
 import {
     Body,
-    Container,
     Head,
-    Heading,
     Html,
     Preview,
-    Section,
-    Text,
     Tailwind
 } from "@react-email/components";
 import * as React from "react";
-import LetterHead from "./components/LetterHead";
+import { themeColors } from "./lib/theme";
+import {
+    EmailContainer,
+    EmailFooter,
+    EmailGreeting,
+    EmailHeading,
+    EmailLetterHead,
+    EmailText
+} from "./components/Email";
 
 interface Props {
     email: string;
@@ -24,52 +28,44 @@ export const TwoFactorAuthNotification = ({ email, enabled }: Props) => {
         <Html>
             <Head />
             <Preview>{previewText}</Preview>
-            <Tailwind
-                config={{
-                    theme: {
-                        extend: {
-                            colors: {
-                                primary: "#16A34A"
-                            }
-                        }
-                    }
-                }}
-            >
+            <Tailwind config={themeColors}>
                 <Body className="font-sans">
-                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <LetterHead />
+                    <EmailContainer>
+                        <EmailLetterHead />
 
-                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
+                        <EmailHeading>
                             Two-Factor Authentication{" "}
                             {enabled ? "Enabled" : "Disabled"}
-                        </Heading>
-                        <Text className="text-base text-gray-700 mt-4">
-                            Hi {email || "there"},
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        </EmailHeading>
+
+                        <EmailGreeting>Hi {email || "there"},</EmailGreeting>
+
+                        <EmailText>
                             This email confirms that Two-Factor Authentication
                             has been successfully{" "}
                             {enabled ? "enabled" : "disabled"} on your account.
-                        </Text>
+                        </EmailText>
+
                         {enabled ? (
-                            <Text className="text-base text-gray-700">
+                            <EmailText>
                                 With Two-Factor Authentication enabled, your
                                 account is now more secure. Please ensure you
                                 keep your authentication method safe.
-                            </Text>
+                            </EmailText>
                         ) : (
-                            <Text className="text-base text-gray-700">
+                            <EmailText>
                                 With Two-Factor Authentication disabled, your
                                 account may be less secure. We recommend
                                 enabling it to protect your account.
-                            </Text>
+                            </EmailText>
                         )}
-                        <Text className="text-sm text-gray-500 mt-6">
+
+                        <EmailFooter>
                             Best regards,
                             <br />
                             Fossorial
-                        </Text>
-                    </Container>
+                        </EmailFooter>
+                    </EmailContainer>
                 </Body>
             </Tailwind>
         </Html>

+ 31 - 36
server/emails/templates/VerifyEmailCode.tsx

@@ -1,16 +1,22 @@
 import {
     Body,
-    Container,
     Head,
-    Heading,
     Html,
     Preview,
-    Section,
-    Text,
     Tailwind
 } from "@react-email/components";
 import * as React from "react";
-import LetterHead from "./components/LetterHead";
+import { themeColors } from "./lib/theme";
+import {
+    EmailContainer,
+    EmailFooter,
+    EmailGreeting,
+    EmailHeading,
+    EmailLetterHead,
+    EmailSection,
+    EmailText
+} from "./components/Email";
+import CopyCodeBox from "./components/CopyCodeBox";
 
 interface VerifyEmailProps {
     username?: string;
@@ -29,47 +35,36 @@ export const VerifyEmail = ({
         <Html>
             <Head />
             <Preview>{previewText}</Preview>
-            <Tailwind
-                config={{
-                    theme: {
-                        extend: {
-                            colors: {
-                                primary: "#F97317"
-                            }
-                        }
-                    }
-                }}
-            >
+            <Tailwind config={themeColors}>
                 <Body className="font-sans">
-                    <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <LetterHead />
+                    <EmailContainer>
+                        <EmailLetterHead />
+
+                        <EmailHeading>Please Verify Your Email</EmailHeading>
+
+                        <EmailGreeting>Hi {username || "there"},</EmailGreeting>
 
-                        <Heading className="text-2xl font-semibold text-gray-800 text-center">
-                            Please Verify Your Email
-                        </Heading>
-                        <Text className="text-base text-gray-700 mt-4">
-                            Hi {username || "there"},
-                        </Text>
-                        <Text className="text-base text-gray-700 mt-2">
+                        <EmailText>
                             You’ve requested to verify your email. Please use
                             the code below to complete the verification process
                             upon logging in.
-                        </Text>
-                        <Section className="text-center">
-                            <Text className="inline-block bg-primary text-xl font-bold text-white py-2 px-4 border border-gray-300 rounded-xl">
-                                {verificationCode}
-                            </Text>
-                        </Section>
-                        <Text className="text-base text-gray-700 mt-2">
+                        </EmailText>
+
+                        <EmailSection>
+                            <CopyCodeBox text={verificationCode} />
+                        </EmailSection>
+
+                        <EmailText>
                             If you didn’t request this, you can safely ignore
                             this email.
-                        </Text>
-                        <Text className="text-sm text-gray-500 mt-6">
+                        </EmailText>
+
+                        <EmailFooter>
                             Best regards,
                             <br />
                             Fossorial
-                        </Text>
-                    </Container>
+                        </EmailFooter>
+                    </EmailContainer>
                 </Body>
             </Tailwind>
         </Html>

+ 18 - 0
server/emails/templates/components/ButtonLink.tsx

@@ -0,0 +1,18 @@
+export default function ButtonLink({
+    href,
+    children,
+    className = ""
+}: {
+    href: string;
+    children: React.ReactNode;
+    className?: string;
+}) {
+    return (
+        <a
+            href={href}
+            className={`rounded-full bg-primary px-4 py-2 text-center font-semibold text-white text-xl no-underline inline-block ${className}`}
+        >
+            {children}
+        </a>
+    );
+}

+ 11 - 0
server/emails/templates/components/CopyCodeBox.tsx

@@ -0,0 +1,11 @@
+import React from "react";
+
+export default function CopyCodeBox({ text }: { text: string }) {
+    return (
+        <div className="flex items-center justify-center rounded-lg bg-neutral-100 p-2">
+            <span className="text-2xl font-mono text-neutral-600 tracking-wide">
+                {text}
+            </span>
+        </div>
+    );
+}

+ 91 - 0
server/emails/templates/components/Email.tsx

@@ -0,0 +1,91 @@
+import { Container } from "@react-email/components";
+import React from "react";
+
+// EmailContainer: Wraps the entire email layout
+export function EmailContainer({ children }: { children: React.ReactNode }) {
+    return (
+        <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
+            {children}
+        </Container>
+    );
+}
+
+// EmailLetterHead: For branding or logo at the top
+export function EmailLetterHead() {
+    return (
+        <div className="mb-4">
+            <table
+                role="presentation"
+                width="100%"
+                style={{
+                    marginBottom: "24px"
+                }}
+            >
+                <tr>
+                    <td
+                        style={{
+                            fontSize: "14px",
+                            fontWeight: "bold",
+                            color: "#F97317"
+                        }}
+                    >
+                        Pangolin
+                    </td>
+                    <td
+                        style={{
+                            fontSize: "14px",
+                            textAlign: "right",
+                            color: "#6B7280"
+                        }}
+                    >
+                        {new Date().getFullYear()}
+                    </td>
+                </tr>
+            </table>
+        </div>
+    );
+}
+
+// EmailHeading: For the primary message or headline
+export function EmailHeading({ children }: { children: React.ReactNode }) {
+    return (
+        <h1 className="text-2xl font-semibold text-gray-800 text-center">
+            {children}
+        </h1>
+    );
+}
+
+export function EmailGreeting({ children }: { children: React.ReactNode }) {
+    return <p className="text-lg text-gray-700 my-4">{children}</p>;
+}
+
+// EmailText: For general text content
+export function EmailText({
+    children,
+    className
+}: {
+    children: React.ReactNode;
+    className?: string;
+}) {
+    return (
+        <p className={`my-2 text-base text-gray-700 ${className}`}>
+            {children}
+        </p>
+    );
+}
+
+// EmailSection: For visually distinct sections (like OTP)
+export function EmailSection({
+    children,
+    className
+}: {
+    children: React.ReactNode;
+    className?: string;
+}) {
+    return <div className={`text-center my-4 ${className}`}>{children}</div>;
+}
+
+// EmailFooter: For closing or signature
+export function EmailFooter({ children }: { children: React.ReactNode }) {
+    return <div className="text-sm text-gray-500 mt-6">{children}</div>;
+}

+ 0 - 36
server/emails/templates/components/LetterHead.tsx

@@ -1,36 +0,0 @@
-import React from "react";
-
-export function LetterHead() {
-    return (
-        <table
-            role="presentation"
-            width="100%"
-            style={{
-                marginBottom: "24px"
-            }}
-        >
-            <tr>
-                <td
-                    style={{
-                        fontSize: "14px",
-                        fontWeight: "bold",
-                        color: "#F97317"
-                    }}
-                >
-                    Pangolin
-                </td>
-                <td
-                    style={{
-                        fontSize: "14px",
-                        textAlign: "right",
-                        color: "#6B7280"
-                    }}
-                >
-                    {new Date().getFullYear()}
-                </td>
-            </tr>
-        </table>
-    );
-}
-
-export default LetterHead;

+ 9 - 0
server/emails/templates/lib/theme.ts

@@ -0,0 +1,9 @@
+export const themeColors = {
+    theme: {
+        extend: {
+            colors: {
+                primary: "#F97317"
+            }
+        }
+    }
+};

+ 3 - 2
src/app/[orgId]/settings/access/roles/RolesDataTable.tsx

@@ -15,6 +15,7 @@ import {
     Table,
     TableBody,
     TableCell,
+    TableContainer,
     TableHead,
     TableHeader,
     TableRow,
@@ -88,7 +89,7 @@ export function RolesDataTable<TData, TValue>({
                     <Plus className="mr-2 h-4 w-4" /> Add Role
                 </Button>
             </div>
-            <div className="border rounded-md">
+            <TableContainer>
                 <Table>
                     <TableHeader>
                         {table.getHeaderGroups().map((headerGroup) => (
@@ -141,7 +142,7 @@ export function RolesDataTable<TData, TValue>({
                         )}
                     </TableBody>
                 </Table>
-            </div>
+            </TableContainer>
             <div className="mt-4">
                 <DataTablePagination table={table} />
             </div>

+ 2 - 2
src/app/[orgId]/settings/access/users/InviteUserForm.tsx

@@ -67,7 +67,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
 
     const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
 
-    const [sendEmail, setSendEmail] = useState(env.EMAIL_ENABLED === "true");
+    const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
 
     const validFor = [
         { hours: 24, name: "1 day" },
@@ -205,7 +205,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
                                             )}
                                         />
 
-                                        {env.EMAIL_ENABLED === "true" && (
+                                        {env.email.emailEnabled && (
                                             <div className="flex items-center space-x-2">
                                                 <Checkbox
                                                     id="send-email"

+ 3 - 2
src/app/[orgId]/settings/access/users/UsersDataTable.tsx

@@ -15,6 +15,7 @@ import {
     Table,
     TableBody,
     TableCell,
+    TableContainer,
     TableHead,
     TableHeader,
     TableRow,
@@ -88,7 +89,7 @@ export function UsersDataTable<TData, TValue>({
                     <Plus className="mr-2 h-4 w-4" /> Invite User
                 </Button>
             </div>
-            <div className="border rounded-md">
+            <TableContainer>
                 <Table>
                     <TableHeader>
                         {table.getHeaderGroups().map((headerGroup) => (
@@ -141,7 +142,7 @@ export function UsersDataTable<TData, TValue>({
                         )}
                     </TableBody>
                 </Table>
-            </div>
+            </TableContainer>
             <div className="mt-4">
                 <DataTablePagination table={table} />
             </div>

+ 2 - 2
src/app/[orgId]/settings/access/users/UsersTable.tsx

@@ -159,7 +159,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
                 const userRow = row.original;
 
                 return (
-                    <div className="flex flex-row items-center gap-1">
+                    <div className="flex flex-row items-center gap-2">
                         {userRow.isOwner && (
                             <Crown className="w-4 h-4 text-yellow-600" />
                         )}
@@ -186,7 +186,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
                             <Link
                                 href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
                             >
-                                <Button variant={"gray"} className="ml-2">
+                                <Button variant={"outline"} className="ml-2">
                                     Manage
                                     <ArrowRight className="ml-2 w-4 h-4" />
                                 </Button>

+ 86 - 66
src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx

@@ -6,7 +6,7 @@ import {
     FormField,
     FormItem,
     FormLabel,
-    FormMessage,
+    FormMessage
 } from "@app/components/ui/form";
 import { Input } from "@app/components/ui/input";
 import {
@@ -14,7 +14,7 @@ import {
     SelectContent,
     SelectItem,
     SelectTrigger,
-    SelectValue,
+    SelectValue
 } from "@app/components/ui/select";
 import { useToast } from "@app/hooks/useToast";
 import { zodResolver } from "@hookform/resolvers/zod";
@@ -27,14 +27,23 @@ import { ListRolesResponse } from "@server/routers/role";
 import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
 import { useParams } from "next/navigation";
 import { Button } from "@app/components/ui/button";
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
-import { formatAxiosError } from "@app/lib/api";;
+import {
+    SettingsContainer,
+    SettingsSection,
+    SettingsSectionHeader,
+    SettingsSectionTitle,
+    SettingsSectionDescription,
+    SettingsSectionBody,
+    SettingsSectionForm,
+    SettingsSectionFooter
+} from "@app/components/Settings";
+import { formatAxiosError } from "@app/lib/api";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 
 const formSchema = z.object({
     email: z.string().email({ message: "Please enter a valid email" }),
-    roleId: z.string().min(1, { message: "Please select a role" }),
+    roleId: z.string().min(1, { message: "Please select a role" })
 });
 
 export default function AccessControlsPage() {
@@ -52,8 +61,8 @@ export default function AccessControlsPage() {
         resolver: zodResolver(formSchema),
         defaultValues: {
             email: user.email!,
-            roleId: user.roleId?.toString(),
-        },
+            roleId: user.roleId?.toString()
+        }
     });
 
     useEffect(() => {
@@ -68,7 +77,7 @@ export default function AccessControlsPage() {
                         description: formatAxiosError(
                             e,
                             "An error occurred while fetching the roles"
-                        ),
+                        )
                     });
                 });
 
@@ -86,9 +95,9 @@ export default function AccessControlsPage() {
         setLoading(true);
 
         const res = await api
-            .post<AxiosResponse<InviteUserResponse>>(
-                `/role/${values.roleId}/add/${user.userId}`
-            )
+            .post<
+                AxiosResponse<InviteUserResponse>
+            >(`/role/${values.roleId}/add/${user.userId}`)
             .catch((e) => {
                 toast({
                     variant: "destructive",
@@ -96,7 +105,7 @@ export default function AccessControlsPage() {
                     description: formatAxiosError(
                         e,
                         "An error occurred while adding user to the role."
-                    ),
+                    )
                 });
             });
 
@@ -104,7 +113,7 @@ export default function AccessControlsPage() {
             toast({
                 variant: "default",
                 title: "User saved",
-                description: "The user has been updated.",
+                description: "The user has been updated."
             });
         }
 
@@ -112,59 +121,70 @@ export default function AccessControlsPage() {
     }
 
     return (
-        <>
-            <div className="space-y-8">
-                <SettingsSectionTitle
-                    title="Access Controls"
-                    description="Manage what this user can access and do in the organization"
-                    size="1xl"
-                />
-
-                <Form {...form}>
-                    <form
-                        onSubmit={form.handleSubmit(onSubmit)}
-                        className="space-y-4"
+        <SettingsContainer>
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>Access Controls</SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Manage what this user can access and do in the
+                        organization
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+
+                <SettingsSectionBody>
+                    <SettingsSectionForm>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="access-controls-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="roleId"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Role</FormLabel>
+                                            <Select
+                                                onValueChange={field.onChange}
+                                                value={field.value}
+                                            >
+                                                <FormControl>
+                                                    <SelectTrigger>
+                                                        <SelectValue placeholder="Select role" />
+                                                    </SelectTrigger>
+                                                </FormControl>
+                                                <SelectContent>
+                                                    {roles.map((role) => (
+                                                        <SelectItem
+                                                            key={role.roleId}
+                                                            value={role.roleId.toString()}
+                                                        >
+                                                            {role.name}
+                                                        </SelectItem>
+                                                    ))}
+                                                </SelectContent>
+                                            </Select>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </SettingsSectionForm>
+                </SettingsSectionBody>
+
+                <SettingsSectionFooter>
+                    <Button
+                        type="submit"
+                        loading={loading}
+                        disabled={loading}
+                        form="access-controls-form"
                     >
-                        <FormField
-                            control={form.control}
-                            name="roleId"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Role</FormLabel>
-                                    <Select
-                                        onValueChange={field.onChange}
-                                        value={field.value}
-                                    >
-                                        <FormControl>
-                                            <SelectTrigger>
-                                                <SelectValue placeholder="Select role" />
-                                            </SelectTrigger>
-                                        </FormControl>
-                                        <SelectContent>
-                                            {roles.map((role) => (
-                                                <SelectItem
-                                                    key={role.roleId}
-                                                    value={role.roleId.toString()}
-                                                >
-                                                    {role.name}
-                                                </SelectItem>
-                                            ))}
-                                        </SelectContent>
-                                    </Select>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
-                        <Button
-                            type="submit"
-                            loading={loading}
-                            disabled={loading}
-                        >
-                            Save Changes
-                        </Button>
-                    </form>
-                </Form>
-            </div>
-        </>
+                        Save Access Controls
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
+        </SettingsContainer>
     );
 }

+ 82 - 59
src/app/[orgId]/settings/general/page.tsx

@@ -21,7 +21,7 @@ import { useForm } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
-import { formatAxiosError } from "@app/lib/api";;
+import { formatAxiosError } from "@app/lib/api";
 import { AlertTriangle, Trash2 } from "lucide-react";
 import {
     Card,
@@ -33,6 +33,16 @@ import {
 import { AxiosResponse } from "axios";
 import { DeleteOrgResponse, ListOrgsResponse } from "@server/routers/org";
 import { redirect, useRouter } from "next/navigation";
+import {
+    SettingsContainer,
+    SettingsSection,
+    SettingsSectionHeader,
+    SettingsSectionTitle,
+    SettingsSectionDescription,
+    SettingsSectionBody,
+    SettingsSectionForm,
+    SettingsSectionFooter
+} from "@app/components/Settings";
 
 const GeneralFormSchema = z.object({
     name: z.string()
@@ -80,10 +90,7 @@ export default function GeneralPage() {
 
     async function pickNewOrgAndNavigate() {
         try {
-
-            const res = await api.get<AxiosResponse<ListOrgsResponse>>(
-                `/orgs`
-            );
+            const res = await api.get<AxiosResponse<ListOrgsResponse>>(`/orgs`);
 
             if (res.status === 200) {
                 if (res.data.data.orgs.length > 0) {
@@ -126,7 +133,7 @@ export default function GeneralPage() {
     }
 
     return (
-        <>
+        <SettingsContainer>
             <ConfirmDeleteDialog
                 open={isDeleteModalOpen}
                 setOpen={(val) => {
@@ -138,12 +145,10 @@ export default function GeneralPage() {
                             Are you sure you want to delete the organization{" "}
                             <b>{org?.org.name}?</b>
                         </p>
-
                         <p className="mb-2">
                             This action is irreversible and will delete all
                             associated data.
                         </p>
-
                         <p>
                             To confirm, type the name of the organization below.
                         </p>
@@ -155,57 +160,75 @@ export default function GeneralPage() {
                 title="Delete Organization"
             />
 
-            <section className="space-y-8 max-w-lg">
-                <Form {...form}>
-                    <form
-                        onSubmit={form.handleSubmit(onSubmit)}
-                        className="space-y-4"
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        Organization Settings
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Manage your organization details and configuration
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+
+                <SettingsSectionBody>
+                    <SettingsSectionForm>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="org-settings-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="name"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Name</FormLabel>
+                                            <FormControl>
+                                                <Input {...field} />
+                                            </FormControl>
+                                            <FormDescription>
+                                                This is the display name of the
+                                                org
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </SettingsSectionForm>
+                </SettingsSectionBody>
+
+                <SettingsSectionFooter>
+                    <Button type="submit" form="org-settings-form">
+                        Save Settings
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
+
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        <AlertTriangle className="h-5 w-5" />
+                        Danger Zone
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Once you delete this org, there is no going back. Please
+                        be certain.
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+
+                <SettingsSectionFooter>
+                    <Button
+                        variant="destructive"
+                        onClick={() => setIsDeleteModalOpen(true)}
+                        className="flex items-center gap-2"
                     >
-                        <FormField
-                            control={form.control}
-                            name="name"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Name</FormLabel>
-                                    <FormControl>
-                                        <Input {...field} />
-                                    </FormControl>
-                                    <FormDescription>
-                                        This is the display name of the org
-                                    </FormDescription>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
-                        <Button type="submit">Save Changes</Button>
-                    </form>
-                </Form>
-
-                <Card>
-                    <CardHeader>
-                        <CardTitle className="flex items-center gap-2 text-red-600">
-                            <AlertTriangle className="h-5 w-5" />
-                            Danger Zone
-                        </CardTitle>
-                    </CardHeader>
-                    <CardContent>
-                        <p className="text-sm">
-                            Once you delete this org, there is no going back.
-                            Please be certain.
-                        </p>
-                    </CardContent>
-                    <CardFooter className="flex justify-end gap-2">
-                        <Button
-                            variant="destructive"
-                            onClick={() => setIsDeleteModalOpen(true)}
-                            className="flex items-center gap-2"
-                        >
-                            <Trash2 className="h-4 w-4" />
-                            Delete Organization Data
-                        </Button>
-                    </CardFooter>
-                </Card>
-            </section>
-        </>
+                        Delete Organization Data
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
+        </SettingsContainer>
     );
 }

+ 2 - 2
src/app/[orgId]/settings/layout.tsx

@@ -36,7 +36,7 @@ const topNavItems = [
         icon: <Users className="h-4 w-4" />
     },
     {
-        title: "Sharable Links",
+        title: "Shareable Links",
         href: "/{orgId}/settings/share-links",
         icon: <Link className="h-4 w-4" />
     },
@@ -95,7 +95,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
 
     return (
         <>
-            <div className="w-full border-b bg-neutral-100 dark:bg-neutral-800 select-none sm:px-0 px-3 fixed top-0 z-10">
+            <div className="w-full border-b bg-card select-none sm:px-0 px-3 fixed top-0 z-10">
                 <div className="container mx-auto flex flex-col content-between">
                     <div className="my-4">
                         <UserProvider user={user}>

+ 1 - 1
src/app/[orgId]/settings/page.tsx

@@ -6,7 +6,7 @@ type OrgPageProps = {
 
 export default async function SettingsPage(props: OrgPageProps) {
     const params = await props.params;
-    redirect(`/${params.orgId}/settings/resources`);
+    redirect(`/${params.orgId}/settings/sites`);
 
     return <></>;
 }

+ 3 - 2
src/app/[orgId]/settings/resources/ResourcesDataTable.tsx

@@ -16,6 +16,7 @@ import {
     Table,
     TableBody,
     TableCell,
+    TableContainer,
     TableHead,
     TableHeader,
     TableRow,
@@ -89,7 +90,7 @@ export function ResourcesDataTable<TData, TValue>({
                     <Plus className="mr-2 h-4 w-4" /> Add Resource
                 </Button>
             </div>
-            <div className="border rounded-md">
+            <TableContainer>
                 <Table>
                     <TableHeader>
                         {table.getHeaderGroups().map((headerGroup) => (
@@ -141,7 +142,7 @@ export function ResourcesDataTable<TData, TValue>({
                         )}
                     </TableBody>
                 </Table>
-            </div>
+            </TableContainer>
             <div className="mt-4">
                 <DataTablePagination table={table} />
             </div>

+ 68 - 0
src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx

@@ -0,0 +1,68 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Server, Lock, Key, Users, X, ArrowRight } from "lucide-react"; // Replace with actual imports
+import { Card, CardContent } from "@app/components/ui/card";
+import { Button } from "@app/components/ui/button";
+
+export const ResourcesSplashCard = () => {
+    const [isDismissed, setIsDismissed] = useState(false);
+
+    const key = "resources-splash-dismissed";
+
+    useEffect(() => {
+        const dismissed = localStorage.getItem(key);
+        if (dismissed === "true") {
+            setIsDismissed(true);
+        }
+    }, []);
+
+    const handleDismiss = () => {
+        setIsDismissed(true);
+        localStorage.setItem(key, "true");
+    };
+
+    if (isDismissed) {
+        return null;
+    }
+
+    return (
+        <Card className="w-full mx-auto overflow-hidden mb-8 hidden md:block relative">
+            <button
+                onClick={handleDismiss}
+                className="absolute top-2 right-2 p-2"
+                aria-label="Dismiss"
+            >
+                <X className="w-5 h-5" />
+            </button>
+            <CardContent className="grid gap-6 p-6">
+                <div className="space-y-4">
+                    <h3 className="text-xl font-semibold flex items-center gap-2">
+                        <Server className="text-blue-500" />
+                        Resources
+                    </h3>
+                    <p className="text-sm">
+                        Resources are proxies to applications running on your private network. Create a resource for any HTTP or HTTPS app on your private network.
+                        Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel.
+                    </p>
+                    <ul className="text-sm text-muted-foreground space-y-2">
+                        <li className="flex items-center gap-2">
+                            <Lock className="text-green-500 w-4 h-4" />
+                            Secure connectivity with WireGuard encryption
+                        </li>
+                        <li className="flex items-center gap-2">
+                            <Key className="text-yellow-500 w-4 h-4" />
+                            Configure multiple authentication methods
+                        </li>
+                        <li className="flex items-center gap-2">
+                            <Users className="text-purple-500 w-4 h-4" />
+                            User and role-based access control
+                        </li>
+                    </ul>
+                </div>
+            </CardContent>
+        </Card>
+    );
+};
+
+export default ResourcesSplashCard;

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

@@ -210,7 +210,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
                         <Link
                             href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
                         >
-                            <Button variant={"gray"} className="ml-2">
+                            <Button variant={"outline"} className="ml-2">
                                 Edit
                                 <ArrowRight className="ml-2 w-4 h-4" />
                             </Button>

+ 1 - 3
src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx

@@ -60,9 +60,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
                                 <div className="flex items-center space-x-2 text-yellow-500">
                                     <ShieldOff className="w-4 h-4" />
                                     <span>
-                                        This resource is not protected with any
-                                        auth method. Anyone can access this
-                                        resource.
+                                        Anyone can access this resource.
                                     </span>
                                 </div>
                             )}

+ 318 - 309
src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx

@@ -28,16 +28,26 @@ import {
     FormMessage
 } from "@app/components/ui/form";
 import { TagInput } from "emblor";
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+// import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
 import { ListUsersResponse } from "@server/routers/user";
 import { Switch } from "@app/components/ui/switch";
 import { Label } from "@app/components/ui/label";
 import { Binary, Key, ShieldCheck } from "lucide-react";
 import SetResourcePasswordForm from "./SetResourcePasswordForm";
-import { Separator } from "@app/components/ui/separator";
 import SetResourcePincodeForm from "./SetResourcePincodeForm";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
+import {
+    SettingsContainer,
+    SettingsSection,
+    SettingsSectionTitle,
+    SettingsSectionHeader,
+    SettingsSectionDescription,
+    SettingsSectionBody,
+    SettingsSectionForm,
+    SettingsSectionFooter
+} from "@app/components/Settings";
+import { SwitchInput } from "@app/components/SwitchInput";
 
 const UsersRolesFormSchema = z.object({
     roles: z.array(
@@ -382,328 +392,145 @@ export default function ResourceAuthenticationPage() {
                 />
             )}
 
-            <div className="space-y-12">
-                <section className="space-y-4 lg:max-w-2xl">
-                    <SettingsSectionTitle
-                        title="Users & Roles"
-                        description="Configure which users and roles can visit this resource"
-                        size="1xl"
-                    />
-
-                    <div>
-                        <div className="flex items-center space-x-2 mb-2">
-                            <Switch
-                                id="sso-toggle"
-                                defaultChecked={resource.sso}
-                                onCheckedChange={(val) => setSsoEnabled(val)}
-                            />
-                            <Label htmlFor="sso-toggle">Use Platform SSO</Label>
-                        </div>
-                        <span className="text-muted-foreground text-sm">
-                            Existing users will only have to login once for all
-                            resources that have this enabled.
-                        </span>
-                    </div>
-
-                    <Form {...usersRolesForm}>
-                        <form
-                            onSubmit={usersRolesForm.handleSubmit(
-                                onSubmitUsersRoles
-                            )}
-                            className="space-y-4"
-                        >
-                            {ssoEnabled && (
-                                <>
-                                    <FormField
-                                        control={usersRolesForm.control}
-                                        name="roles"
-                                        render={({ field }) => (
-                                            <FormItem className="flex flex-col items-start">
-                                                <FormLabel>Roles</FormLabel>
-                                                <FormControl>
-                                                    {/* @ts-ignore */}
-                                                    <TagInput
-                                                        {...field}
-                                                        activeTagIndex={
-                                                            activeRolesTagIndex
-                                                        }
-                                                        setActiveTagIndex={
-                                                            setActiveRolesTagIndex
-                                                        }
-                                                        placeholder="Enter a role"
-                                                        tags={
-                                                            usersRolesForm.getValues()
-                                                                .roles
-                                                        }
-                                                        setTags={(newRoles) => {
-                                                            usersRolesForm.setValue(
-                                                                "roles",
-                                                                newRoles as [
-                                                                    Tag,
-                                                                    ...Tag[]
-                                                                ]
-                                                            );
-                                                        }}
-                                                        enableAutocomplete={
-                                                            true
-                                                        }
-                                                        autocompleteOptions={
-                                                            allRoles
-                                                        }
-                                                        allowDuplicates={false}
-                                                        restrictTagsToAutocompleteOptions={
-                                                            true
-                                                        }
-                                                        sortTags={true}
-                                                        styleClasses={{
-                                                            tag: {
-                                                                body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
-                                                            },
-                                                            input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
-                                                            inlineTagsContainer:
-                                                                "bg-transparent p-2"
-                                                        }}
-                                                    />
-                                                </FormControl>
-                                                <FormDescription>
-                                                    These roles will be able to
-                                                    access this resource. Admins
-                                                    can always access this
-                                                    resource.
-                                                </FormDescription>
-                                                <FormMessage />
-                                            </FormItem>
-                                        )}
-                                    />
-                                    <FormField
-                                        control={usersRolesForm.control}
-                                        name="users"
-                                        render={({ field }) => (
-                                            <FormItem className="flex flex-col items-start">
-                                                <FormLabel>Users</FormLabel>
-                                                <FormControl>
-                                                    {/* @ts-ignore */}
-                                                    <TagInput
-                                                        {...field}
-                                                        activeTagIndex={
-                                                            activeUsersTagIndex
-                                                        }
-                                                        setActiveTagIndex={
-                                                            setActiveUsersTagIndex
-                                                        }
-                                                        placeholder="Enter a user"
-                                                        tags={
-                                                            usersRolesForm.getValues()
-                                                                .users
-                                                        }
-                                                        setTags={(newUsers) => {
-                                                            usersRolesForm.setValue(
-                                                                "users",
-                                                                newUsers as [
-                                                                    Tag,
-                                                                    ...Tag[]
-                                                                ]
-                                                            );
-                                                        }}
-                                                        enableAutocomplete={
-                                                            true
-                                                        }
-                                                        autocompleteOptions={
-                                                            allUsers
-                                                        }
-                                                        allowDuplicates={false}
-                                                        restrictTagsToAutocompleteOptions={
-                                                            true
-                                                        }
-                                                        sortTags={true}
-                                                        styleClasses={{
-                                                            tag: {
-                                                                body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
-                                                            },
-                                                            input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
-                                                            inlineTagsContainer:
-                                                                "bg-transparent p-2"
-                                                        }}
-                                                    />
-                                                </FormControl>
-                                                <FormDescription>
-                                                    Users added here will be
-                                                    able to access this
-                                                    resource. A user will always
-                                                    have access to a resource if
-                                                    they have a role that has
-                                                    access to it.
-                                                </FormDescription>
-                                                <FormMessage />
-                                            </FormItem>
-                                        )}
-                                    />
-                                </>
-                            )}
-                            <Button
-                                type="submit"
-                                loading={loadingSaveUsersRoles}
-                                disabled={loadingSaveUsersRoles}
-                            >
-                                Save Users & Roles
-                            </Button>
-                        </form>
-                    </Form>
-                </section>
-
-                <Separator />
-
-                <section className="space-y-4 lg:max-w-2xl">
-                    <SettingsSectionTitle
-                        title="Authentication Methods"
-                        description="Allow access to the resource via additional auth methods"
-                        size="1xl"
-                    />
-
-                    <div className="flex flex-col space-y-4">
-                        <div className="flex items-center justify-between space-x-4">
-                            <div
-                                className={`flex items-center text-${!authInfo.password ? "red" : "green"}-500 space-x-2`}
-                            >
-                                <Key />
-                                <span>
-                                    Password Protection{" "}
-                                    {authInfo?.password
-                                        ? "Enabled"
-                                        : "Disabled"}
-                                </span>
-                            </div>
-                            {authInfo?.password ? (
-                                <Button
-                                    variant="gray"
-                                    type="button"
-                                    loading={loadingRemoveResourcePassword}
-                                    disabled={loadingRemoveResourcePassword}
-                                    onClick={removeResourcePassword}
-                                >
-                                    Remove Password
-                                </Button>
-                            ) : (
-                                <Button
-                                    variant="gray"
-                                    type="button"
-                                    onClick={() => setIsSetPasswordOpen(true)}
-                                >
-                                    Add Password
-                                </Button>
-                            )}
-                        </div>
-
-                        <div className="flex items-center justify-between space-x-4">
-                            <div
-                                className={`flex items-center text-${!authInfo.pincode ? "red" : "green"}-500 space-x-2`}
+            <SettingsContainer>
+                <SettingsSection>
+                    <SettingsSectionHeader>
+                        <SettingsSectionTitle>
+                            Users & Roles
+                        </SettingsSectionTitle>
+                        <SettingsSectionDescription>
+                            Configure which users and roles can visit this
+                            resource
+                        </SettingsSectionDescription>
+                    </SettingsSectionHeader>
+                    <SettingsSectionBody>
+                        <SwitchInput
+                            id="sso-toggle"
+                            label="Use Platform SSO"
+                            description="Existing users will only have to login once for all resources that have this enabled."
+                            defaultChecked={resource.sso}
+                            onCheckedChange={(val) => setSsoEnabled(val)}
+                        />
+
+                        <Form {...usersRolesForm}>
+                            <form
+                                onSubmit={usersRolesForm.handleSubmit(
+                                    onSubmitUsersRoles
+                                )}
+                                id="users-roles-form"
+                                className="space-y-4"
                             >
-                                <Binary />
-                                <span>
-                                    PIN Code Protection{" "}
-                                    {authInfo?.pincode ? "Enabled" : "Disabled"}
-                                </span>
-                            </div>
-                            {authInfo?.pincode ? (
-                                <Button
-                                    variant="gray"
-                                    type="button"
-                                    loading={loadingRemoveResourcePincode}
-                                    disabled={loadingRemoveResourcePincode}
-                                    onClick={removeResourcePincode}
-                                >
-                                    Remove PIN Code
-                                </Button>
-                            ) : (
-                                <Button
-                                    variant="gray"
-                                    type="button"
-                                    onClick={() => setIsSetPincodeOpen(true)}
-                                >
-                                    Add PIN Code
-                                </Button>
-                            )}
-                        </div>
-                    </div>
-                </section>
-
-                <Separator />
-
-                <section className="space-y-4 lg:max-w-2xl">
-                    {env.EMAIL_ENABLED === "true" && (
-                        <>
-                            <div>
-                                <div className="flex items-center space-x-2 mb-2">
-                                    <Switch
-                                        id="whitelist-toggle"
-                                        defaultChecked={
-                                            resource.emailWhitelistEnabled
-                                        }
-                                        onCheckedChange={(val) =>
-                                            setWhitelistEnabled(val)
-                                        }
-                                    />
-                                    <Label htmlFor="whitelist-toggle">
-                                        Email Whitelist
-                                    </Label>
-                                </div>
-                                <span className="text-muted-foreground text-sm">
-                                    Enable resource whitelist to require
-                                    email-based authentication (one-time
-                                    passwords) for resource access.
-                                </span>
-                            </div>
-
-                            {whitelistEnabled && (
-                                <Form {...whitelistForm}>
-                                    <form className="space-y-4">
+                                {ssoEnabled && (
+                                    <>
                                         <FormField
-                                            control={whitelistForm.control}
-                                            name="emails"
+                                            control={usersRolesForm.control}
+                                            name="roles"
                                             render={({ field }) => (
                                                 <FormItem className="flex flex-col items-start">
-                                                    <FormLabel>
-                                                        Whitelisted Emails
-                                                    </FormLabel>
+                                                    <FormLabel>Roles</FormLabel>
                                                     <FormControl>
                                                         {/* @ts-ignore */}
                                                         <TagInput
                                                             {...field}
                                                             activeTagIndex={
-                                                                activeEmailTagIndex
+                                                                activeRolesTagIndex
                                                             }
-                                                            validateTag={(
-                                                                tag
-                                                            ) => {
-                                                                return z
-                                                                    .string()
-                                                                    .email()
-                                                                    .safeParse(
-                                                                        tag
-                                                                    ).success;
-                                                            }}
                                                             setActiveTagIndex={
-                                                                setActiveEmailTagIndex
+                                                                setActiveRolesTagIndex
                                                             }
-                                                            placeholder="Enter an email"
+                                                            placeholder="Enter a role"
                                                             tags={
-                                                                whitelistForm.getValues()
-                                                                    .emails
+                                                                usersRolesForm.getValues()
+                                                                    .roles
                                                             }
                                                             setTags={(
                                                                 newRoles
                                                             ) => {
-                                                                whitelistForm.setValue(
-                                                                    "emails",
+                                                                usersRolesForm.setValue(
+                                                                    "roles",
                                                                     newRoles as [
                                                                         Tag,
                                                                         ...Tag[]
                                                                     ]
                                                                 );
                                                             }}
+                                                            enableAutocomplete={
+                                                                true
+                                                            }
+                                                            autocompleteOptions={
+                                                                allRoles
+                                                            }
+                                                            allowDuplicates={
+                                                                false
+                                                            }
+                                                            restrictTagsToAutocompleteOptions={
+                                                                true
+                                                            }
+                                                            sortTags={true}
+                                                            styleClasses={{
+                                                                tag: {
+                                                                    body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
+                                                                },
+                                                                input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
+                                                                inlineTagsContainer:
+                                                                    "bg-transparent p-2"
+                                                            }}
+                                                        />
+                                                    </FormControl>
+                                                    <FormDescription>
+                                                        These roles will be able
+                                                        to access this resource.
+                                                        Admins can always access
+                                                        this resource.
+                                                    </FormDescription>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+                                        <FormField
+                                            control={usersRolesForm.control}
+                                            name="users"
+                                            render={({ field }) => (
+                                                <FormItem className="flex flex-col items-start">
+                                                    <FormLabel>Users</FormLabel>
+                                                    <FormControl>
+                                                        {/* @ts-ignore */}
+                                                        <TagInput
+                                                            {...field}
+                                                            activeTagIndex={
+                                                                activeUsersTagIndex
+                                                            }
+                                                            setActiveTagIndex={
+                                                                setActiveUsersTagIndex
+                                                            }
+                                                            placeholder="Enter a user"
+                                                            tags={
+                                                                usersRolesForm.getValues()
+                                                                    .users
+                                                            }
+                                                            setTags={(
+                                                                newUsers
+                                                            ) => {
+                                                                usersRolesForm.setValue(
+                                                                    "users",
+                                                                    newUsers as [
+                                                                        Tag,
+                                                                        ...Tag[]
+                                                                    ]
+                                                                );
+                                                            }}
+                                                            enableAutocomplete={
+                                                                true
+                                                            }
+                                                            autocompleteOptions={
+                                                                allUsers
+                                                            }
                                                             allowDuplicates={
                                                                 false
                                                             }
+                                                            restrictTagsToAutocompleteOptions={
+                                                                true
+                                                            }
                                                             sortTags={true}
                                                             styleClasses={{
                                                                 tag: {
@@ -715,24 +542,206 @@ export default function ResourceAuthenticationPage() {
                                                             }}
                                                         />
                                                     </FormControl>
+                                                    <FormDescription>
+                                                        Users added here will be
+                                                        able to access this
+                                                        resource. A user will
+                                                        always have access to a
+                                                        resource if they have a
+                                                        role that has access to
+                                                        it.
+                                                    </FormDescription>
+                                                    <FormMessage />
                                                 </FormItem>
                                             )}
                                         />
-                                    </form>
-                                </Form>
-                            )}
+                                    </>
+                                )}
+                            </form>
+                        </Form>
+                    </SettingsSectionBody>
+                    <SettingsSectionFooter>
+                        <Button
+                            type="submit"
+                            loading={loadingSaveUsersRoles}
+                            disabled={loadingSaveUsersRoles}
+                            form="users-roles-form"
+                        >
+                            Save Users & Roles
+                        </Button>
+                    </SettingsSectionFooter>
+                </SettingsSection>
+
+                <SettingsSection>
+                    <SettingsSectionHeader>
+                        <SettingsSectionTitle>
+                            Authentication Methods
+                        </SettingsSectionTitle>
+                        <SettingsSectionDescription>
+                            Allow access to the resource via additional auth
+                            methods
+                        </SettingsSectionDescription>
+                    </SettingsSectionHeader>
+                    <SettingsSectionBody>
+                        {/* Password Protection */}
+                        <div className="flex items-center justify-between">
+                            <div
+                                className={`flex items-center text-${!authInfo.password ? "neutral" : "green"}-500 space-x-2`}
+                            >
+                                <Key />
+                                <span>
+                                    Password Protection{" "}
+                                    {authInfo.password ? "Enabled" : "Disabled"}
+                                </span>
+                            </div>
+                            <Button
+                                variant="outline"
+                                onClick={
+                                    authInfo.password
+                                        ? removeResourcePassword
+                                        : () => setIsSetPasswordOpen(true)
+                                }
+                                loading={loadingRemoveResourcePassword}
+                            >
+                                {authInfo.password
+                                    ? "Remove Password"
+                                    : "Add Password"}
+                            </Button>
+                        </div>
 
+                        {/* PIN Code Protection */}
+                        <div className="flex items-center justify-between">
+                            <div
+                                className={`flex items-center text-${!authInfo.pincode ? "neutral" : "green"}-500 space-x-2`}
+                            >
+                                <Binary />
+                                <span>
+                                    PIN Code Protection{" "}
+                                    {authInfo.pincode ? "Enabled" : "Disabled"}
+                                </span>
+                            </div>
                             <Button
-                                loading={loadingSaveWhitelist}
-                                disabled={loadingSaveWhitelist}
-                                onClick={saveWhitelist}
+                                variant="outline"
+                                onClick={
+                                    authInfo.pincode
+                                        ? removeResourcePincode
+                                        : () => setIsSetPincodeOpen(true)
+                                }
+                                loading={loadingRemoveResourcePincode}
                             >
-                                Save Whitelist
+                                {authInfo.pincode
+                                    ? "Remove PIN Code"
+                                    : "Add PIN Code"}
                             </Button>
-                        </>
-                    )}
-                </section>
-            </div>
+                        </div>
+                    </SettingsSectionBody>
+                </SettingsSection>
+
+                <SettingsSection>
+                    <SettingsSectionHeader>
+                        <SettingsSectionTitle>
+                            One-time Passwords
+                        </SettingsSectionTitle>
+                        <SettingsSectionDescription>
+                            Require email-based authentication for resource
+                            access
+                        </SettingsSectionDescription>
+                    </SettingsSectionHeader>
+                    <SettingsSectionBody>
+                        {env.email.emailEnabled && (
+                            <>
+                                <SwitchInput
+                                    id="whitelist-toggle"
+                                    label="Email Whitelist"
+                                    defaultChecked={
+                                        resource.emailWhitelistEnabled
+                                    }
+                                    onCheckedChange={setWhitelistEnabled}
+                                />
+
+                                {whitelistEnabled && (
+                                    <Form {...whitelistForm}>
+                                        <form id="whitelist-form">
+                                            <FormField
+                                                control={whitelistForm.control}
+                                                name="emails"
+                                                render={({ field }) => (
+                                                    <FormItem>
+                                                        <FormLabel>
+                                                            Whitelisted Emails
+                                                        </FormLabel>
+                                                        <FormControl>
+                                                            {/* @ts-ignore */}
+                                                            {/* @ts-ignore */}
+                                                            <TagInput
+                                                                {...field}
+                                                                activeTagIndex={
+                                                                    activeEmailTagIndex
+                                                                }
+                                                                validateTag={(
+                                                                    tag
+                                                                ) => {
+                                                                    return z
+                                                                        .string()
+                                                                        .email()
+                                                                        .safeParse(
+                                                                            tag
+                                                                        )
+                                                                        .success;
+                                                                }}
+                                                                setActiveTagIndex={
+                                                                    setActiveEmailTagIndex
+                                                                }
+                                                                placeholder="Enter an email"
+                                                                tags={
+                                                                    whitelistForm.getValues()
+                                                                        .emails
+                                                                }
+                                                                setTags={(
+                                                                    newRoles
+                                                                ) => {
+                                                                    whitelistForm.setValue(
+                                                                        "emails",
+                                                                        newRoles as [
+                                                                            Tag,
+                                                                            ...Tag[]
+                                                                        ]
+                                                                    );
+                                                                }}
+                                                                allowDuplicates={
+                                                                    false
+                                                                }
+                                                                sortTags={true}
+                                                                styleClasses={{
+                                                                    tag: {
+                                                                        body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
+                                                                    },
+                                                                    input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
+                                                                    inlineTagsContainer:
+                                                                        "bg-transparent p-2"
+                                                                }}
+                                                            />
+                                                        </FormControl>
+                                                    </FormItem>
+                                                )}
+                                            />
+                                        </form>
+                                    </Form>
+                                )}
+                            </>
+                        )}
+                    </SettingsSectionBody>
+                    <SettingsSectionFooter>
+                        <Button
+                            onClick={saveWhitelist}
+                            form="whitelist-form"
+                            loading={loadingSaveWhitelist}
+                        >
+                            Save Whitelist
+                        </Button>
+                    </SettingsSectionFooter>
+                </SettingsSection>
+            </SettingsContainer>
         </>
     );
 }

+ 182 - 227
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -40,28 +40,34 @@ import {
     Table,
     TableBody,
     TableCell,
+    TableContainer,
     TableHead,
     TableHeader,
     TableRow
 } from "@app/components/ui/table";
 import { useToast } from "@app/hooks/useToast";
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { ArrayElement } from "@server/types/ArrayElement";
-import { formatAxiosError } from "@app/lib/api/formatAxiosError";;
+import { formatAxiosError } from "@app/lib/api/formatAxiosError";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { createApiClient } from "@app/lib/api";
 import { GetSiteResponse } from "@server/routers/site";
+import {
+    SettingsContainer,
+    SettingsSection,
+    SettingsSectionHeader,
+    SettingsSectionTitle,
+    SettingsSectionDescription,
+    SettingsSectionBody,
+    SettingsSectionForm,
+    SettingsSectionFooter
+} from "@app/components/Settings";
+import { SwitchInput } from "@app/components/SwitchInput";
 
 const addTargetSchema = z.object({
     ip: z.string().ip(),
     method: z.string(),
-    port: z
-        .string()
-        .refine((val) => !isNaN(Number(val)), {
-            message: "Port must be a number"
-        })
-        .transform((val) => Number(val))
+    port: z.coerce.number().int().positive()
     // protocol: z.string(),
 });
 
@@ -99,7 +105,7 @@ export default function ReverseProxyTargets(props: {
         defaultValues: {
             ip: "",
             method: "http",
-            port: "80"
+            port: 80
             // protocol: "TCP",
         }
     });
@@ -154,7 +160,7 @@ export default function ReverseProxyTargets(props: {
         fetchSite();
     }, []);
 
-    async function addTarget(data: AddTargetFormValues) {
+    async function addTarget(data: z.infer<typeof addTargetSchema>) {
         // Check if target with same IP, port and method already exists
         const isDuplicate = targets.some(
             (target) =>
@@ -218,16 +224,10 @@ export default function ReverseProxyTargets(props: {
         );
     }
 
-    async function saveAll() {
+    async function saveTargets() {
         try {
             setLoading(true);
 
-            const res = await api.post(`/resource/${params.resourceId}`, {
-                ssl: sslEnabled
-            });
-
-            updateResource({ ssl: sslEnabled });
-
             for (let target of targets) {
                 const data = {
                     ip: target.ip,
@@ -269,8 +269,8 @@ export default function ReverseProxyTargets(props: {
             }
 
             toast({
-                title: "Resource updated",
-                description: "Resource and targets updated successfully"
+                title: "Targets updated",
+                description: "Targets updated successfully"
             });
 
             setTargetsToRemove([]);
@@ -289,6 +289,20 @@ export default function ReverseProxyTargets(props: {
         setLoading(false);
     }
 
+    async function saveSsl(val: boolean) {
+        const res = await api.post(`/resource/${params.resourceId}`, {
+            ssl: val
+        });
+
+        setSslEnabled(val);
+        updateResource({ ssl: sslEnabled });
+
+        toast({
+            title: "SSL Configuration",
+            description: "SSL configuration updated successfully"
+        });
+    }
+
     const columns: ColumnDef<LocalTarget>[] = [
         {
             accessorKey: "method",
@@ -410,239 +424,180 @@ export default function ReverseProxyTargets(props: {
     }
 
     return (
-        <>
-            <div className="space-y-12">
-                <section className="space-y-4">
-                    <SettingsSectionTitle
-                        title="SSL"
-                        description="Setup SSL to secure your connections with LetsEncrypt certificates"
-                        size="1xl"
+        <SettingsContainer>
+            {/* SSL Section */}
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        SSL Configuration
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Setup SSL to secure your connections with LetsEncrypt
+                        certificates
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+                <SettingsSectionBody>
+                    <SwitchInput
+                        id="ssl-toggle"
+                        label="Enable SSL (https)"
+                        defaultChecked={resource.ssl}
+                        onCheckedChange={async (val) => {
+                            await saveSsl(val);
+                        }}
                     />
-
-                    <div className="flex items-center space-x-2">
-                        <Switch
-                            id="ssl-toggle"
-                            defaultChecked={resource.ssl}
-                            onCheckedChange={(val) => setSslEnabled(val)}
-                        />
-                        <Label htmlFor="ssl-toggle">Enable SSL (https)</Label>
-                    </div>
-                </section>
-
-                <hr />
-
-                <section className="space-y-4">
-                    <SettingsSectionTitle
-                        title="Targets"
-                        description="Setup targets to route traffic to your services"
-                        size="1xl"
-                    />
-
-                    <div className="space-y-4">
-                        <Form {...addTargetForm}>
-                            <form
-                                onSubmit={addTargetForm.handleSubmit(
-                                    addTarget as any
-                                )}
-                                className="space-y-4"
-                            >
-                                <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
-                                    <FormField
-                                        control={addTargetForm.control}
-                                        name="method"
-                                        render={({ field }) => (
-                                            <FormItem>
-                                                <FormLabel>Method</FormLabel>
-                                                <FormControl>
-                                                    <Select
-                                                        {...field}
-                                                        onValueChange={(
-                                                            value
-                                                        ) => {
-                                                            addTargetForm.setValue(
-                                                                "method",
-                                                                value
-                                                            );
-                                                        }}
-                                                    >
-                                                        <SelectTrigger id="method">
-                                                            <SelectValue placeholder="Select method" />
-                                                        </SelectTrigger>
-                                                        <SelectContent>
-                                                            <SelectItem value="http">
-                                                                http
-                                                            </SelectItem>
-                                                            <SelectItem value="https">
-                                                                https
-                                                            </SelectItem>
-                                                        </SelectContent>
-                                                    </Select>
-                                                </FormControl>
-                                                {/* <FormDescription> */}
-                                                {/*     Choose the method for how */}
-                                                {/*     the target is accessed. */}
-                                                {/* </FormDescription> */}
-                                                <FormMessage />
-                                            </FormItem>
-                                        )}
-                                    />
-                                    <FormField
-                                        control={addTargetForm.control}
-                                        name="ip"
-                                        render={({ field }) => (
-                                            <FormItem>
-                                                <FormLabel>
-                                                    IP Address
-                                                </FormLabel>
-                                                <FormControl>
-                                                    <Input id="ip" {...field} />
-                                                </FormControl>
-                                                {/* <FormDescription> */}
-                                                {/*     Use the IP of the resource on your private network if using Newt, or the peer IP if using raw WireGuard. */}
-                                                {/* </FormDescription> */}
-                                                <FormMessage />
-                                            </FormItem>
-                                        )}
-                                    />
-                                    <FormField
-                                        control={addTargetForm.control}
-                                        name="port"
-                                        render={({ field }) => (
-                                            <FormItem>
-                                                <FormLabel>Port</FormLabel>
-                                                <FormControl>
-                                                    <Input
-                                                        id="port"
-                                                        type="number"
-                                                        {...field}
-                                                        required
-                                                    />
-                                                </FormControl>
-                                                {/* <FormDescription> */}
-                                                {/*     Specify the port number for */}
-                                                {/*     the target. */}
-                                                {/* </FormDescription> */}
-                                                <FormMessage />
-                                            </FormItem>
-                                        )}
-                                    />
-                                    {/* <FormField
+                </SettingsSectionBody>
+            </SettingsSection>
+
+            {/* Targets Section */}
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        Target Configuration
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Setup targets to route traffic to your services
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+                <SettingsSectionBody>
+                    <Form {...addTargetForm}>
+                        <form
+                            onSubmit={addTargetForm.handleSubmit(addTarget)}
+                            className="space-y-4"
+                        >
+                            <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
+                                <FormField
                                     control={addTargetForm.control}
-                                    name="protocol"
+                                    name="method"
                                     render={({ field }) => (
                                         <FormItem>
-                                            <FormLabel>Protocol</FormLabel>
+                                            <FormLabel>Method</FormLabel>
                                             <FormControl>
                                                 <Select
                                                     {...field}
                                                     onValueChange={(value) => {
                                                         addTargetForm.setValue(
-                                                            "protocol",
+                                                            "method",
                                                             value
                                                         );
                                                     }}
                                                 >
-                                                    <SelectTrigger id="protocol">
-                                                        <SelectValue placeholder="Select protocol" />
+                                                    <SelectTrigger id="method">
+                                                        <SelectValue placeholder="Select method" />
                                                     </SelectTrigger>
                                                     <SelectContent>
-                                                        <SelectItem value="UDP">
-                                                            UDP
+                                                        <SelectItem value="http">
+                                                            http
                                                         </SelectItem>
-                                                        <SelectItem value="TCP">
-                                                            TCP
+                                                        <SelectItem value="https">
+                                                            https
                                                         </SelectItem>
                                                     </SelectContent>
                                                 </Select>
                                             </FormControl>
-                                            <FormDescription>
-                                                Select the protocol used by the
-                                                target
-                                            </FormDescription>
                                             <FormMessage />
                                         </FormItem>
                                     )}
-                                /> */}
-                                </div>
-                                <Button type="submit" variant="gray">
-                                    Add Target
-                                </Button>
-                            </form>
-                        </Form>
-
-                        <div className="rounded-md border">
-                            <Table>
-                                <TableHeader>
-                                    {table
-                                        .getHeaderGroups()
-                                        .map((headerGroup) => (
-                                            <TableRow key={headerGroup.id}>
-                                                {headerGroup.headers.map(
-                                                    (header) => (
-                                                        <TableHead
-                                                            key={header.id}
-                                                        >
-                                                            {header.isPlaceholder
-                                                                ? null
-                                                                : flexRender(
-                                                                      header
-                                                                          .column
-                                                                          .columnDef
-                                                                          .header,
-                                                                      header.getContext()
-                                                                  )}
-                                                        </TableHead>
-                                                    )
-                                                )}
-                                            </TableRow>
+                                />
+                                <FormField
+                                    control={addTargetForm.control}
+                                    name="ip"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>IP Address</FormLabel>
+                                            <FormControl>
+                                                <Input id="ip" {...field} />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <FormField
+                                    control={addTargetForm.control}
+                                    name="port"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Port</FormLabel>
+                                            <FormControl>
+                                                <Input
+                                                    id="port"
+                                                    type="number"
+                                                    {...field}
+                                                    required
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </div>
+                            <Button type="submit" variant="outline">
+                                Add Target
+                            </Button>
+                        </form>
+                    </Form>
+
+                    <TableContainer>
+                        <Table>
+                            <TableHeader>
+                                {table.getHeaderGroups().map((headerGroup) => (
+                                    <TableRow key={headerGroup.id}>
+                                        {headerGroup.headers.map((header) => (
+                                            <TableHead key={header.id}>
+                                                {header.isPlaceholder
+                                                    ? null
+                                                    : flexRender(
+                                                          header.column
+                                                              .columnDef.header,
+                                                          header.getContext()
+                                                      )}
+                                            </TableHead>
                                         ))}
-                                </TableHeader>
-                                <TableBody>
-                                    {table.getRowModel().rows?.length ? (
-                                        table.getRowModel().rows.map((row) => (
-                                            <TableRow key={row.id}>
-                                                {row
-                                                    .getVisibleCells()
-                                                    .map((cell) => (
-                                                        <TableCell
-                                                            key={cell.id}
-                                                        >
-                                                            {flexRender(
-                                                                cell.column
-                                                                    .columnDef
-                                                                    .cell,
-                                                                cell.getContext()
-                                                            )}
-                                                        </TableCell>
-                                                    ))}
-                                            </TableRow>
-                                        ))
-                                    ) : (
-                                        <TableRow>
-                                            <TableCell
-                                                colSpan={columns.length}
-                                                className="h-24 text-center"
-                                            >
-                                                No targets. Add a target using
-                                                the form.
-                                            </TableCell>
+                                    </TableRow>
+                                ))}
+                            </TableHeader>
+                            <TableBody>
+                                {table.getRowModel().rows?.length ? (
+                                    table.getRowModel().rows.map((row) => (
+                                        <TableRow key={row.id}>
+                                            {row
+                                                .getVisibleCells()
+                                                .map((cell) => (
+                                                    <TableCell key={cell.id}>
+                                                        {flexRender(
+                                                            cell.column
+                                                                .columnDef.cell,
+                                                            cell.getContext()
+                                                        )}
+                                                    </TableCell>
+                                                ))}
                                         </TableRow>
-                                    )}
-                                </TableBody>
-                            </Table>
-                        </div>
-
-                        <Button
-                            onClick={saveAll}
-                            loading={loading}
-                            disabled={loading}
-                        >
-                            Save Changes
-                        </Button>
-                    </div>
-                </section>
-            </div>
-        </>
+                                    ))
+                                ) : (
+                                    <TableRow>
+                                        <TableCell
+                                            colSpan={columns.length}
+                                            className="h-24 text-center"
+                                        >
+                                            No targets. Add a target using the
+                                            form.
+                                        </TableCell>
+                                    </TableRow>
+                                )}
+                            </TableBody>
+                        </Table>
+                    </TableContainer>
+                </SettingsSectionBody>
+                <SettingsSectionFooter>
+                    <Button
+                        onClick={saveTargets}
+                        loading={loading}
+                        disabled={loading}
+                    >
+                        Save Targets
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
+        </SettingsContainer>
     );
 }
 

+ 97 - 156
src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx

@@ -11,7 +11,7 @@ import {
     FormField,
     FormItem,
     FormLabel,
-    FormMessage,
+    FormMessage
 } from "@/components/ui/form";
 import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
 import { Input } from "@/components/ui/input";
@@ -21,13 +21,13 @@ import {
     CommandGroup,
     CommandInput,
     CommandItem,
-    CommandList,
+    CommandList
 } from "@/components/ui/command";
 
 import {
     Popover,
     PopoverContent,
-    PopoverTrigger,
+    PopoverTrigger
 } from "@/components/ui/popover";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { ListSitesResponse } from "@server/routers/site";
@@ -37,7 +37,16 @@ import { useParams, useRouter } from "next/navigation";
 import { useForm } from "react-hook-form";
 import { GetResourceAuthInfoResponse } from "@server/routers/resource";
 import { useToast } from "@app/hooks/useToast";
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+import {
+    SettingsContainer,
+    SettingsSection,
+    SettingsSectionHeader,
+    SettingsSectionTitle,
+    SettingsSectionDescription,
+    SettingsSectionBody,
+    SettingsSectionForm,
+    SettingsSectionFooter
+} from "@app/components/Settings";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import CustomDomainInput from "../CustomDomainInput";
 import ResourceInfoBox from "../ResourceInfoBox";
@@ -47,7 +56,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
 
 const GeneralFormSchema = z.object({
     name: z.string(),
-    subdomain: subdomainSchema,
+    subdomain: subdomainSchema
     // siteId: z.number(),
 });
 
@@ -72,10 +81,10 @@ export default function GeneralForm() {
         resolver: zodResolver(GeneralFormSchema),
         defaultValues: {
             name: resource.name,
-            subdomain: resource.subdomain,
+            subdomain: resource.subdomain
             // siteId: resource.siteId!,
         },
-        mode: "onChange",
+        mode: "onChange"
     });
 
     useEffect(() => {
@@ -95,7 +104,7 @@ export default function GeneralForm() {
             `resource/${resource?.resourceId}`,
             {
                 name: data.name,
-                subdomain: data.subdomain,
+                subdomain: data.subdomain
                 // siteId: data.siteId,
             }
         )
@@ -106,13 +115,13 @@ export default function GeneralForm() {
                     description: formatAxiosError(
                         e,
                         "An error occurred while updating the resource"
-                    ),
+                    )
                 });
             })
             .then(() => {
                 toast({
                     title: "Resource updated",
-                    description: "The resource has been updated successfully",
+                    description: "The resource has been updated successfully"
                 });
 
                 updateResource({ name: data.name, subdomain: data.subdomain });
@@ -123,153 +132,85 @@ export default function GeneralForm() {
     }
 
     return (
-        <>
-            <div className="space-y-12 lg:max-w-2xl">
-                <section className="space-y-4">
-                    <SettingsSectionTitle
-                        title="General Settings"
-                        description="Configure the general settings for this resource"
-                        size="1xl"
-                    />
-
-                    <Form {...form}>
-                        <form
-                            onSubmit={form.handleSubmit(onSubmit)}
-                            className="space-y-4"
-                        >
-                            <FormField
-                                control={form.control}
-                                name="name"
-                                render={({ field }) => (
-                                    <FormItem>
-                                        <FormLabel>Name</FormLabel>
-                                        <FormControl>
-                                            <Input {...field} />
-                                        </FormControl>
-                                        <FormDescription>
-                                            This is the display name of the
-                                            resource.
-                                        </FormDescription>
-                                        <FormMessage />
-                                    </FormItem>
-                                )}
-                            />
-
-                            <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>
-                                )}
-                            />
-                            {/* <FormField
-                            control={form.control}
-                            name="siteId"
-                            render={({ field }) => (
-                                <FormItem className="flex flex-col">
-                                    <FormLabel>Site</FormLabel>
-                                    <Popover>
-                                        <PopoverTrigger asChild>
+        <SettingsContainer>
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        General Settings
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Configure the general settings for this resource
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+
+                <SettingsSectionBody>
+                    <SettingsSectionForm>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="name"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Name</FormLabel>
                                             <FormControl>
-                                                <Button
-                                                    variant="outline"
-                                                    role="combobox"
-                                                    className={cn(
-                                                        "w-[350px] 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>
+                                                <Input {...field} />
                                             </FormControl>
-                                        </PopoverTrigger>
-                                        <PopoverContent className="w-[350px] p-0">
-                                            <Command>
-                                                <CommandInput placeholder="Search sites" />
-                                                <CommandList>
-                                                    <CommandEmpty>
-                                                        No sites found.
-                                                    </CommandEmpty>
-                                                    <CommandGroup>
-                                                        {sites.map((site) => (
-                                                            <CommandItem
-                                                                value={
-                                                                    site.name
-                                                                }
-                                                                key={
-                                                                    site.siteId
-                                                                }
-                                                                onSelect={() => {
-                                                                    form.setValue(
-                                                                        "siteId",
-                                                                        site.siteId
-                                                                    );
-                                                                }}
-                                                            >
-                                                                <CheckIcon
-                                                                    className={cn(
-                                                                        "mr-2 h-4 w-4",
-                                                                        site.siteId ===
-                                                                            field.value
-                                                                            ? "opacity-100"
-                                                                            : "opacity-0"
-                                                                    )}
-                                                                />
-                                                                {site.name}
-                                                            </CommandItem>
-                                                        ))}
-                                                    </CommandGroup>
-                                                </CommandList>
-                                            </Command>
-                                        </PopoverContent>
-                                    </Popover>
-                                    <FormDescription>
-                                        This is the site that will be used in
-                                        the dashboard.
-                                    </FormDescription>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        /> */}
-                            <Button
-                                type="submit"
-                                loading={saveLoading}
-                                disabled={saveLoading}
-                            >
-                                Save Changes
-                            </Button>
-                        </form>
-                    </Form>
-                </section>
-            </div>
-        </>
+                                            <FormDescription>
+                                                This is the display name of the
+                                                resource.
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+
+                                <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>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </SettingsSectionForm>
+                </SettingsSectionBody>
+
+                <SettingsSectionFooter>
+                    <Button
+                        type="submit"
+                        loading={saveLoading}
+                        disabled={saveLoading}
+                        form="general-settings-form"
+                    >
+                        Save Settings
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
+        </SettingsContainer>
     );
 }

+ 3 - 0
src/app/[orgId]/settings/resources/page.tsx

@@ -8,6 +8,7 @@ import { redirect } from "next/navigation";
 import { cache } from "react";
 import { GetOrgResponse } from "@server/routers/org";
 import OrgProvider from "@app/providers/OrgProvider";
+import ResourcesSplashCard from "./ResourcesSplashCard";
 
 type ResourcesPageProps = {
     params: Promise<{ orgId: string }>;
@@ -62,6 +63,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
 
     return (
         <>
+            <ResourcesSplashCard />
+
             <SettingsSectionTitle
                 title="Manage Resources"
                 description="Create secure proxies to your private applications"

+ 3 - 2
src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx

@@ -16,6 +16,7 @@ import {
     Table,
     TableBody,
     TableCell,
+    TableContainer,
     TableHead,
     TableHeader,
     TableRow
@@ -89,7 +90,7 @@ export function ShareLinksDataTable<TData, TValue>({
                     <Plus className="mr-2 h-4 w-4" /> Create Share Link
                 </Button>
             </div>
-            <div className="border rounded-md">
+            <TableContainer>
                 <Table>
                     <TableHeader>
                         {table.getHeaderGroups().map((headerGroup) => (
@@ -141,7 +142,7 @@ export function ShareLinksDataTable<TData, TValue>({
                         )}
                     </TableBody>
                 </Table>
-            </div>
+            </TableContainer>
             <div className="mt-4">
                 <DataTablePagination table={table} />
             </div>

+ 70 - 0
src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx

@@ -0,0 +1,70 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Link, X, Clock, Share, ArrowRight, Lock } from "lucide-react"; // Replace with actual imports
+import { Card, CardContent } from "@app/components/ui/card";
+import { Button } from "@app/components/ui/button";
+
+export const ShareableLinksSplash = () => {
+    const [isDismissed, setIsDismissed] = useState(false);
+
+    const key = "share-links-splash-dismissed";
+
+    useEffect(() => {
+        const dismissed = localStorage.getItem(key);
+        if (dismissed === "true") {
+            setIsDismissed(true);
+        }
+    }, []);
+
+    const handleDismiss = () => {
+        setIsDismissed(true);
+        localStorage.setItem(key, "true");
+    };
+
+    if (isDismissed) {
+        return null;
+    }
+
+    return (
+        <Card className="w-full mx-auto overflow-hidden mb-8 hidden md:block relative">
+            <button
+                onClick={handleDismiss}
+                className="absolute top-2 right-2 p-2"
+                aria-label="Dismiss"
+            >
+                <X className="w-5 h-5" />
+            </button>
+            <CardContent className="grid gap-6 p-6">
+                <div className="space-y-4">
+                    <h3 className="text-xl font-semibold flex items-center gap-2">
+                        <Link className="text-blue-500" />
+                        Shareable Links
+                    </h3>
+                    <p className="text-sm">
+                        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.
+                    </p>
+                    <ul className="text-sm text-muted-foreground space-y-2">
+                        <li className="flex items-center gap-2">
+                            <Share className="text-green-500 w-4 h-4" />
+                            Easy to create and share
+                        </li>
+                        <li className="flex items-center gap-2">
+                            <Clock className="text-yellow-500 w-4 h-4" />
+                            Configurable expiration duration
+                        </li>
+                        <li className="flex items-center gap-2">
+                            <Lock className="text-red-500 w-4 h-4" />
+                            Secure and revocable
+                        </li>
+                    </ul>
+                </div>
+            </CardContent>
+        </Card>
+    );
+};
+
+export default ShareableLinksSplash;

+ 3 - 0
src/app/[orgId]/settings/share-links/page.tsx

@@ -8,6 +8,7 @@ import { GetOrgResponse } from "@server/routers/org";
 import OrgProvider from "@app/providers/OrgProvider";
 import { ListAccessTokensResponse } from "@server/routers/accessToken";
 import ShareLinksTable, { ShareLinkRow } from "./ShareLinksTable";
+import ShareableLinksSplash from "./ShareLinksSplash";
 
 type ShareLinksPageProps = {
     params: Promise<{ orgId: string }>;
@@ -52,6 +53,8 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) {
 
     return (
         <>
+            <ShareableLinksSplash />
+
             <SettingsSectionTitle
                 title="Manage Share Links"
                 description="Create shareable links to grant temporary or permanent access to your resources"

+ 21 - 0
src/app/[orgId]/settings/sites/CreateSiteForm.tsx

@@ -36,6 +36,9 @@ import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { SiteRow } from "./SitesTable";
 import { AxiosResponse } from "axios";
+import { Button } from "@app/components/ui/button";
+import Link from "next/link";
+import { ArrowUpRight } from "lucide-react";
 
 const createSiteFormSchema = z.object({
     name: z
@@ -274,6 +277,24 @@ PersistentKeepalive = 5`
                         You will only be able to see the configuration once.
                     </span>
 
+                    {form.watch("method") === "newt" && (
+                        <>
+                            <br />
+                            <Link
+                                className="text-sm text-primary flex items-center gap-1"
+                                href="https://docs.fossorial.io/Newt/install"
+                                target="_blank"
+                                rel="noopener noreferrer"
+                            >
+                                <span>
+                                    {" "}
+                                    Learn how to install Newt on your system
+                                </span>
+                                <ArrowUpRight className="w-5 h-5" />
+                            </Link>
+                        </>
+                    )}
+
                     <div className="flex items-center space-x-2">
                         <Checkbox
                             id="terms"

+ 3 - 2
src/app/[orgId]/settings/sites/SitesDataTable.tsx

@@ -16,6 +16,7 @@ import {
     Table,
     TableBody,
     TableCell,
+    TableContainer,
     TableHead,
     TableHeader,
     TableRow,
@@ -89,7 +90,7 @@ export function SitesDataTable<TData, TValue>({
                     <Plus className="mr-2 h-4 w-4" /> Add Site
                 </Button>
             </div>
-            <div className="border rounded-md">
+            <TableContainer>
                 <Table>
                     <TableHeader>
                         {table.getHeaderGroups().map((headerGroup) => (
@@ -141,7 +142,7 @@ export function SitesDataTable<TData, TValue>({
                         )}
                     </TableBody>
                 </Table>
-            </div>
+            </TableContainer>
             <div className="mt-4">
                 <DataTablePagination table={table} />
             </div>

+ 98 - 0
src/app/[orgId]/settings/sites/SitesSplashCard.tsx

@@ -0,0 +1,98 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { ArrowRight, DockIcon as Docker, Globe, Server, X } from "lucide-react";
+import Link from "next/link";
+
+export const SitesSplashCard = () => {
+    const [isDismissed, setIsDismissed] = useState(true);
+
+    const key = "sites-splash-card-dismissed";
+
+    useEffect(() => {
+        const dismissed = localStorage.getItem(key);
+        if (dismissed === "true") {
+            setIsDismissed(true);
+        } else {
+            setIsDismissed(false);
+        }
+    }, []);
+
+    const handleDismiss = () => {
+        setIsDismissed(true);
+        localStorage.setItem(key, "true");
+    };
+
+    if (isDismissed) {
+        return null;
+    }
+
+    return (
+        <Card className="w-full mx-auto overflow-hidden mb-8 hidden md:block relative">
+            <button
+                onClick={handleDismiss}
+                className="absolute top-2 right-2 p-2"
+                aria-label="Dismiss"
+            >
+                <X className="w-5 h-5" />
+            </button>
+            <CardContent className="grid gap-6 p-6 sm:grid-cols-2">
+                <div className="space-y-4">
+                    <h3 className="text-xl font-semibold flex items-center gap-2">
+                        <Globe className="text-blue-500" />
+                        Newt (Recommended)
+                    </h3>
+                    <p className="text-sm">
+                        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.
+                    </p>
+                    <ul className="text-sm text-muted-foreground space-y-2">
+                        <li className="flex items-center gap-2">
+                            <Server className="text-green-500 w-4 h-4" />
+                            Runs in Docker
+                        </li>
+                        <li className="flex items-center gap-2">
+                            <Server className="text-green-500 w-4 h-4" />
+                            Runs in shell on macOS, Linux, and Windows
+                        </li>
+                    </ul>
+                    <Button className="w-full" variant="secondary">
+                        <Link
+                            href="https://docs.fossorial.io/Newt/install"
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="flex items-center"
+                        >
+                            Install Newt <ArrowRight className="ml-2 w-4 h-4" />
+                        </Link>
+                    </Button>
+                </div>
+                <div className="space-y-4">
+                    <h3 className="text-xl font-semibold flex items-center gap-2">
+                        Basic WireGuard
+                    </h3>
+                    <p className="text-sm">
+                        Use any WireGuard client to connect. You will have to
+                        address your internal resources using the peer IP.
+                    </p>
+                    <ul className="text-sm text-muted-foreground space-y-2">
+                        <li className="flex items-center gap-2">
+                            <Docker className="text-purple-500 w-4 h-4" />
+                            Compatible with all WireGuard clients
+                        </li>
+                        <li className="flex items-center gap-2">
+                            <Server className="text-purple-500 w-4 h-4" />
+                            Manual configuration required
+                        </li>
+                    </ul>
+                </div>
+            </CardContent>
+        </Card>
+    );
+};
+
+export default SitesSplashCard;

+ 1 - 1
src/app/[orgId]/settings/sites/SitesTable.tsx

@@ -256,7 +256,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
                         <Link
                             href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
                         >
-                            <Button variant={"gray"} className="ml-2">
+                            <Button variant={"outline"} className="ml-2">
                                 Edit
                                 <ArrowRight className="ml-2 w-4 h-4" />
                             </Button>

+ 64 - 41
src/app/[orgId]/settings/sites/[niceId]/general/page.tsx

@@ -10,20 +10,29 @@ import {
     FormField,
     FormItem,
     FormLabel,
-    FormMessage,
+    FormMessage
 } from "@/components/ui/form";
 import { Input } from "@/components/ui/input";
 import { useSiteContext } from "@app/hooks/useSiteContext";
 import { useForm } from "react-hook-form";
 import { useToast } from "@app/hooks/useToast";
 import { useRouter } from "next/navigation";
-import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
-import { formatAxiosError } from "@app/lib/api";;
+import {
+    SettingsContainer,
+    SettingsSection,
+    SettingsSectionHeader,
+    SettingsSectionTitle,
+    SettingsSectionDescription,
+    SettingsSectionBody,
+    SettingsSectionForm,
+    SettingsSectionFooter
+} from "@app/components/Settings";
+import { formatAxiosError } from "@app/lib/api";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 
 const GeneralFormSchema = z.object({
-    name: z.string(),
+    name: z.string()
 });
 
 type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
@@ -39,15 +48,15 @@ export default function GeneralPage() {
     const form = useForm<GeneralFormValues>({
         resolver: zodResolver(GeneralFormSchema),
         defaultValues: {
-            name: site?.name,
+            name: site?.name
         },
-        mode: "onChange",
+        mode: "onChange"
     });
 
     async function onSubmit(data: GeneralFormValues) {
         await api
             .post(`/site/${site?.siteId}`, {
-                name: data.name,
+                name: data.name
             })
             .catch((e) => {
                 toast({
@@ -56,7 +65,7 @@ export default function GeneralPage() {
                     description: formatAxiosError(
                         e,
                         "An error occurred while updating the site."
-                    ),
+                    )
                 });
             });
 
@@ -66,39 +75,53 @@ export default function GeneralPage() {
     }
 
     return (
-        <>
-            <div className="space-y-4 max-w-xl">
-                <SettingsSectionTitle
-                    title="General Settings"
-                    description="Configure the general settings for this site"
-                    size="1xl"
-                />
+        <SettingsContainer>
+            <SettingsSection>
+                <SettingsSectionHeader>
+                    <SettingsSectionTitle>
+                        General Settings
+                    </SettingsSectionTitle>
+                    <SettingsSectionDescription>
+                        Configure the general settings for this site
+                    </SettingsSectionDescription>
+                </SettingsSectionHeader>
+
+                <SettingsSectionBody>
+                    <SettingsSectionForm>
+                        <Form {...form}>
+                            <form
+                                onSubmit={form.handleSubmit(onSubmit)}
+                                className="space-y-4"
+                                id="general-settings-form"
+                            >
+                                <FormField
+                                    control={form.control}
+                                    name="name"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormLabel>Name</FormLabel>
+                                            <FormControl>
+                                                <Input {...field} />
+                                            </FormControl>
+                                            <FormDescription>
+                                                This is the display name of the
+                                                site
+                                            </FormDescription>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                            </form>
+                        </Form>
+                    </SettingsSectionForm>
+                </SettingsSectionBody>
 
-                <Form {...form}>
-                    <form
-                        onSubmit={form.handleSubmit(onSubmit)}
-                        className="space-y-4"
-                    >
-                        <FormField
-                            control={form.control}
-                            name="name"
-                            render={({ field }) => (
-                                <FormItem>
-                                    <FormLabel>Name</FormLabel>
-                                    <FormControl>
-                                        <Input {...field} />
-                                    </FormControl>
-                                    <FormDescription>
-                                        This is the display name of the site
-                                    </FormDescription>
-                                    <FormMessage />
-                                </FormItem>
-                            )}
-                        />
-                        <Button type="submit">Save Changes</Button>
-                    </form>
-                </Form>
-            </div>
-        </>
+                <SettingsSectionFooter>
+                    <Button type="submit" form="general-settings-form">
+                        Save Settings
+                    </Button>
+                </SettingsSectionFooter>
+            </SettingsSection>
+        </SettingsContainer>
     );
 }

+ 3 - 0
src/app/[orgId]/settings/sites/page.tsx

@@ -4,6 +4,7 @@ import { ListSitesResponse } from "@server/routers/site";
 import { AxiosResponse } from "axios";
 import SitesTable, { SiteRow } from "./SitesTable";
 import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+import SitesSplashCard from "./SitesSplashCard";
 
 type SitesPageProps = {
     params: Promise<{ orgId: string }>;
@@ -47,6 +48,8 @@ export default async function SitesPage(props: SitesPageProps) {
 
     return (
         <>
+            <SitesSplashCard />
+
             <SettingsSectionTitle
                 title="Manage Sites"
                 description="Allow connectivity to your network through secure tunnels"

+ 15 - 4
src/app/auth/login/DashboardLoginForm.tsx

@@ -12,6 +12,7 @@ import LoginForm from "@app/components/LoginForm";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useRouter } from "next/navigation";
 import { useEffect } from "react";
+import Image from "next/image";
 
 type DashboardLoginFormProps = {
     redirect?: string;
@@ -37,10 +38,20 @@ export default function DashboardLoginForm({
     return (
         <Card className="w-full max-w-md">
             <CardHeader>
-                <CardTitle>Welcome to Pangolin</CardTitle>
-                <CardDescription>
-                    Enter your credentials to access your dashboard
-                </CardDescription>
+                <div className="flex flex-row items-center justify-center">
+                    <Image
+                        src={`/logo/pangolin_orange.svg`}
+                        alt="Pangolin Logo"
+                        width="100"
+                        height="100"
+                    />
+                </div>
+                <div className="text-center space-y-1">
+                    <h1 className="text-2xl font-bold mt-1">
+                        Welcome to Pangolin
+                    </h1>
+                    <p className="text-sm text-muted-foreground">Log in to get started</p>
+                </div>
             </CardHeader>
             <CardContent>
                 <LoginForm

+ 4 - 1
src/app/auth/login/page.tsx

@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
 import { cache } from "react";
 import DashboardLoginForm from "./DashboardLoginForm";
 import { Mail } from "lucide-react";
+import { pullEnv } from "@app/lib/pullEnv";
 
 export const dynamic = "force-dynamic";
 
@@ -16,7 +17,9 @@ export default async function Page(props: {
 
     const isInvite = searchParams?.redirect?.includes("/invite");
 
-    const signUpDisabled = process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true";
+    const env = pullEnv();
+
+    const signUpDisabled = env.flags.disableSignupWithoutInvite;
 
     if (user) {
         redirect("/");

+ 0 - 2
src/app/auth/reset-password/ResetPasswordForm.tsx

@@ -364,8 +364,6 @@ export default function ResetPasswordForm({
                                                                 <InputOTPSlot
                                                                     index={2}
                                                                 />
-                                                            </InputOTPGroup>
-                                                            <InputOTPGroup>
                                                                 <InputOTPSlot
                                                                     index={3}
                                                                 />

+ 1 - 1
src/app/auth/reset-password/page.tsx

@@ -38,7 +38,7 @@ export default async function Page(props: {
                     }
                     className="underline"
                 >
-                    Go to login
+                    Go back to log in
                 </Link>
             </p>
         </>

+ 8 - 7
src/app/auth/resource/[resourceId]/page.tsx

@@ -16,6 +16,7 @@ import { cookies } from "next/headers";
 import { CheckResourceSessionResponse } from "@server/routers/auth";
 import AccessTokenInvalid from "./AccessToken";
 import AccessToken from "./AccessToken";
+import { pullEnv } from "@app/lib/pullEnv";
 
 export default async function ResourceAuthPage(props: {
     params: Promise<{ resourceId: number }>;
@@ -27,6 +28,8 @@ export default async function ResourceAuthPage(props: {
     const params = await props.params;
     const searchParams = await props.searchParams;
 
+    const env = pullEnv();
+
     let authInfo: GetResourceAuthInfoResponse | undefined;
     try {
         const res = await internal.get<
@@ -42,7 +45,9 @@ export default async function ResourceAuthPage(props: {
     const user = await getUser({ skipCheckVerifyEmail: true });
 
     if (!authInfo) {
-        {/* @ts-ignore */} // TODO: fix this
+        {
+            /* @ts-ignore */
+        } // TODO: fix this
         return (
             <div className="w-full max-w-md">
                 <ResourceNotFound />
@@ -63,11 +68,7 @@ export default async function ResourceAuthPage(props: {
         !authInfo.pincode &&
         !authInfo.whitelist;
 
-    if (
-        user &&
-        !user.emailVerified &&
-        process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true"
-    ) {
+    if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
         redirect(
             `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceId}`
         );
@@ -75,7 +76,7 @@ export default async function ResourceAuthPage(props: {
 
     const allCookies = await cookies();
     const cookieName =
-        process.env.RESOURCE_SESSION_COOKIE_NAME + `_${params.resourceId}`;
+        env.server.resourceSessionCookieName + `_${params.resourceId}`;
     const sessionId = allCookies.get(cookieName)?.value ?? null;
 
     if (sessionId) {

+ 29 - 12
src/app/auth/signup/SignupForm.tsx

@@ -12,23 +12,24 @@ import {
     FormField,
     FormItem,
     FormLabel,
-    FormMessage,
+    FormMessage
 } from "@/components/ui/form";
 import {
     Card,
     CardContent,
     CardDescription,
     CardHeader,
-    CardTitle,
+    CardTitle
 } from "@/components/ui/card";
 import { Alert, AlertDescription } from "@/components/ui/alert";
 import { SignUpResponse } from "@server/routers/auth";
 import { useRouter } from "next/navigation";
 import { passwordSchema } from "@server/auth/passwordSchema";
 import { AxiosResponse } from "axios";
-import { formatAxiosError } from "@app/lib/api";;
+import { formatAxiosError } from "@app/lib/api";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
+import Image from "next/image";
 
 type SignupFormProps = {
     redirect?: string;
@@ -40,14 +41,18 @@ const formSchema = z
     .object({
         email: z.string().email({ message: "Invalid email address" }),
         password: passwordSchema,
-        confirmPassword: passwordSchema,
+        confirmPassword: passwordSchema
     })
     .refine((data) => data.password === data.confirmPassword, {
         path: ["confirmPassword"],
-        message: "Passwords do not match",
+        message: "Passwords do not match"
     });
 
-export default function SignupForm({ redirect, inviteId, inviteToken }: SignupFormProps) {
+export default function SignupForm({
+    redirect,
+    inviteId,
+    inviteToken
+}: SignupFormProps) {
     const router = useRouter();
 
     const api = createApiClient(useEnvContext());
@@ -60,8 +65,8 @@ export default function SignupForm({ redirect, inviteId, inviteToken }: SignupFo
         defaultValues: {
             email: "",
             password: "",
-            confirmPassword: "",
-        },
+            confirmPassword: ""
+        }
     });
 
     async function onSubmit(values: z.infer<typeof formSchema>) {
@@ -109,10 +114,22 @@ export default function SignupForm({ redirect, inviteId, inviteToken }: SignupFo
     return (
         <Card className="w-full max-w-md">
             <CardHeader>
-                <CardTitle>Create Account</CardTitle>
-                <CardDescription>
-                    Enter your details to create an account
-                </CardDescription>
+                <div className="flex flex-row items-center justify-center">
+                    <Image
+                        src={`/logo/pangolin_orange.svg`}
+                        alt="Pangolin Logo"
+                        width="100"
+                        height="100"
+                    />
+                </div>
+                <div className="text-center space-y-1">
+                    <h1 className="text-2xl font-bold mt-1">
+                        Welcome to Pangolin
+                    </h1>
+                    <p className="text-sm text-muted-foreground">
+                        Create an account to get started
+                    </p>
+                </div>
             </CardHeader>
             <CardContent>
                 <Form {...form}>

+ 4 - 1
src/app/auth/signup/page.tsx

@@ -1,5 +1,6 @@
 import SignupForm from "@app/app/auth/signup/SignupForm";
 import { verifySession } from "@app/lib/auth/verifySession";
+import { pullEnv } from "@app/lib/pullEnv";
 import { Mail } from "lucide-react";
 import Link from "next/link";
 import { redirect } from "next/navigation";
@@ -14,9 +15,11 @@ export default async function Page(props: {
     const getUser = cache(verifySession);
     const user = await getUser();
 
+    const env = pullEnv();
+
     const isInvite = searchParams?.redirect?.includes("/invite");
 
-    if (process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true" && !isInvite) {
+    if (env.flags.disableSignupWithoutInvite && !isInvite) {
         redirect("/");
     }
 

+ 4 - 1
src/app/auth/verify-email/page.tsx

@@ -1,5 +1,6 @@
 import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
 import { verifySession } from "@app/lib/auth/verifySession";
+import { pullEnv } from "@app/lib/pullEnv";
 import { redirect } from "next/navigation";
 import { cache } from "react";
 
@@ -8,7 +9,9 @@ export const dynamic = "force-dynamic";
 export default async function Page(props: {
     searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
 }) {
-    if (process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED !== "true") {
+    const env = pullEnv();
+
+    if (!env.flags.emailVerificationRequired) {
         redirect("/");
     }
 

BIN
src/app/favicon.ico


+ 6 - 6
src/app/globals.css

@@ -15,14 +15,14 @@
     --primary-foreground: 60 9.1% 97.8%;
     --secondary: 60 4.8% 95.9%;
     --secondary-foreground: 24 9.8% 10%;
-    --muted: 60 4.8% 95.9%;
+    --muted: 60 4.8% 85.0%;
     --muted-foreground: 25 5.3% 44.7%;
-    --accent: 60 4.8% 95.9%;
+    --accent: 60 4.8% 90%;
     --accent-foreground: 24 9.8% 10%;
     --destructive: 0 84.2% 60.2%;
     --destructive-foreground: 60 9.1% 97.8%;
-    --border: 20 5.9% 90%;
-    --input: 20 5.9% 90%;
+    --border: 20 5.9% 85%;
+    --input: 20 5.9% 85%;
     --ring: 24.6 95% 53.1%;
     --radius: 0.75rem;
     --chart-1: 12 76% 61%;
@@ -41,11 +41,11 @@
     --popover-foreground: 60 9.1% 97.8%;
     --primary: 20.5 90.2% 48.2%;
     --primary-foreground: 60 9.1% 97.8%;
-    --secondary: 12 6.5% 25.0%;
+    --secondary: 12 6.5% 15.0%;
     --secondary-foreground: 60 9.1% 97.8%;
     --muted: 12 6.5% 25.0%;
     --muted-foreground: 24 5.4% 63.9%;
-    --accent: 12 6.5% 25.0%;
+    --accent: 12 2.5% 15.0%;
     --accent-foreground: 60 9.1% 97.8%;
     --destructive: 0 72.2% 50.6%;
     --destructive-foreground: 60 9.1% 97.8%;

+ 19 - 17
src/app/layout.tsx

@@ -1,24 +1,28 @@
 import type { Metadata } from "next";
 import "./globals.css";
-import { Figtree } from "next/font/google";
+import { Figtree, Inter } from "next/font/google";
 import { Toaster } from "@/components/ui/toaster";
 import { ThemeProvider } from "@app/providers/ThemeProvider";
 import EnvProvider from "@app/providers/EnvProvider";
 import { Separator } from "@app/components/ui/separator";
+import { pullEnv } from "@app/lib/pullEnv";
 
 export const metadata: Metadata = {
     title: `Dashboard - Pangolin`,
     description: ""
 };
 
-const font = Figtree({ subsets: ["latin"] });
+// const font = Figtree({ subsets: ["latin"] });
+const font = Inter({ subsets: ["latin"] });
 
 export default async function RootLayout({
     children
 }: Readonly<{
     children: React.ReactNode;
 }>) {
-    const version = process.env.APP_VERSION;
+    const env = pullEnv();
+
+    const version = env.app.version;
 
     return (
         <html suppressHydrationWarning>
@@ -29,24 +33,12 @@ export default async function RootLayout({
                     enableSystem
                     disableTransitionOnChange
                 >
-                    <EnvProvider
-                        env={{
-                            NEXT_PORT: process.env.NEXT_PORT as string,
-                            SERVER_EXTERNAL_PORT: process.env
-                                .SERVER_EXTERNAL_PORT as string,
-                            ENVIRONMENT: process.env.ENVIRONMENT as string,
-                            EMAIL_ENABLED: process.env.EMAIL_ENABLED as string,
-                            DISABLE_USER_CREATE_ORG:
-                                process.env.DISABLE_USER_CREATE_ORG,
-                            DISABLE_SIGNUP_WITHOUT_INVITE:
-                                process.env.DISABLE_SIGNUP_WITHOUT_INVITE
-                        }}
-                    >
+                    <EnvProvider env={pullEnv()}>
                         {/* Main content */}
                         <div className="flex-grow">{children}</div>
 
                         {/* Footer */}
-                        <footer className="w-full mt-12 py-3 mb-4">
+                        <footer className="w-full mt-12 py-3 mb-6">
                             <div className="container mx-auto flex flex-wrap justify-center items-center h-4 space-x-4 text-sm text-neutral-400 select-none">
                                 <div className="whitespace-nowrap">
                                     Pangolin
@@ -73,6 +65,16 @@ export default async function RootLayout({
                                         <path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
                                     </svg>
                                 </a>
+                                <Separator orientation="vertical" />
+                                <a
+                                    href="https://docs.fossorial.io/Pangolin/overview"
+                                    target="_blank"
+                                    rel="noopener noreferrer"
+                                    aria-label="GitHub"
+                                    className="flex items-center space-x-3 whitespace-nowrap"
+                                >
+                                    <span>Docs</span>
+                                </a>
                                 {version && (
                                     <>
                                         <Separator orientation="vertical" />

+ 5 - 2
src/app/page.tsx

@@ -10,6 +10,7 @@ import Link from "next/link";
 import { redirect } from "next/navigation";
 import { cache } from "react";
 import OrganizationLanding from "./components/OrganizationLanding";
+import { pullEnv } from "@app/lib/pullEnv";
 
 export const dynamic = "force-dynamic";
 
@@ -21,6 +22,8 @@ export default async function Page(props: {
 }) {
     const params = await props.searchParams; // this is needed to prevent static optimization
 
+    const env = pullEnv();
+
     const getUser = cache(verifySession);
     const user = await getUser({ skipCheckVerifyEmail: true });
 
@@ -34,7 +37,7 @@ export default async function Page(props: {
 
     if (
         !user.emailVerified &&
-        process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true"
+        env.flags.emailVerificationRequired
     ) {
         if (params.redirect) {
             redirect(`/auth/verify-email?redirect=${params.redirect}`);
@@ -57,7 +60,7 @@ export default async function Page(props: {
 
     if (!orgs.length) {
         if (
-            process.env.DISABLE_USER_CREATE_ORG === "false" ||
+            !env.flags.disableUserCreateOrg ||
             user.serverAdmin
         ) {
             redirect("/setup");

+ 4 - 1
src/app/setup/layout.tsx

@@ -1,5 +1,6 @@
 import ProfileIcon from "@app/components/ProfileIcon";
 import { verifySession } from "@app/lib/auth/verifySession";
+import { pullEnv } from "@app/lib/pullEnv";
 import UserProvider from "@app/providers/UserProvider";
 import { Metadata } from "next";
 import { redirect } from "next/navigation";
@@ -20,12 +21,14 @@ export default async function SetupLayout({
     const getUser = cache(verifySession);
     const user = await getUser();
 
+    const env = pullEnv();
+
     if (!user) {
         redirect("/?redirect=/setup");
     }
 
     if (
-        !(process.env.DISABLE_USER_CREATE_ORG === "false" || user.serverAdmin)
+        !(!env.flags.disableUserCreateOrg || user.serverAdmin)
     ) {
         redirect("/");
     }

+ 2 - 2
src/components/CopyTextBox.tsx

@@ -23,7 +23,7 @@ export default function CopyTextBox({ text = "", wrapText = false }) {
     };
 
     return (
-        <div className="relative w-full border rounded-md">
+        <div className="relative w-full border rounded-md bg-card">
             <pre
                 ref={textRef}
                 className={`p-4 pr-16 text-sm w-full ${
@@ -38,7 +38,7 @@ export default function CopyTextBox({ text = "", wrapText = false }) {
                 variant="outline"
                 size="icon"
                 type="button"
-                className="absolute top-1 right-1 z-10"
+                className="absolute top-1 right-1 z-10 bg-card"
                 onClick={copyToClipboard}
                 aria-label="Copy to clipboard"
             >

+ 1 - 8
src/components/Credenza.tsx

@@ -90,14 +90,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
 
     const CredenzaContent = isDesktop ? DialogContent : SheetContent;
 
-    return isDesktop ? (
-        <CredenzaContent
-            className={cn("overflow-y-auto max-h-screen", className)}
-            {...props}
-        >
-            {children}
-        </CredenzaContent>
-    ) : (
+    return (
         <CredenzaContent
             className={cn("overflow-y-auto max-h-screen", className)}
             {...props}

+ 3 - 1
src/components/Enable2FaForm.tsx

@@ -224,7 +224,9 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
                             <div className="h-[250px] mx-auto flex items-center justify-center">
                                 <QRCodeCanvas value={secretUri} size={200} />
                             </div>
-                            <CopyTextBox text={secretUri} wrapText={false} />
+                            <div className="max-w-md mx-auto">
+                                <CopyTextBox text={secretUri} wrapText={false} />
+                            </div>
 
                             <Form {...confirmForm}>
                                 <form

+ 2 - 2
src/components/Header.tsx

@@ -48,7 +48,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
                     <div className="hidden md:block">
                         <div className="flex items-center gap-4 mr-4">
                             <Link
-                                href="https://docs.fossorial.io"
+                                href="https://docs.fossorial.io/Pangolin/overview"
                                 target="_blank"
                                 rel="noopener noreferrer"
                                 className="text-muted-foreground hover:text-foreground"
@@ -99,7 +99,7 @@ export function Header({ orgId, orgs }: HeaderProps) {
                                     <CommandEmpty>
                                         No organizations found.
                                     </CommandEmpty>
-                                    {(env.DISABLE_USER_CREATE_ORG === "false" ||
+                                    {(!env.flags.disableUserCreateOrg ||
                                         user.serverAdmin) && (
                                         <>
                                             <CommandGroup heading="Create">

+ 1 - 2
src/components/LoginForm.tsx

@@ -37,6 +37,7 @@ import {
 } from "./ui/input-otp";
 import Link from "next/link";
 import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
+import Image from 'next/image'
 
 type LoginFormProps = {
     redirect?: string;
@@ -227,8 +228,6 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) {
                                                         <InputOTPSlot
                                                             index={2}
                                                         />
-                                                    </InputOTPGroup>
-                                                    <InputOTPGroup>
                                                         <InputOTPSlot
                                                             index={3}
                                                         />

+ 3 - 3
src/components/ProfileIcon.tsx

@@ -38,7 +38,7 @@ export default function ProfileIcon() {
     const [openDisable2fa, setOpenDisable2fa] = useState(false);
 
     function getInitials() {
-        return user.email.substring(0, 2).toUpperCase();
+        return user.email.substring(0, 1).toUpperCase();
     }
 
     function handleThemeChange(theme: "light" | "dark" | "system") {
@@ -144,8 +144,8 @@ export default function ProfileIcon() {
                         )}
                         <DropdownMenuSeparator />
                         <DropdownMenuItem onClick={() => logout()}>
-                            <LogOut className="mr-2 h-4 w-4" />
-                            <span>Log out</span>
+                            {/* <LogOut className="mr-2 h-4 w-4" /> */}
+                            <span>Log Out</span>
                         </DropdownMenuItem>
                     </DropdownMenuContent>
                 </DropdownMenu>

+ 31 - 0
src/components/Settings.tsx

@@ -0,0 +1,31 @@
+export function SettingsContainer({ children }: { children: React.ReactNode }) {
+    return <div className="space-y-4">{children}</div>
+}
+
+export function SettingsSection({ children }: { children: React.ReactNode }) {
+    return <div className="border rounded-md bg-card p-4">{children}</div>
+}
+
+export function SettingsSectionHeader({ children }: { children: React.ReactNode }) {
+    return <div className="space-y-0.5 pb-8">{children}</div>
+}
+
+export function SettingsSectionForm({ children }: { children: React.ReactNode }) {
+    return <div className="max-w-xl">{children}</div>
+}
+
+export function SettingsSectionTitle({ children }: { children: React.ReactNode }) {
+    return <h2 className="text-1xl font-bold tracking-tight flex items-center gap-2">{children}</h2>
+}
+
+export function SettingsSectionDescription({ children }: { children: React.ReactNode }) {
+    return <p className="text-muted-foreground">{children}</p>
+}
+
+export function SettingsSectionBody({ children }: { children: React.ReactNode }) {
+    return <div className="space-y-5">{children}</div>
+}
+
+export function SettingsSectionFooter({ children }: { children: React.ReactNode }) {
+    return <div className="flex justify-end space-x-4 mt-8">{children}</div>
+}

+ 1 - 1
src/components/SettingsSectionTitle.tsx

@@ -11,7 +11,7 @@ export default function SettingsSectionTitle({
 }: SettingsSectionTitleProps) {
     return (
         <div
-            className={`space-y-0.5 select-none ${!size || size === "2xl" ? "mb-6 md:mb-12" : ""}`}
+            className={`space-y-0.5 select-none ${!size || size === "2xl" ? "mb-8 md:mb-8" : ""}`}
         >
             <h2
                 className={`text-${

+ 2 - 2
src/components/SidebarNav.tsx

@@ -88,7 +88,7 @@ export function SidebarNav({
             </div>
             <nav
                 className={cn(
-                    "hidden lg:flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-3",
+                    "hidden lg:flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-3 pr-8",
                     disabled && "opacity-50 pointer-events-none",
                     className
                 )}
@@ -102,7 +102,7 @@ export function SidebarNav({
                             buttonVariants({ variant: "ghost" }),
                             pathname === hydrateHref(item.href) &&
                                 !pathname.includes("create")
-                                ? "bg-muted hover:bg-muted dark:bg-border dark:hover:bg-border"
+                                ? "bg-accent hover:bg-accent dark:bg-border dark:hover:bg-border"
                                 : "hover:bg-transparent hover:underline",
                             "justify-start",
                             disabled && "cursor-not-allowed"

+ 2 - 2
src/components/SidebarSettings.tsx

@@ -21,8 +21,8 @@ export function SidebarSettings({
     limitWidth
 }: SideBarSettingsProps) {
     return (
-        <div className="space-y-8 pb-16k">
-            <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-32 lg:space-y-0">
+        <div className="space-y-4">
+            <div className="flex flex-col space-y-4 lg:flex-row lg:space-x-6 lg:space-y-0">
                 <aside className="lg:w-1/5">
                     <SidebarNav items={sidebarNavItems} disabled={disabled} />
                 </aside>

+ 37 - 0
src/components/SwitchInput.tsx

@@ -0,0 +1,37 @@
+import React from "react";
+import { Switch } from "./ui/switch";
+import { Label } from "./ui/label";
+
+interface SwitchComponentProps {
+    id: string;
+    label: string;
+    description?: string;
+    defaultChecked?: boolean;
+    onCheckedChange: (checked: boolean) => void;
+}
+
+export function SwitchInput({
+    id,
+    label,
+    description,
+    defaultChecked = false,
+    onCheckedChange
+}: SwitchComponentProps) {
+    return (
+        <div>
+            <div className="flex items-center space-x-2 mb-2">
+                <Switch
+                    id={id}
+                    defaultChecked={defaultChecked}
+                    onCheckedChange={onCheckedChange}
+                />
+                <Label htmlFor={id}>{label}</Label>
+            </div>
+            {description && (
+                <span className="text-muted-foreground text-sm">
+                    {description}
+                </span>
+            )}
+        </div>
+    );
+}

+ 1 - 1
src/components/TopbarNav.tsx

@@ -38,7 +38,7 @@ export function TopbarNav({
                     key={item.href}
                     href={item.href.replace("{orgId}", orgId || "")}
                     className={cn(
-                        "relative px-3 py-3 text-md",
+                        "relative md:px-3 px-1 py-3 text-md",
                         pathname.startsWith(item.href.replace("{orgId}", orgId || ""))
                             ? "border-b-2 border-primary text-primary font-medium"
                             : "hover:text-primary text-muted-foreground font-medium",

+ 1 - 1
src/components/ui/alert.tsx

@@ -8,7 +8,7 @@ const alertVariants = cva(
     {
         variants: {
             variant: {
-                default: "bg-background text-foreground",
+                default: "bg-card text-foreground",
                 destructive:
                     "border-destructive/50 bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
                 success:

+ 4 - 5
src/components/ui/button.tsx

@@ -6,7 +6,7 @@ import { cn } from "@app/lib/cn";
 import { Loader2 } from "lucide-react";
 
 const buttonVariants = cva(
-    "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+    "inline-flex items-center justify-center rounded-full whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
     {
         variants: {
             variant: {
@@ -15,11 +15,10 @@ const buttonVariants = cva(
                 destructive:
                     "bg-destructive text-destructive-foreground hover:bg-destructive/90",
                 outline:
-                    "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+                    "border border-input bg-card hover:bg-accent hover:text-accent-foreground",
                 secondary:
-                    "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+                    "bg-secondary border border-input text-secondary-foreground hover:bg-secondary/80",
                 ghost: "hover:bg-accent hover:text-accent-foreground",
-                gray: "bg-accent text-accent-foreground hover:bg-accent/90",
                 link: "text-primary underline-offset-4 hover:underline",
             },
             size: {
@@ -27,7 +26,7 @@ const buttonVariants = cva(
                 sm: "h-8 rounded-md px-3",
                 lg: "h-10 rounded-md px-8",
                 icon: "h-9 w-9",
-            },
+            }
         },
         defaultVariants: {
             variant: "default",

+ 2 - 2
src/components/ui/command.tsx

@@ -117,8 +117,8 @@ const CommandItem = React.forwardRef<
   <CommandPrimitive.Item
     ref={ref}
     className={cn(
-      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
-      className
+      "relative flex cursor-default select-none items-center px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
+      className,
     )}
     {...props}
   />

+ 1 - 1
src/components/ui/dialog.tsx

@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
         <DialogPrimitive.Content
             ref={ref}
             className={cn(
-                "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
+                "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
                 className
             )}
             {...props}

+ 2 - 2
src/components/ui/input.tsx

@@ -15,7 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
                 <input
                     type={showPassword ? "text" : "password"}
                     className={cn(
-                        "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+                        "flex h-9 w-full rounded-md border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
                         className
                     )}
                     ref={ref}
@@ -39,7 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
             <input
                 type={type}
                 className={cn(
-                    "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+                    "flex h-9 w-full rounded-md border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
                     className
                 )}
                 ref={ref}

+ 10 - 1
src/components/ui/popover.tsx

@@ -7,7 +7,16 @@ import { cn } from "@app/lib/cn"
 
 const Popover = PopoverPrimitive.Root
 
-const PopoverTrigger = PopoverPrimitive.Trigger
+const PopoverTrigger = React.forwardRef<
+    React.ElementRef<typeof PopoverPrimitive.Trigger>,
+    React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>
+>(({ className, ...props }, ref) => (
+    <PopoverPrimitive.Trigger
+        ref={ref}
+        className={cn(className, "rounded-md")}
+        {...props}
+    />
+))
 
 const PopoverContent = React.forwardRef<
   React.ElementRef<typeof PopoverPrimitive.Content>,

+ 3 - 2
src/components/ui/select.tsx

@@ -19,8 +19,9 @@ const SelectTrigger = React.forwardRef<
   <SelectPrimitive.Trigger
     ref={ref}
     className={cn(
-      "flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
-      className
+      "flex h-9 w-full items-center justify-between border border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+      className,
+      "rounded-md"
     )}
     {...props}
   >

+ 6 - 6
src/components/ui/sheet.tsx

@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
 SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
 
 const sheetVariants = cva(
-  "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-100 data-[state=open]:duration-300",
+  "fixed z-50 gap-4 bg-card p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-100 data-[state=open]:duration-300",
   {
     variants: {
       side: {
@@ -65,10 +65,10 @@ const SheetContent = React.forwardRef<
       {...props}
     >
       {children}
-      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
-        <X className="h-4 w-4" />
-        <span className="sr-only">Close</span>
-      </SheetPrimitive.Close>
+      {/* <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> */}
+      {/*   <X className="h-4 w-4" /> */}
+      {/*   <span className="sr-only">Close</span> */}
+      {/* </SheetPrimitive.Close> */}
     </SheetPrimitive.Content>
   </SheetPortal>
 ))
@@ -80,7 +80,7 @@ const SheetHeader = ({
 }: React.HTMLAttributes<HTMLDivElement>) => (
   <div
     className={cn(
-      "flex flex-col text-center sm:text-left mb-4",
+      "flex flex-col sm:text-left mb-4",
       className
     )}
     {...props}

+ 2 - 2
src/components/ui/switch.tsx

@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
 >(({ className, ...props }, ref) => (
   <SwitchPrimitives.Root
     className={cn(
-      "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
+      "peer inline-flex h-4 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
       className
     )}
     {...props}
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
   >
     <SwitchPrimitives.Thumb
       className={cn(
-        "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
+        "pointer-events-none block h-3 w-3 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
       )}
     />
   </SwitchPrimitives.Root>

+ 4 - 0
src/components/ui/table.tsx

@@ -2,6 +2,10 @@ import * as React from "react"
 
 import { cn } from "@app/lib/cn"
 
+export function TableContainer({ children }: { children: React.ReactNode }) {
+    return <div className="border rounded-md bg-card">{children}</div>
+}
+
 const Table = React.forwardRef<
   HTMLTableElement,
   React.HTMLAttributes<HTMLTableElement>

+ 2 - 2
src/contexts/envContext.ts

@@ -1,8 +1,8 @@
-import { env } from "@app/lib/types/env";
+import { Env } from "@app/lib/types/env";
 import { createContext } from "react";
 
 interface EnvContextType {
-    env: env;
+    env: Env;
 }
 
 const EnvContext = createContext<EnvContextType | undefined>(undefined);

+ 4 - 1
src/lib/api/cookies.ts

@@ -1,8 +1,11 @@
 import { cookies } from "next/headers";
+import { pullEnv } from "../pullEnv";
 
 export async function authCookieHeader() {
+    const env = pullEnv();
+
     const allCookies = await cookies();
-    const cookieName = process.env.SESSION_COOKIE_NAME!;
+    const cookieName = env.server.sessionCookieName;
     const sessionId = allCookies.get(cookieName)?.value ?? null;
     return {
         headers: {

+ 4 - 4
src/lib/api/index.ts

@@ -1,9 +1,9 @@
-import { env } from "@app/lib/types/env";
+import { Env } from "@app/lib/types/env";
 import axios, { AxiosInstance } from "axios";
 
 let apiInstance: AxiosInstance | null = null;
 
-export function createApiClient({ env }: { env: env }): AxiosInstance {
+export function createApiClient({ env }: { env: Env }): AxiosInstance {
     if (apiInstance) {
         return apiInstance;
     }
@@ -16,9 +16,9 @@ export function createApiClient({ env }: { env: env }): AxiosInstance {
     let baseURL;
     const suffix = "api/v1";
 
-    if (window.location.port === env.NEXT_PORT) {
+    if (window.location.port === env.server.nextPort) {
         // this means the user is addressing the server directly
-        baseURL = `${window.location.protocol}//${window.location.hostname}:${env.SERVER_EXTERNAL_PORT}/${suffix}`;
+        baseURL = `${window.location.protocol}//${window.location.hostname}:${env.server.externalPort}/${suffix}`;
         axios.defaults.withCredentials = true;
     } else {
         // user is accessing through a proxy

+ 4 - 1
src/lib/auth/verifySession.ts

@@ -2,12 +2,15 @@ import { internal } from "@app/lib/api";
 import { authCookieHeader } from "@app/lib/api/cookies";
 import { GetUserResponse } from "@server/routers/user";
 import { AxiosResponse } from "axios";
+import { pullEnv } from "../pullEnv";
 
 export async function verifySession({
     skipCheckVerifyEmail,
 }: {
     skipCheckVerifyEmail?: boolean;
 } = {}): Promise<GetUserResponse | null> {
+    const env = pullEnv();
+
     try {
         const res = await internal.get<AxiosResponse<GetUserResponse>>(
             "/user",
@@ -23,7 +26,7 @@ export async function verifySession({
         if (
             !skipCheckVerifyEmail &&
             !user.emailVerified &&
-            process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED == "true"
+            env.flags.emailVerificationRequired
         ) {
             return null;
         }

+ 31 - 0
src/lib/pullEnv.ts

@@ -0,0 +1,31 @@
+import { Env } from "./types/env";
+
+export function pullEnv(): Env {
+    return {
+        server: {
+            nextPort: process.env.NEXT_PORT as string,
+            externalPort: process.env.SERVER_EXTERNAL_PORT as string,
+            sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
+            resourceSessionCookieName: process.env.RESOURCE_SESSION_COOKIE_NAME as string
+        },
+        app: {
+            environment: process.env.ENVIRONMENT as string,
+            version: process.env.APP_VERSION as string
+        },
+        email: {
+            emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
+        },
+        flags: {
+            disableUserCreateOrg:
+                process.env.DISABLE_USER_CREATE_ORG === "true" ? true : false,
+            disableSignupWithoutInvite:
+                process.env.DISABLE_SIGNUP_WITHOUT_INVITE === "true"
+                    ? true
+                    : false,
+            emailVerificationRequired:
+                process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true"
+                    ? true
+                    : false
+        }
+    };
+}

+ 19 - 7
src/lib/types/env.ts

@@ -1,8 +1,20 @@
-export type env = {
-    SERVER_EXTERNAL_PORT: string;
-    NEXT_PORT: string;
-    ENVIRONMENT: string;
-    EMAIL_ENABLED: string;
-    DISABLE_SIGNUP_WITHOUT_INVITE?: string;
-    DISABLE_USER_CREATE_ORG?: string;
+export type Env = {
+    app: {
+        environment: string;
+        version: string;
+    },
+    server: {
+        externalPort: string;
+        nextPort: string;
+        sessionCookieName: string;
+        resourceSessionCookieName: string;
+    },
+    email: {
+        emailEnabled: boolean;
+    },
+    flags: {
+        disableSignupWithoutInvite: boolean;
+        disableUserCreateOrg: boolean;
+        emailVerificationRequired: boolean;
+    }
 };

+ 2 - 2
src/providers/EnvProvider.tsx

@@ -1,11 +1,11 @@
 "use client";
 
 import EnvContext from "@app/contexts/envContext";
-import { env } from "@app/lib/types/env";
+import { Env } from "@app/lib/types/env";
 
 interface ApiProviderProps {
     children: React.ReactNode;
-    env: env;
+    env: Env;
 }
 
 export function EnvProvider({ children, env }: ApiProviderProps) {