Compare commits

..

2 Commits

Author SHA1 Message Date
jc21
64c5a863f8 Merge pull request #4878 from NginxProxyManager/develop
v2.13.2
2025-11-09 21:16:26 +10:00
jc21
e88d55f1d2 Merge pull request #4839 from NginxProxyManager/develop
v2.13.1
2025-11-05 15:40:32 +10:00
8 changed files with 152 additions and 165 deletions

View File

@@ -1,6 +1,6 @@
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0 AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
AUTHENTIK_REDIS__HOST=authentik-redis AUTHENTIK_REDIS__HOST=authentik-redis
AUTHENTIK_POSTGRESQL__HOST=pgdb.internal AUTHENTIK_POSTGRESQL__HOST=db-postgres
AUTHENTIK_POSTGRESQL__USER=authentik AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj

View File

@@ -6,7 +6,7 @@ services:
fullstack: fullstack:
environment: environment:
DB_POSTGRES_HOST: "pgdb.internal" DB_POSTGRES_HOST: "db-postgres"
DB_POSTGRES_PORT: "5432" DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm" DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass" DB_POSTGRES_PASSWORD: "npmpass"
@@ -27,9 +27,7 @@ services:
- psql_vol:/var/lib/postgresql/data - psql_vol:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d - ./ci/postgres:/docker-entrypoint-initdb.d
networks: networks:
fulltest: - fulltest
aliases:
- pgdb.internal
authentik-redis: authentik-redis:
image: "redis:alpine" image: "redis:alpine"
@@ -43,8 +41,6 @@ services:
timeout: 3s timeout: 3s
volumes: volumes:
- redis_vol:/data - redis_vol:/data
networks:
- fulltest
authentik: authentik:
image: ghcr.io/goauthentik/server:2024.10.1 image: ghcr.io/goauthentik/server:2024.10.1
@@ -55,8 +51,6 @@ services:
depends_on: depends_on:
- authentik-redis - authentik-redis
- db-postgres - db-postgres
networks:
- fulltest
authentik-worker: authentik-worker:
image: ghcr.io/goauthentik/server:2024.10.1 image: ghcr.io/goauthentik/server:2024.10.1
@@ -67,8 +61,6 @@ services:
depends_on: depends_on:
- authentik-redis - authentik-redis
- db-postgres - db-postgres
networks:
- fulltest
authentik-ldap: authentik-ldap:
image: ghcr.io/goauthentik/ldap:2024.10.1 image: ghcr.io/goauthentik/ldap:2024.10.1
@@ -79,8 +71,6 @@ services:
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- authentik - authentik
networks:
- fulltest
volumes: volumes:
psql_vol: psql_vol:

View File

