فهرست منبع

major ui tweaks and refactoring

Milo Schwartz 6 ماه پیش
والد
کامیت
64158a823b
91فایلهای تغییر یافته به همراه1792 افزوده شده و 1247 حذف شده
  1. 2 0
      Dockerfile
  2. 1 1
      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/roles.png
  10. BIN
      public/screenshots/share-link.png
  11. BIN
      public/screenshots/sites.png
  12. BIN
      public/screenshots/users.png
  13. 1 1
      server/db/names.ts
  14. 1 1
      server/emails/sendEmail.ts
  15. 25 31
      server/emails/templates/NotifyResetPassword.tsx
  16. 31 36
      server/emails/templates/ResetPasswordCode.tsx
  17. 29 33
      server/emails/templates/ResourceOTPCode.tsx
  18. 34 42
      server/emails/templates/SendInviteLink.tsx
  19. 28 32
      server/emails/templates/TwoFactorAuthNotification.tsx
  20. 31 36
      server/emails/templates/VerifyEmailCode.tsx
  21. 18 0
      server/emails/templates/components/ButtonLink.tsx
  22. 11 0
      server/emails/templates/components/CopyCodeBox.tsx
  23. 91 0
      server/emails/templates/components/Email.tsx
  24. 0 36
      server/emails/templates/components/LetterHead.tsx
  25. 9 0
      server/emails/templates/lib/theme.ts
  26. 3 2
      src/app/[orgId]/settings/access/roles/RolesDataTable.tsx
  27. 2 2
      src/app/[orgId]/settings/access/users/InviteUserForm.tsx
  28. 3 2
      src/app/[orgId]/settings/access/users/UsersDataTable.tsx
  29. 2 2
      src/app/[orgId]/settings/access/users/UsersTable.tsx
  30. 86 66
      src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx
  31. 82 59
      src/app/[orgId]/settings/general/page.tsx
  32. 2 2
      src/app/[orgId]/settings/layout.tsx
  33. 1 1
      src/app/[orgId]/settings/page.tsx
  34. 3 2
      src/app/[orgId]/settings/resources/ResourcesDataTable.tsx
  35. 68 0
      src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx
  36. 1 1
      src/app/[orgId]/settings/resources/ResourcesTable.tsx
  37. 1 3
      src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx
  38. 318 309
      src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx
  39. 182 227
      src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
  40. 97 156
      src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
  41. 3 0
      src/app/[orgId]/settings/resources/page.tsx
  42. 3 2
      src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx
  43. 70 0
      src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx
  44. 3 0
      src/app/[orgId]/settings/share-links/page.tsx
  45. 21 0
      src/app/[orgId]/settings/sites/CreateSiteForm.tsx
  46. 3 2
      src/app/[orgId]/settings/sites/SitesDataTable.tsx
  47. 98 0
      src/app/[orgId]/settings/sites/SitesSplashCard.tsx
  48. 1 1
      src/app/[orgId]/settings/sites/SitesTable.tsx
  49. 64 41
      src/app/[orgId]/settings/sites/[niceId]/general/page.tsx
  50. 3 0
      src/app/[orgId]/settings/sites/page.tsx
  51. 15 4
      src/app/auth/login/DashboardLoginForm.tsx
  52. 4 1
      src/app/auth/login/page.tsx
  53. 0 2
      src/app/auth/reset-password/ResetPasswordForm.tsx
  54. 1 1
      src/app/auth/reset-password/page.tsx
  55. 8 7
      src/app/auth/resource/[resourceId]/page.tsx
  56. 29 12
      src/app/auth/signup/SignupForm.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"]

+ 1 - 1
README.md

@@ -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._
 

+ 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/roles.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/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) {