diff --git a/README.md b/README.md index 5baef277f..707c4b7cc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ _Your own self-hosted zero trust tunnel._ Full Documentation + | + + Contact Us + @@ -68,41 +72,17 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected ### Easy Deployment - Run on any cloud provider or on-premises. -- Docker Compose based setup for simplified deployment. +- **Docker Compose based setup** for simplified deployment. - Future-proof installation script for streamlined setup and feature additions. -- Use your preferred WireGuard client to connect, or use Newt, our custom user space client for the best experience. +- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience. ### Modular Design -- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [Fail2Ban](https://plugins.traefik.io/plugins/628c9ebcffc0cd18356a979f/fail2-ban) or [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin). +- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](github.com/PascalMinder/geoblock). + - **Automatically install and configure Crowdsec via Pangolin's installer script.** - Attach as many sites to the central server as you wish. -## Screenshots - -
- - - - - - - - - - - - - - - - - - - - - -
Sites ExampleUsers ExampleShare Link Example
SitesUsersShare Link
Authentication ExampleConnectivity Example
AuthenticationConnectivity
-
+Collage ## Deployment and Usage Example @@ -112,7 +92,7 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected > [!TIP] > Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal! -> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you sign up using [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone. +> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone. 2. **Domain Configuration**: @@ -123,10 +103,10 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected - Install Newt or use another WireGuard client on private sites. - Automatically establish a connection from these sites to the central server. -4. **Configure Users & Roles** +4. **Expose Resources**: - - Define organizations and invite users. - - Implement user- or role-based permissions to control resource access. + - Add resources to the central server and configure access control rules. + - Access these resources securely from anywhere. **Use Case Example - Bypassing Port Restrictions in Home Lab**: Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity. @@ -134,6 +114,11 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected **Use Case Example - IoT Networks**: IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups. + +Resources + +_Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resources with access control rules._ + ## Similar Projects and Inspirations **Cloudflare Tunnels**: diff --git a/config/config.example.yml b/config/config.example.yml index d60ab2ba6..d7b70a69f 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,3 +1,6 @@ +# To see all available options, please visit the docs: +# https://docs.fossorial.io/Pangolin/Configuration/config + app: dashboard_url: "http://localhost:3002" log_level: "info" diff --git a/install/config/config.yml b/install/config/config.yml index ff99b1f91..8b21b840c 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -1,3 +1,6 @@ +# To see all available options, please visit the docs: +# https://docs.fossorial.io/Pangolin/Configuration/config + app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" @@ -26,7 +29,6 @@ traefik: cert_resolver: "letsencrypt" http_entrypoint: "web" https_entrypoint: "websecure" - prefer_wildcard_cert: false gerbil: start_port: 51820 diff --git a/install/config/crowdsec/docker-compose.yml b/install/config/crowdsec/docker-compose.yml index 982b33356..d03861d49 100644 --- a/install/config/crowdsec/docker-compose.yml +++ b/install/config/crowdsec/docker-compose.yml @@ -11,8 +11,6 @@ services: ENROLL_TAGS: docker healthcheck: test: ["CMD", "cscli", "capi", "status"] - depends_on: - - gerbil # Wait for gerbil to be healthy labels: - "traefik.enable=false" # Disable traefik for crowdsec volumes: @@ -24,12 +22,5 @@ services: - ./config/crowdsec_logs/syslog:/var/log/syslog:ro # syslog - ./config/crowdsec_logs:/var/log # crowdsec logs - ./config/traefik/logs:/var/log/traefik # traefik logs - ports: - - 9090:9090 # port mapping for local firewall bouncers - - 6060:6060 # metrics endpoint for prometheus - expose: - - 9090 # http api for bouncers - - 6060 # metrics endpoint for prometheus - - 7422 # appsec waf endpoint restart: unless-stopped command: -t # Add test config flag to verify configuration \ No newline at end of file diff --git a/install/crowdsec.go b/install/crowdsec.go index 2d56ecc60..c545a90d3 100644 --- a/install/crowdsec.go +++ b/install/crowdsec.go @@ -82,6 +82,11 @@ func installCrowdsec(config Config) error { return fmt.Errorf("failed to restart containers: %v", err) } + if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") { + fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:") + fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer") + } + return nil } @@ -119,3 +124,14 @@ func GetCrowdSecAPIKey() (string, error) { return apiKey, nil } + +func checkIfTextInFile(file, text string) bool { + // Read file + content, err := os.ReadFile(file) + if err != nil { + return false + } + + // Check for text + return bytes.Contains(content, []byte(text)) +} diff --git a/public/screenshots/auth.png b/public/screenshots/auth.png deleted file mode 100644 index 1bcc35e6c..000000000 Binary files a/public/screenshots/auth.png and /dev/null differ diff --git a/public/screenshots/collage.png b/public/screenshots/collage.png new file mode 100644 index 000000000..74fe6deb5 Binary files /dev/null and b/public/screenshots/collage.png differ diff --git a/public/screenshots/connectivity.png b/public/screenshots/connectivity.png deleted file mode 100644 index 7b6ca88dd..000000000 Binary files a/public/screenshots/connectivity.png and /dev/null differ diff --git a/public/screenshots/resources.png b/public/screenshots/resources.png new file mode 100644 index 000000000..2ee2c6e24 Binary files /dev/null and b/public/screenshots/resources.png differ diff --git a/public/screenshots/share-link.png b/public/screenshots/share-link.png deleted file mode 100644 index 7515c8fe9..000000000 Binary files a/public/screenshots/share-link.png and /dev/null differ diff --git a/public/screenshots/sites.png b/public/screenshots/sites.png index eb82212ff..aa7294f5b 100644 Binary files a/public/screenshots/sites.png and b/public/screenshots/sites.png differ diff --git a/public/screenshots/users.png b/public/screenshots/users.png deleted file mode 100644 index 08a8f591b..000000000 Binary files a/public/screenshots/users.png and /dev/null differ diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts index 62850453c..bdd593f79 100644 --- a/server/auth/sessions/app.ts +++ b/server/auth/sessions/app.ts @@ -129,18 +129,19 @@ export async function invalidateAllSessions(userId: string): Promise { export function serializeSessionCookie( token: string, - isSecure: boolean + isSecure: boolean, + expiresAt: Date ): string { if (isSecure) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`; + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/;`; } } export function createBlankSessionTokenCookie(isSecure: boolean): string { if (isSecure) { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; + return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`; } diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 3336ebdeb..65f4674d2 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -167,12 +167,19 @@ export function serializeResourceSessionCookie( cookieName: string, domain: string, token: string, - isHttp: boolean = false + isHttp: boolean = false, + expiresAt?: Date ): string { if (!isHttp) { - return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`; + if (expiresAt === undefined) { + return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`; + } + return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`; } else { - return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`; + if (expiresAt === undefined) { + return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`; + } + return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`; } } diff --git a/server/emails/index.ts b/server/emails/index.ts index c1c2bc878..46d1df698 100644 --- a/server/emails/index.ts +++ b/server/emails/index.ts @@ -3,6 +3,7 @@ export * from "@server/emails/sendEmail"; import nodemailer from "nodemailer"; import config from "@server/lib/config"; import logger from "@server/logger"; +import SMTPTransport from "nodemailer/lib/smtp-transport"; function createEmailClient() { const emailConfig = config.getRawConfig().email; @@ -13,7 +14,7 @@ function createEmailClient() { return; } - return nodemailer.createTransport({ + const settings = { host: emailConfig.smtp_host, port: emailConfig.smtp_port, secure: emailConfig.smtp_secure || false, @@ -21,7 +22,15 @@ function createEmailClient() { user: emailConfig.smtp_user, pass: emailConfig.smtp_pass } - }); + } as SMTPTransport.Options; + + if (emailConfig.smtp_tls_reject_unauthorized !== undefined) { + settings.tls = { + rejectUnauthorized: emailConfig.smtp_tls_reject_unauthorized + }; + } + + return nodemailer.createTransport(settings); } export const emailClient = createEmailClient(); diff --git a/server/lib/config.ts b/server/lib/config.ts index 7d8d9c8b8..ee6d6c59c 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -14,12 +14,12 @@ import { passwordSchema } from "@server/auth/passwordSchema"; import stoi from "./stoi"; const portSchema = z.number().positive().gt(0).lte(65535); -const hostnameSchema = z - .string() - .regex( - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/ - ) - .or(z.literal("localhost")); +// const hostnameSchema = z +// .string() +// .regex( +// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/ +// ) +// .or(z.literal("localhost")); const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => { return process.env[envVar] ?? valFromYaml; @@ -42,9 +42,10 @@ const configSchema = z.object({ .record( z.string(), z.object({ - base_domain: hostnameSchema.transform((url) => - url.toLowerCase() - ), + base_domain: z + .string() + .nonempty("base_domain must not be empty") + .transform((url) => url.toLowerCase()), cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional() }) @@ -68,7 +69,7 @@ const configSchema = z.object({ const envBaseDomain = process.env.APP_BASE_DOMAIN; if (envBaseDomain) { - return hostnameSchema.safeParse(envBaseDomain).success; + return z.string().nonempty().safeParse(envBaseDomain).success; } return true; @@ -160,6 +161,7 @@ const configSchema = z.object({ smtp_user: z.string().optional(), smtp_pass: z.string().optional(), smtp_secure: z.boolean().optional(), + smtp_tls_reject_unauthorized: z.boolean().optional(), no_reply: z.string().email().optional() }) .optional(), @@ -184,7 +186,8 @@ const configSchema = z.object({ disable_signup_without_invite: z.boolean().optional(), disable_user_create_org: z.boolean().optional(), allow_raw_resources: z.boolean().optional(), - allow_base_domain_resources: z.boolean().optional() + allow_base_domain_resources: z.boolean().optional(), + allow_local_sites: z.boolean().optional() }) .optional() }); diff --git a/server/lib/validators.ts b/server/lib/validators.ts index abb2ebb45..ce677c9c0 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -56,7 +56,7 @@ export function isValidUrlGlobPattern(pattern: string): boolean { // - unreserved (A-Z a-z 0-9 - . _ ~) // - sub-delims (! $ & ' ( ) * + , ; =) // - @ : for compatibility with some systems - if (!/^[A-Za-z0-9\-._~!$&'()*+,;=@:]$/.test(char)) { + if (!/^[A-Za-z0-9\-._~!$&'()*+,;#=@:]$/.test(char)) { return false; } } diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 09bd96610..8ed49cf06 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -137,9 +137,13 @@ export async function login( } const token = generateSessionToken(); - await createSession(token, existingUser.userId); + const sess = await createSession(token, existingUser.userId); const isSecure = req.protocol === "https"; - const cookie = serializeSessionCookie(token, isSecure); + const cookie = serializeSessionCookie( + token, + isSecure, + new Date(sess.expiresAt) + ); res.appendHeader("Set-Cookie", cookie); diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 4bb5394e8..cb099162e 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -170,9 +170,13 @@ export async function signup( // }); const token = generateSessionToken(); - await createSession(token, userId); + const sess = await createSession(token, userId); const isSecure = req.protocol === "https"; - const cookie = serializeSessionCookie(token, isSecure); + const cookie = serializeSessionCookie( + token, + isSecure, + new Date(sess.expiresAt) + ); res.appendHeader("Set-Cookie", cookie); if (config.getRawConfig().flags?.require_email_verification) { diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index 093dfbb98..ad8eb9766 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -102,6 +102,8 @@ export async function exchangeSession( const token = generateSessionToken(); + let expiresAt: number | null = null; + if (requestSession.userSessionId) { const [res] = await db .select() @@ -118,6 +120,7 @@ export async function exchangeSession( expiresAt: res.expiresAt, sessionLength: SESSION_COOKIE_EXPIRES }); + expiresAt = res.expiresAt; } } else if (requestSession.accessTokenId) { const [res] = await db @@ -140,8 +143,12 @@ export async function exchangeSession( expiresAt: res.expiresAt, sessionLength: res.sessionLength }); + expiresAt = res.expiresAt; } } else { + const expires = new Date( + Date.now() + SESSION_COOKIE_EXPIRES + ).getTime(); await createResourceSession({ token, resourceId: resource.resourceId, @@ -152,11 +159,10 @@ export async function exchangeSession( whitelistId: requestSession.whitelistId, accessTokenId: requestSession.accessTokenId, doNotExtend: false, - expiresAt: new Date( - Date.now() + SESSION_COOKIE_EXPIRES - ).getTime(), + expiresAt: expires, sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES }); + expiresAt = expires; } const cookieName = `${config.getRawConfig().server.session_cookie_name}`; @@ -164,7 +170,8 @@ export async function exchangeSession( cookieName, resource.fullDomain!, token, - !resource.ssl + !resource.ssl, + expiresAt ? new Date(expiresAt) : undefined ); logger.debug(JSON.stringify("Exchange cookie: " + cookie)); diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 86ba06291..35617f370 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -384,7 +384,7 @@ async function createAccessTokenSession( tokenItem: ResourceAccessToken ) { const token = generateSessionToken(); - await createResourceSession({ + const sess = await createResourceSession({ resourceId: resource.resourceId, token, accessTokenId: tokenItem.accessTokenId, @@ -397,7 +397,8 @@ async function createAccessTokenSession( cookieName, resource.fullDomain!, token, - !resource.ssl + !resource.ssl, + new Date(sess.expiresAt) ); res.appendHeader("Set-Cookie", cookie); logger.debug("Access token is valid, creating new session"); diff --git a/src/app/[orgId]/settings/access/roles/CreateRoleForm.tsx b/src/app/[orgId]/settings/access/roles/CreateRoleForm.tsx index d95f3e202..2312d67a9 100644 --- a/src/app/[orgId]/settings/access/roles/CreateRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/CreateRoleForm.tsx @@ -7,7 +7,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { toast } from "@app/hooks/useToast"; @@ -24,11 +24,11 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role"; -import { formatAxiosError } from "@app/lib/api";; +import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -40,13 +40,13 @@ type CreateRoleFormProps = { const formSchema = z.object({ name: z.string({ message: "Name is required" }).max(32), - description: z.string().max(255).optional(), + description: z.string().max(255).optional() }); export default function CreateRoleForm({ open, setOpen, - afterCreate, + afterCreate }: CreateRoleFormProps) { const { org } = useOrgContext(); @@ -58,8 +58,8 @@ export default function CreateRoleForm({ resolver: zodResolver(formSchema), defaultValues: { name: "", - description: "", - }, + description: "" + } }); async function onSubmit(values: z.infer) { @@ -70,7 +70,7 @@ export default function CreateRoleForm({ `/org/${org?.org.orgId}/role`, { name: values.name, - description: values.description, + description: values.description } as CreateRoleBody ) .catch((e) => { @@ -80,7 +80,7 @@ export default function CreateRoleForm({ description: formatAxiosError( e, "An error occurred while creating the role." - ), + ) }); }); @@ -88,7 +88,7 @@ export default function CreateRoleForm({ toast({ variant: "default", title: "Role created", - description: "The role has been successfully created.", + description: "The role has been successfully created." }); if (open) { @@ -135,9 +135,7 @@ export default function CreateRoleForm({ Role Name - + @@ -150,9 +148,7 @@ export default function CreateRoleForm({ Description - + @@ -162,6 +158,9 @@ export default function CreateRoleForm({ + + + - - - diff --git a/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx b/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx index 6bd41df8a..80d97267d 100644 --- a/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx +++ b/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx @@ -7,7 +7,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -23,7 +23,7 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { ListRolesResponse } from "@server/routers/role"; @@ -32,10 +32,10 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectValue, + SelectValue } from "@app/components/ui/select"; import { RoleRow } from "./RolesTable"; -import { formatAxiosError } from "@app/lib/api";; +import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -47,14 +47,14 @@ type CreateRoleFormProps = { }; const formSchema = z.object({ - newRoleId: z.string({ message: "New role is required" }), + newRoleId: z.string({ message: "New role is required" }) }); export default function DeleteRoleForm({ open, roleToDelete, setOpen, - afterDelete, + afterDelete }: CreateRoleFormProps) { const { org } = useOrgContext(); @@ -66,9 +66,9 @@ export default function DeleteRoleForm({ useEffect(() => { async function fetchRoles() { const res = await api - .get>( - `/org/${org?.org.orgId}/roles` - ) + .get< + AxiosResponse + >(`/org/${org?.org.orgId}/roles`) .catch((e) => { console.error(e); toast({ @@ -77,7 +77,7 @@ export default function DeleteRoleForm({ description: formatAxiosError( e, "An error occurred while fetching the roles" - ), + ) }); }); @@ -96,8 +96,8 @@ export default function DeleteRoleForm({ const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - newRoleId: "", - }, + newRoleId: "" + } }); async function onSubmit(values: z.infer) { @@ -106,8 +106,8 @@ export default function DeleteRoleForm({ const res = await api .delete(`/role/${roleToDelete.roleId}`, { data: { - roleId: values.newRoleId, - }, + roleId: values.newRoleId + } }) .catch((e) => { toast({ @@ -116,7 +116,7 @@ export default function DeleteRoleForm({ description: formatAxiosError( e, "An error occurred while removing the role." - ), + ) }); }); @@ -124,7 +124,7 @@ export default function DeleteRoleForm({ toast({ variant: "default", title: "Role removed", - description: "The role has been successfully removed.", + description: "The role has been successfully removed." }); if (open) { @@ -214,6 +214,9 @@ export default function DeleteRoleForm({ + + + - - - diff --git a/src/app/[orgId]/settings/access/users/InviteUserForm.tsx b/src/app/[orgId]/settings/access/users/InviteUserForm.tsx index c629c0bc8..0285123a5 100644 --- a/src/app/[orgId]/settings/access/users/InviteUserForm.tsx +++ b/src/app/[orgId]/settings/access/users/InviteUserForm.tsx @@ -37,7 +37,7 @@ import { } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { ListRolesResponse } from "@server/routers/role"; -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 { Checkbox } from "@app/components/ui/checkbox"; @@ -194,9 +194,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { Email - + @@ -340,6 +338,9 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { + + + - - - diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/app/[orgId]/settings/access/users/UsersTable.tsx index 7c11c06b2..29529d66c 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersTable.tsx @@ -185,7 +185,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { - diff --git a/src/app/[orgId]/settings/access/users/[userId]/layout.tsx b/src/app/[orgId]/settings/access/users/[userId]/layout.tsx index 11ae20ac4..135c47a3e 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/layout.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/layout.tsx @@ -64,7 +64,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) { -
+