@@ -3,30 +3,31 @@
# This is a base compose file, it should be extended with a # This is a base compose file, it should be extended with a
# docker-compose.ci.*.yml file # docker-compose.ci.*.yml file
services: services:
fullstack: fullstack:
image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}"
environment: environment:
TZ: "${TZ:-Australia/Brisbane}" TZ: "${TZ:-Australia/Brisbane}"
DEBUG: "true" DEBUG: 'true'
CI: "true" CI: 'true'
FORCE_COLOR: 1 FORCE_COLOR: 1
# Required for DNS Certificate provisioning in CI # Required for DNS Certificate provisioning in CI
LE_SERVER: "https://ca.internal/acme/acme/directory" LE_SERVER: 'https://ca.internal/acme/acme/directory'
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/NginxProxyManager.crt" REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt'
volumes: volumes:
- "npm_data_ci:/data" - 'npm_data_ci:/data'
- "npm_le_ci:/etc/letsencrypt" - 'npm_le_ci:/etc/letsencrypt'
- "./dev/letsencrypt.ini:/etc/letsencrypt.ini:ro" - './dev/letsencrypt.ini:/etc/letsencrypt.ini:ro'
- "./dev/resolv.conf:/etc/resolv.conf:ro" - './dev/resolv.conf:/etc/resolv.conf:ro'
- "/etc/localtime:/etc/localtime:ro" - '/etc/localtime:/etc/localtime:ro'
healthcheck: healthcheck:
test: ["CMD", "/usr/bin/check-health"] test: ["CMD", "/usr/bin/check-health"]
interval: 10s interval: 10s
timeout: 3s timeout: 3s
expose: expose:
- "80-81/tcp" - '80-81/tcp'
- "443/tcp" - '443/tcp'
- "1500-1503/tcp" - '1500-1503/tcp'
networks: networks:
fulltest: fulltest:
aliases: aliases:
@@ -37,8 +38,8 @@ services:
stepca: stepca:
image: jc21/testca image: jc21/testca
volumes: volumes:
- "./dev/resolv.conf:/etc/resolv.conf:ro" - './dev/resolv.conf:/etc/resolv.conf:ro'
- "/etc/localtime:/etc/localtime:ro" - '/etc/localtime:/etc/localtime:ro'
networks: networks:
fulltest: fulltest:
aliases: aliases:
@@ -47,18 +48,18 @@ services:
pdns: pdns:
image: pschiffe/pdns-mysql:4.8 image: pschiffe/pdns-mysql:4.8
volumes: volumes:
- "/etc/localtime:/etc/localtime:ro" - '/etc/localtime:/etc/localtime:ro'
environment: environment:
PDNS_master: "yes" PDNS_master: 'yes'
PDNS_api: "yes" PDNS_api: 'yes'
PDNS_api_key: "npm" PDNS_api_key: 'npm'
PDNS_webserver: "yes" PDNS_webserver: 'yes'
PDNS_webserver_address: "0.0.0.0" PDNS_webserver_address: '0.0.0.0'
PDNS_webserver_password: "npm" PDNS_webserver_password: 'npm'
PDNS_webserver-allow-from: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8" PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
PDNS_version_string: "anonymous" PDNS_version_string: 'anonymous'
PDNS_default_ttl: 1500 PDNS_default_ttl: 1500
PDNS_allow_axfr_ips: "127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8" PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8'
PDNS_gmysql_host: pdns-db PDNS_gmysql_host: pdns-db
PDNS_gmysql_port: 3306 PDNS_gmysql_port: 3306
PDNS_gmysql_user: pdns PDNS_gmysql_user: pdns
@@ -75,14 +76,14 @@ services:
pdns-db: pdns-db:
image: mariadb image: mariadb
environment: environment:
MYSQL_ROOT_PASSWORD: "pdns" MYSQL_ROOT_PASSWORD: 'pdns'
MYSQL_DATABASE: "pdns" MYSQL_DATABASE: 'pdns'
MYSQL_USER: "pdns" MYSQL_USER: 'pdns'
MYSQL_PASSWORD: "pdns" MYSQL_PASSWORD: 'pdns'
volumes: volumes:
- "pdns_mysql_vol:/var/lib/mysql" - 'pdns_mysql_vol:/var/lib/mysql'
- "/etc/localtime:/etc/localtime:ro" - '/etc/localtime:/etc/localtime:ro'
- "./dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro" - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro'
networks: networks:
- fulltest - fulltest
@@ -99,12 +100,12 @@ services:
context: ../ context: ../
dockerfile: test/cypress/Dockerfile dockerfile: test/cypress/Dockerfile
environment: environment:
HTTP_PROXY: "squid:3128" HTTP_PROXY: 'squid:3128'
HTTPS_PROXY: "squid:3128" HTTPS_PROXY: 'squid:3128'
volumes: volumes:
- "cypress_logs:/test/results" - 'cypress_logs:/test/results'
- "./dev/resolv.conf:/etc/resolv.conf:ro" - './dev/resolv.conf:/etc/resolv.conf:ro'
- "/etc/localtime:/etc/localtime:ro" - '/etc/localtime:/etc/localtime:ro'
command: cypress run --browser chrome --config-file=cypress/config/ci.js command: cypress run --browser chrome --config-file=cypress/config/ci.js
networks: networks:
- fulltest - fulltest
@@ -112,9 +113,9 @@ services:
squid: squid:
image: ubuntu/squid image: ubuntu/squid
volumes: volumes:
- "./dev/squid.conf:/etc/squid/squid.conf:ro" - './dev/squid.conf:/etc/squid/squid.conf:ro'
- "./dev/resolv.conf:/etc/resolv.conf:ro" - './dev/resolv.conf:/etc/resolv.conf:ro'
- "/etc/localtime:/etc/localtime:ro" - '/etc/localtime:/etc/localtime:ro'
networks: networks:
- fulltest - fulltest

View File