User {user?.email}

diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index b0b561a2b..b99121060 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -1,6 +1,13 @@ import { Metadata } from "next"; import { TopbarNav } from "@app/components/TopbarNav"; -import { Cog, Combine, Link, Settings, Users, Waypoints } from "lucide-react"; +import { + Cog, + Combine, + LinkIcon, + Settings, + Users, + Waypoints +} from "lucide-react"; import { Header } from "@app/components/Header"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; @@ -11,6 +18,14 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; import { GetOrgUserResponse } from "@server/routers/user"; import UserProvider from "@app/providers/UserProvider"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import Link from "next/link"; export const dynamic = "force-dynamic"; @@ -38,7 +53,7 @@ const topNavItems = [ { title: "Shareable Links", href: "/{orgId}/settings/share-links", - icon: + icon: }, { title: "General", @@ -95,19 +110,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <> -
-
-
- -
- +
+
+
+
+ +
+ +
+
-
-
- {children} +
+
+ {children} +
); diff --git a/src/app/[orgId]/settings/resources/CreateResourceForm.tsx b/src/app/[orgId]/settings/resources/CreateResourceForm.tsx index ea8542f6b..9bb3ecdea 100644 --- a/src/app/[orgId]/settings/resources/CreateResourceForm.tsx +++ b/src/app/[orgId]/settings/resources/CreateResourceForm.tsx @@ -66,6 +66,8 @@ import CopyTextBox from "@app/components/CopyTextBox"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Label } from "@app/components/ui/label"; import { ListDomainsResponse } from "@server/routers/domain"; +import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; +import { StrategySelect } from "@app/components/StrategySelect"; const createResourceFormSchema = z .object({ @@ -140,6 +142,7 @@ export default function CreateResourceForm({ const [domainType, setDomainType] = useState<"subdomain" | "basedomain">( "subdomain" ); + const [loadingPage, setLoadingPage] = useState(true); const form = useForm({ resolver: zodResolver(createResourceFormSchema), @@ -215,8 +218,17 @@ export default function CreateResourceForm({ } }; - fetchSites(); - fetchDomains(); + const load = async () => { + setLoadingPage(true); + + await fetchSites(); + await fetchDomains(); + await new Promise((r) => setTimeout(r, 200)); + + setLoadingPage(false); + }; + + load(); }, [open]); async function onSubmit(data: CreateResourceFormValues) { @@ -231,7 +243,7 @@ export default function CreateResourceForm({ protocol: data.protocol, proxyPort: data.http ? undefined : data.proxyPort, siteId: data.siteId, - isBaseDomain: data.http ? undefined : data.isBaseDomain + isBaseDomain: data.http ? data.isBaseDomain : undefined } ) .catch((e) => { @@ -253,6 +265,7 @@ export default function CreateResourceForm({ goToResource(id); } else { setShowSnippets(true); + router.refresh(); } } } @@ -262,6 +275,21 @@ export default function CreateResourceForm({ router.push(`/${orgId}/settings/resources/${id || resourceId}`); } + const launchOptions = [ + { + id: "http", + title: "HTTPS Resource", + description: + "Proxy requests to your app over HTTPS using a subdomain or base domain." + }, + { + id: "raw", + title: "Raw TCP/UDP Resource", + description: + "Proxy requests to your app over TCP/UDP using a port number." + } + ]; + return ( <> - {!showSnippets && ( -
- - {!env.flags.allowRawResources || ( - ( - -
- - HTTP Resource - - - Toggle if this is an - HTTP resource or a - raw TCP/UDP - resource. - -
- - - -
+ {loadingPage ? ( + + ) : ( +
+ {!showSnippets && ( + + - )} - - ( - - Name - - - - - - This is display name for the - resource. - - - )} - /> - - {form.watch("http") && - env.flags.allowBaseDomainResources && ( -
- { - setDomainType( - val as any - ); - form.setValue( - "isBaseDomain", - val === "basedomain" - ); - }} - > -
- - -
-
- - -
-
-
- )} - - {form.watch("http") && ( - <> - {domainType === "subdomain" ? ( -
- {!env.flags - .allowBaseDomainResources && ( + className="space-y-4" + id="create-resource-form" + > + ( + - Subdomain + Name - )} -
-
- ( - - - - )} - /> -
-
- ( - - - - - )} - /> -
-
-
- ) : ( - ( - - + + + + )} + /> + + ( + + + Site + + + - - - + - - {baseDomains.map( - ( - option - ) => ( - - { - option.baseDomain - } - - ) - )} - - - - - )} - /> - )} - - )} + + + + + + + No + site + found. + + + {sites.map( + ( + site + ) => ( + { + form.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + + This site will + provide connectivity + to the resource. + + + )} + /> + + {!env.flags.allowRawResources || ( +
+ + Resource Type + + + form.setValue( + "http", + value === "http" + ) + } + /> + + You cannot change the + type of resource after + creation. + +
+ )} + + {form.watch("http") && + env.flags + .allowBaseDomainResources && ( + ( + + + Domain Type + + + + + )} + /> + )} + + {form.watch("http") && ( + <> + {domainType === + "subdomain" ? ( +
+ + Subdomain + +
+
+ ( + + + + + + + )} + /> +
+
+ ( + + + + + )} + /> +
+
+
+ ) : ( + ( + + + Base + Domain + + + + + )} + /> + )} + + )} + + {!form.watch("http") && ( + <> + ( + + + Protocol + + + + + )} + /> + ( + + + Port Number + + + + field.onChange( + e + .target + .value + ? parseInt( + e + .target + .value + ) + : null + ) + } + /> + + + + The external + port number + to proxy + requests. + + + )} + /> + + )} + + + )} + + {showSnippets && ( +
+
+
+

+ Traefik: Add Entrypoints +

+ +
+
+ +
+
+

+ Gerbil: Expose Ports in + Docker Compose +

+ +
+
- {!form.watch("http") && ( - )} - - {!form.watch("http") && ( - <> - ( - - - Protocol - - - - - The protocol to use - for the resource. - - - )} - /> - ( - - - Port Number - - - - field.onChange( - e.target - .value - ? parseInt( - e - .target - .value - ) - : null - ) - } - /> - - - - The port number to - proxy requests to - (required for - non-HTTP resources). - - - )} - /> - - )} - - ( - - Site - - - - - - - - - - - - No site - found. - - - {sites.map( - ( - site - ) => ( - { - form.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - This site will provide - connectivity to the - resource. - - - )} - /> - - - )} - - {showSnippets && ( -
-
-
- 1
-
-

- Traefik: Add Entrypoints -

- -
-
- -
-
- 2 -
-
-

- Gerbil: Expose Ports in Docker - Compose -

- -
-
- - - - Make sure to follow the full guide - - - + )}
)} + + + {!showSnippets && ( - diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 848838b34..6ff9e730f 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -233,7 +233,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { - diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx index 35eb29a31..f3ab705cf 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx @@ -8,7 +8,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { toast } from "@app/hooks/useToast"; @@ -24,22 +24,22 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; -import { formatAxiosError } from "@app/lib/api";; +import { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { Resource } from "@server/db/schema"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; const setPasswordFormSchema = z.object({ - password: z.string().min(4).max(100), + password: z.string().min(4).max(100) }); type SetPasswordFormValues = z.infer; const defaultValues: Partial = { - password: "", + password: "" }; type SetPasswordFormProps = { @@ -53,7 +53,7 @@ export default function SetResourcePasswordForm({ open, setOpen, resourceId, - onSetPassword, + onSetPassword }: SetPasswordFormProps) { const api = createApiClient(useEnvContext()); @@ -61,7 +61,7 @@ export default function SetResourcePasswordForm({ const form = useForm({ resolver: zodResolver(setPasswordFormSchema), - defaultValues, + defaultValues }); useEffect(() => { @@ -76,7 +76,7 @@ export default function SetResourcePasswordForm({ setLoading(true); api.post>(`/resource/${resourceId}/password`, { - password: data.password, + password: data.password }) .catch((e) => { toast({ @@ -85,14 +85,14 @@ export default function SetResourcePasswordForm({ description: formatAxiosError( e, "An error occurred while setting the resource password" - ), + ) }); }) .then(() => { toast({ title: "Resource password set", description: - "The resource password has been set successfully", + "The resource password has been set successfully" }); if (onSetPassword) { @@ -153,6 +153,9 @@ export default function SetResourcePasswordForm({ + + + - - - diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx index 4a850b335..9d89d3bd2 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx @@ -8,7 +8,7 @@ import { FormField, FormItem, FormLabel, - FormMessage, + FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { toast } from "@app/hooks/useToast"; @@ -24,27 +24,27 @@ import { CredenzaDescription, CredenzaFooter, CredenzaHeader, - CredenzaTitle, + CredenzaTitle } from "@app/components/Credenza"; -import { formatAxiosError } from "@app/lib/api";; +import { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { Resource } from "@server/db/schema"; import { InputOTP, InputOTPGroup, - InputOTPSlot, + InputOTPSlot } from "@app/components/ui/input-otp"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; const setPincodeFormSchema = z.object({ - pincode: z.string().length(6), + pincode: z.string().length(6) }); type SetPincodeFormValues = z.infer; const defaultValues: Partial = { - pincode: "", + pincode: "" }; type SetPincodeFormProps = { @@ -58,7 +58,7 @@ export default function SetResourcePincodeForm({ open, setOpen, resourceId, - onSetPincode, + onSetPincode }: SetPincodeFormProps) { const [loading, setLoading] = useState(false); @@ -66,7 +66,7 @@ export default function SetResourcePincodeForm({ const form = useForm({ resolver: zodResolver(setPincodeFormSchema), - defaultValues, + defaultValues }); useEffect(() => { @@ -81,7 +81,7 @@ export default function SetResourcePincodeForm({ setLoading(true); api.post>(`/resource/${resourceId}/pincode`, { - pincode: data.pincode, + pincode: data.pincode }) .catch((e) => { toast({ @@ -89,15 +89,15 @@ export default function SetResourcePincodeForm({ title: "Error setting resource PIN code", description: formatAxiosError( e, - "An error occurred while setting the resource PIN code", - ), + "An error occurred while setting the resource PIN code" + ) }); }) .then(() => { toast({ title: "Resource PIN code set", description: - "The resource pincode has been set successfully", + "The resource pincode has been set successfully" }); if (onSetPincode) { @@ -181,6 +181,9 @@ export default function SetResourcePincodeForm({ + + + - - - diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index fa0491f08..c50afc4d3 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -38,7 +38,8 @@ import { SettingsSectionHeader, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter + SettingsSectionFooter, + SettingsSectionForm } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { InfoPopup } from "@app/components/ui/info-popup"; @@ -438,6 +439,7 @@ export default function ResourceAuthenticationPage() { setActiveRolesTagIndex } placeholder="Select a role" + size="sm" tags={ usersRolesForm.getValues() .roles @@ -466,14 +468,6 @@ export default function ResourceAuthenticationPage() { 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" - }} /> @@ -504,6 +498,7 @@ export default function ResourceAuthenticationPage() { usersRolesForm.getValues() .users } + size="sm" setTags={( newUsers ) => { @@ -528,14 +523,6 @@ export default function ResourceAuthenticationPage() { 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" - }} /> @@ -582,7 +569,7 @@ export default function ResourceAuthenticationPage() {
- - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column - .columnDef.header, - header.getContext() - )} - +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + ))} - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row - .getVisibleCells() - .map((cell) => ( - - {flexRender( - cell.column - .columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No targets. Add a target using the - form. - - - )} - -
-
-

- Adding more than one target above will enable load - balancing. -

+ )) + ) : ( + + + No targets. Add a target using the form. + + + )} + + + Adding more than one target above will enable load + balancing. + + - - + {resource.http && ( + <> + {env.flags + .allowBaseDomainResources && ( + ( + + + Domain Type + + + + + )} + /> + )} - - - - Transfer Resource - - - Transfer this resource to a different site - - - - - -
- - ( - - - Destination Site - - - - - - - - - - - - No sites found. - - - {sites.map( - (site) => ( - { - transferForm.setValue( - "siteId", - site.siteId - ); - setOpen( - false - ); - }} - > - { - site.name - } - + {domainType === "subdomain" ? ( +
+ + Subdomain + +
+
+ ( + + + + + + + )} + /> +
+
+ ( + + + + + )} + /> +
+
+
+ ) : ( + ( + + + Base Domain + + + + + )} + /> + )} +
+ )} - /> - - - - - - - - - + {!resource.http && ( + ( + + + Port Number + + + + field.onChange( + e.target + .value + ? parseInt( + e + .target + .value + ) + : null + ) + } + /> + + + + )} + /> + )} + + + + + + + + + + + + + + Transfer Resource + + + Transfer this resource to a different site + + + + + +
+ + ( + + + Destination Site + + + + + + + + + + + + No sites found. + + + {sites.map( + (site) => ( + { + transferForm.setValue( + "siteId", + site.siteId + ); + setOpen( + false + ); + }} + > + { + site.name + } + + + ) + )} + + + + + + + )} + /> + + +
+
+ + + + +
+ + ) ); } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx index 51b147fec..af335c421 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx @@ -130,9 +130,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { -
- -
+ {children}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx index 2a4d0e514..7632a0074 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -33,6 +33,7 @@ import { import { Table, TableBody, + TableCaption, TableCell, TableContainer, TableHead, @@ -94,7 +95,7 @@ enum RuleAction { enum RuleMatch { PATH = "Path", IP = "IP", - CIDR = "IP Range", + CIDR = "IP Range" } export default function ResourceRules(props: { @@ -623,7 +624,7 @@ export default function ResourceRules(props: { onSubmit={addRuleForm.handleSubmit(addRule)} className="space-y-4" > -
+
)} /> +
- - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column - .columnDef.header, - header.getContext() - )} - +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + ))} - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row - .getVisibleCells() - .map((cell) => ( - - {flexRender( - cell.column - .columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No rules. Add a rule using the form. - - - )} - -
-
-

- Rules are evaluated by priority in ascending order. -

+ )) + ) : ( + + + No rules. Add a rule using the form. + + + )} + + + Rules are evaluated by priority in ascending order. + + + - - - diff --git a/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx b/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx index 5d5d341f1..2acc1399a 100644 --- a/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx +++ b/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx @@ -273,7 +273,21 @@ export default function ShareLinksTable({ } return "Never"; } + }, + { + id: "delete", + cell: ({ row }) => ( +
+ +
+ ) } + ]; return ( diff --git a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx index 0a4cca141..546b2a5bf 100644 --- a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx @@ -41,6 +41,7 @@ import Link from "next/link"; import { ArrowUpRight, ChevronsUpDown, + Loader2, SquareArrowOutUpRight } from "lucide-react"; import { @@ -48,6 +49,7 @@ import { CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; +import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; const createSiteFormSchema = z.object({ name: z @@ -97,6 +99,8 @@ export default function CreateSiteForm({ const [siteDefaults, setSiteDefaults] = useState(null); + const [loadingPage, setLoadingPage] = useState(true); + const handleCheckboxChange = (checked: boolean) => { // setChecked?.(checked); setIsChecked(checked); @@ -121,27 +125,36 @@ export default function CreateSiteForm({ useEffect(() => { if (!open) return; - // reset all values - setLoading?.(false); - setIsLoading(false); - form.reset(); - setChecked?.(false); - setKeypair(null); - setSiteDefaults(null); + const load = async () => { + setLoadingPage(true); + // reset all values + setLoading?.(false); + setIsLoading(false); + form.reset(); + setChecked?.(false); + setKeypair(null); + setSiteDefaults(null); - const generatedKeypair = generateKeypair(); - setKeypair(generatedKeypair); + const generatedKeypair = generateKeypair(); + setKeypair(generatedKeypair); - api.get(`/org/${orgId}/pick-site-defaults`) - .catch((e) => { - // update the default value of the form to be local method - form.setValue("method", "local"); - }) - .then((res) => { - if (res && res.status === 200) { - setSiteDefaults(res.data.data); - } - }); + await api + .get(`/org/${orgId}/pick-site-defaults`) + .catch((e) => { + // update the default value of the form to be local method + form.setValue("method", "local"); + }) + .then((res) => { + if (res && res.status === 200) { + setSiteDefaults(res.data.data); + } + }); + await new Promise((resolve) => setTimeout(resolve, 200)); + + setLoadingPage(false); + }; + + load(); }, [open]); async function onSubmit(data: CreateSiteFormValues) { @@ -257,7 +270,9 @@ PersistentKeepalive = 5` const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`; - return ( + return loadingPage ? ( + + ) : (
- This is the the display name for the - site. + This is the the display name for the site. )} @@ -331,7 +345,6 @@ PersistentKeepalive = 5` rel="noopener noreferrer" > - {" "} Learn how to install Newt on your system @@ -358,12 +371,16 @@ PersistentKeepalive = 5` onOpenChange={setIsOpen} className="space-y-2" > -
+
+ + You will only be able to see the + configuration once. +
- - You will only be able to see the - configuration once. - ) : null}
diff --git a/src/app/[orgId]/settings/sites/CreateSiteModal.tsx b/src/app/[orgId]/settings/sites/CreateSiteModal.tsx index fd6ff9144..1666000d0 100644 --- a/src/app/[orgId]/settings/sites/CreateSiteModal.tsx +++ b/src/app/[orgId]/settings/sites/CreateSiteModal.tsx @@ -58,6 +58,9 @@ export default function CreateSiteFormModal({
+ + + - - - diff --git a/src/app/[orgId]/settings/sites/SitesTable.tsx b/src/app/[orgId]/settings/sites/SitesTable.tsx index d9d0ba038..9b56aaebb 100644 --- a/src/app/[orgId]/settings/sites/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesTable.tsx @@ -268,7 +268,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { - diff --git a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx index 7b0fa46dd..6b6a58e25 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx @@ -68,9 +68,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { -
- -
+ {children}
diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index 67ee0b028..7d68263e3 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -198,8 +198,7 @@ export default function VerifyEmailForm({ We sent a verification code to your - email address. Please enter the code - to verify your email address. + email address. )} diff --git a/src/app/globals.css b/src/app/globals.css index bcb475100..cb32e061b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -21,8 +21,8 @@ --accent-foreground: 24 9.8% 10%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 60 9.1% 97.8%; - --border: 20 5.9% 85%; - --input: 20 5.9% 85%; + --border: 20 5.9% 80%; + --input: 20 5.9% 75%; --ring: 24.6 95% 53.1%; --radius: 0.75rem; --chart-1: 12 76% 61%; @@ -49,8 +49,8 @@ --accent-foreground: 60 9.1% 97.8%; --destructive: 0 72.2% 50.6%; --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 25.0%; - --input: 12 6.5% 25.0%; + --border: 12 6.5% 30.0%; + --input: 12 6.5% 35.0%; --ring: 20.5 90.2% 48.2%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 02a9dabad..a892ccef2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,11 +37,11 @@ export default async function RootLayout({ > {/* Main content */} -
{children}
+
{children}
{/* Footer */} -