@@ -32,7 +32,7 @@ services:
# DB_MYSQL_PASSWORD: 'npm' # DB_MYSQL_PASSWORD: 'npm'
# DB_MYSQL_NAME: 'npm' # DB_MYSQL_NAME: 'npm'
# db-postgres: # db-postgres:
DB_POSTGRES_HOST: "pgdb.internal" DB_POSTGRES_HOST: "db-postgres"
DB_POSTGRES_PORT: "5432" DB_POSTGRES_PORT: "5432"
DB_POSTGRES_USER: "npm" DB_POSTGRES_USER: "npm"
DB_POSTGRES_PASSWORD: "npmpass" DB_POSTGRES_PASSWORD: "npmpass"
@@ -81,6 +81,8 @@ services:
db-postgres: db-postgres:
image: postgres:17 image: postgres:17
container_name: npm2dev.db-postgres container_name: npm2dev.db-postgres
networks:
- nginx_proxy_manager
environment: environment:
POSTGRES_USER: "npm" POSTGRES_USER: "npm"
POSTGRES_PASSWORD: "npmpass" POSTGRES_PASSWORD: "npmpass"
@@ -88,10 +90,6 @@ services:
volumes: volumes:
- psql_data:/var/lib/postgresql/data - psql_data:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d - ./ci/postgres:/docker-entrypoint-initdb.d
networks:
nginx_proxy_manager:
aliases:
- pgdb.internal
stepca: stepca:
image: jc21/testca image: jc21/testca

View File

@@ -1,6 +1,15 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { ErrorNotFound, LoadingPage, Page, SiteContainer, SiteFooter, SiteHeader, Unhealthy } from "src/components"; import {
ErrorNotFound,
LoadingPage,
Page,
SiteContainer,
SiteFooter,
SiteHeader,
SiteMenu,
Unhealthy,
} from "src/components";
import { useAuthState } from "src/context"; import { useAuthState } from "src/context";
import { useHealth } from "src/hooks"; import { useHealth } from "src/hooks";
@@ -46,6 +55,7 @@ function Router() {
<Page> <Page>
<div> <div>
<SiteHeader /> <SiteHeader />
<SiteMenu />
</div> </div>
<SiteContainer> <SiteContainer>
<Suspense fallback={<LoadingPage noLogo />}> <Suspense fallback={<LoadingPage noLogo />}>

View File

@@ -1,7 +1,5 @@
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react"; import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
import cs from "classnames"; import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
import { useState } from "react";
import { LocalePicker, NavLink, SiteMenu, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context"; import { useAuthState } from "src/context";
import { useUser } from "src/hooks"; import { useUser } from "src/hooks";
import { T } from "src/locale"; import { T } from "src/locale";
@@ -12,110 +10,105 @@ export function SiteHeader() {
const { data: currentUser } = useUser("me"); const { data: currentUser } = useUser("me");
const isAdmin = currentUser?.roles.includes("admin"); const isAdmin = currentUser?.roles.includes("admin");
const { logout } = useAuthState(); const { logout } = useAuthState();
const [expanded, setExpanded] = useState(false);
return ( return (
<> <header className="navbar navbar-expand-md d-print-none">
<header className="navbar navbar-expand-md d-print-none"> <div className="container-xl">
<div className="container-xl"> <button
<button className="navbar-toggler"
className={cs("navbar-toggler", { collapsed: !expanded })} type="button"
type="button" data-bs-toggle="collapse"
data-bs-toggle="collapse" data-bs-target="#navbar-menu"
data-bs-target="#navbar-menu" aria-controls="navbar-menu"
aria-controls="navbar-menu" aria-expanded="false"
aria-expanded={expanded} aria-label="Toggle navigation"
aria-label="Toggle navigation" >
onClick={() => setExpanded(!expanded)} <span className="navbar-toggler-icon" />
> </button>
<span className="navbar-toggler-icon" /> <div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
</button> <NavLink to="/">
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3"> <div className={styles.logo}>
<NavLink to="/"> <img
<div className={styles.logo}> src="/images/logo-no-text.svg"
<img width={40}
src="/images/logo-no-text.svg" height={40}
width={40} className="navbar-brand-image"
height={40} alt="Logo"
className="navbar-brand-image" />
alt="Logo"
/>
</div>
Nginx Proxy Manager
</NavLink>
</div>
<div className="navbar-nav flex-row order-md-last">
<div className="d-none d-md-flex">
<div className="nav-item">
<LocalePicker />
</div>
<div className="nav-item">
<ThemeSwitcher />
</div>
</div> </div>
<div className="nav-item d-none d-md-flex me-3"> Nginx Proxy Manager
<div className="nav-item dropdown"> </NavLink>
<a </div>
href="/" <div className="navbar-nav flex-row order-md-last">
className="nav-link d-flex lh-1 p-0 px-2" <div className="d-none d-md-flex">
data-bs-toggle="dropdown" <div className="nav-item">
aria-label="Open user menu" <LocalePicker />
> </div>
<span <div className="nav-item">
className="avatar avatar-sm" <ThemeSwitcher />
style={{ </div>
backgroundImage: `url(${currentUser?.avatar || "/images/default-avatar.jpg"})`, </div>
}} <div className="nav-item d-none d-md-flex me-3">
/> <div className="nav-item dropdown">
<div className="d-none d-xl-block ps-2"> <a
<div>{currentUser?.nickname}</div> href="/"
<div className="mt-1 small text-secondary"> className="nav-link d-flex lh-1 p-0 px-2"
<T id={isAdmin ? "role.admin" : "role.standard-user"} /> data-bs-toggle="dropdown"
</div> aria-label="Open user menu"
>
<span
className="avatar avatar-sm"
style={{
backgroundImage: `url(${currentUser?.avatar || "/images/default-avatar.jpg"})`,
}}
/>
<div className="d-none d-xl-block ps-2">
<div>{currentUser?.nickname}</div>
<div className="mt-1 small text-secondary">
<T id={isAdmin ? "role.admin" : "role.standard-user"} />
</div> </div>
</a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
showUserModal("me");
}}
>
<IconUser width={18} />
<T id="user.edit-profile" />
</a>
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
showChangePasswordModal("me");
}}
>
<IconLock width={18} />
<T id="user.change-password" />
</a>
<div className="dropdown-divider" />
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
logout();
}}
>
<IconLogout width={18} />
<T id="user.logout" />
</a>
</div> </div>
</a>
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
showUserModal("me");
}}
>
<IconUser width={18} />
<T id="user.edit-profile" />
</a>
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
showChangePasswordModal("me");
}}
>
<IconLock width={18} />
<T id="user.change-password" />
</a>
<div className="dropdown-divider" />
<a
href="?"
className="dropdown-item"
onClick={(e) => {
e.preventDefault();
logout();
}}
>
<IconLogout width={18} />
<T id="user.logout" />
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</header> </div>
<SiteMenu mobileExpanded={expanded} setMobileExpanded={setExpanded} /> </header>
</>
); );
} }

View File

@@ -175,11 +175,7 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
); );
}; };
interface Props { export function SiteMenu() {
mobileExpanded?: boolean;
setMobileExpanded?: (expanded: boolean) => void;
}
export function SiteMenu({ mobileExpanded = false, setMobileExpanded }: Props) {
// This is hacky AF. But that's the price of using a non-react UI kit. // This is hacky AF. But that's the price of using a non-react UI kit.
const closeMenus = () => { const closeMenus = () => {
const navMenus = document.querySelectorAll(".nav-item.dropdown"); const navMenus = document.querySelectorAll(".nav-item.dropdown");
@@ -189,13 +185,12 @@ export function SiteMenu({ mobileExpanded = false, setMobileExpanded }: Props) {
if (dropdown) { if (dropdown) {
dropdown.classList.remove("show"); dropdown.classList.remove("show");
} }
setMobileExpanded?.(false);
}); });
}; };
return ( return (
<header className="navbar-expand-md"> <header className="navbar-expand-md">
<div className={cn("collapse", "navbar-collapse", { show: mobileExpanded })} id="navbar-menu"> <div className="collapse navbar-collapse">
<div className="navbar"> <div className="navbar">
<div className="container-xl"> <div className="container-xl">
<div className="row flex-column flex-md-row flex-fill align-items-center"> <div className="row flex-column flex-md-row flex-fill align-items-center">

View File

@@ -44,7 +44,7 @@ const validateEmail = () => {
if (!value.length) { if (!value.length) {
return intl.formatMessage({ id: "error.required" }); return intl.formatMessage({ id: "error.required" });
} }
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+$/i.test(value)) { if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
return intl.formatMessage({ id: "error.invalid-email" }); return intl.formatMessage({ id: "error.invalid-email" });
} }
}; };