diff --git a/backend/app.js b/backend/app.js index 3039bbb8..b1f04766 100644 --- a/backend/app.js +++ b/backend/app.js @@ -62,7 +62,7 @@ app.use("/", mainRoutes); app.use((err, req, res, _) => { const payload = { error: { - code: err.status, + code: err.status || 500, message: err.public ? err.message : "Internal Error", }, }; diff --git a/backend/certbot/dns-plugins.json b/backend/certbot/dns-plugins.json index 4cf2c878..974c7265 100644 --- a/backend/certbot/dns-plugins.json +++ b/backend/certbot/dns-plugins.json @@ -335,6 +335,14 @@ "package_name": "certbot-dns-hetzner-cloud", "version": "~=1.0.4" }, + "hostinger": { + "credentials": "dns_hostinger_api_token = 0123456789abcdef0123456789abcdef", + "dependencies": "", + "full_plugin_name": "dns-hostinger", + "name": "Hostinger.com", + "package_name": "certbot-dns-hostinger", + "version": "~=0.1.5" + }, "hostingnl": { "credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef", "dependencies": "", diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 59c13bf6..6498422c 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -614,7 +614,7 @@ const internalCertificate = { const certificate = await internalCertificate.update(access, { id: data.id, expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), - domain_names: [validations.certificate.cn], + domain_names: validations.certificate.cn ? [validations.certificate.cn] : [], meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later }); @@ -683,13 +683,15 @@ const internalCertificate = { try { const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]); + // Examples: // subject=CN = *.jc21.com // subject=CN = something.example.com - const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; + // subject=CN=*.jc21.com + const regex = /(?:subject=)?[^=]+=\s*(\S+)/gim; const match = regex.exec(result); if (match && typeof match[1] !== "undefined") { - certData.cn = match[1]; + certData.cn = match[1].trim(); } const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]); @@ -779,6 +781,7 @@ const internalCertificate = { const args = [ "certonly", + "-n", // non-interactive "--config", letsencryptConfig, "--work-dir", @@ -834,6 +837,7 @@ const internalCertificate = { const args = [ "certonly", + "-n", // non-interactive "--config", letsencryptConfig, "--work-dir", diff --git a/backend/models/certificate.js b/backend/models/certificate.js index ad6e0a65..9a349f30 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -15,6 +15,16 @@ Model.knex(db()); const boolFields = ["is_deleted"]; +const cleanDomainNames = (domainNames) => { + // Sort domain_names + if (typeof domainNames !== "undefined") { + const newDomainNames = domainNames.filter((name) => name != null); + newDomainNames.sort(); + return newDomainNames; + } + return []; +}; + class Certificate extends Model { $beforeInsert() { this.created_on = now(); @@ -26,25 +36,17 @@ class Certificate extends Model { } // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } + this.domain_names = cleanDomainNames(this.domain_names); // Default for meta if (typeof this.meta === "undefined") { this.meta = {}; } - - this.domain_names.sort(); } $beforeUpdate() { this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== "undefined") { - this.domain_names.sort(); - } + this.domain_names = cleanDomainNames(this.domain_names); } $parseDatabaseJson(json) { diff --git a/backend/schema/common.json b/backend/schema/common.json index 00b06e00..e56c9f70 100644 --- a/backend/schema/common.json +++ b/backend/schema/common.json @@ -77,14 +77,12 @@ "example": 3 }, "domain_names": { - "description": "Domain Names separated by a comma", + "description": "Domain Names array", "type": "array", - "minItems": 1, - "maxItems": 100, "uniqueItems": true, "items": { "type": "string", - "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" + "minLength": 1 }, "example": ["example.com", "www.example.com"] }, diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json index 80cd92be..4ef666b4 100644 --- a/backend/schema/components/certificate-object.json +++ b/backend/schema/components/certificate-object.json @@ -25,15 +25,7 @@ "example": "My Custom Cert" }, "domain_names": { - "description": "Domain Names separated by a comma", - "type": "array", - "maxItems": 100, - "uniqueItems": true, - "items": { - "type": "string", - "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" - }, - "example": ["example.com", "www.example.com"] + "$ref": "../common.json#/properties/domain_names" }, "expires_on": { "description": "Date and time of expiration", diff --git a/backend/templates/_access.conf b/backend/templates/_access.conf index 4f388545..81983f3a 100644 --- a/backend/templates/_access.conf +++ b/backend/templates/_access.conf @@ -10,11 +10,13 @@ {% endif %} + {% if access_list.clients.length > 0 %} # Access Rules: {{ access_list.clients | size }} total {% for client in access_list.clients %} {{client | nginxAccessRule}} {% endfor %} deny all; + {% endif %} # Access checks must... {% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %} diff --git a/docs/src/third-party/index.md b/docs/src/third-party/index.md index cd54b45b..c08a1caf 100644 --- a/docs/src/third-party/index.md +++ b/docs/src/third-party/index.md @@ -14,7 +14,9 @@ Known integrations: - [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) - [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager) - [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) +- [selfhosting.sh Nginx Proxy Manager Guide](https://selfhosting.sh/apps/nginx-proxy-manager/) - Complete Docker Compose setup guide with SSL configuration, access lists, and proxy host management. +- [NPM Auth Gateway](https://github.com/Mark0025/npm-auth-gateway) — User-level access control with auto IP whitelisting via auth providers. [Details](/third-party/npm-auth-gateway) If you would like your integration of NPM listed, please open a -[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) +[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) \ No newline at end of file diff --git a/docs/src/third-party/npm-auth-gateway.md b/docs/src/third-party/npm-auth-gateway.md new file mode 100644 index 00000000..7dce9327 --- /dev/null +++ b/docs/src/third-party/npm-auth-gateway.md @@ -0,0 +1,83 @@ +--- +outline: deep +--- + +# NPM Auth Gateway + +User-level access control for Nginx Proxy Manager with auto IP whitelisting. + +**Repository:** [github.com/Mark0025/npm-auth-gateway](https://github.com/Mark0025/npm-auth-gateway) + +## What It Does + +NPM Auth Gateway is a companion app that adds user management on top of NPM's access list system. Instead of manually adding IPs to access lists, users log in through an auth provider and their IP is automatically whitelisted on the access lists they've been assigned to. + +NPM remains fully in control — the gateway only reads and writes through NPM's REST API. All access enforcement stays in NPM's nginx config. + +## The Problem It Solves + +- **Manual IP management** — every time a user needs access, an admin manually adds their IP to an access list +- **IP changes** — mobile users, VPNs, and travel mean IPs change constantly +- **No user visibility** — access lists contain IPs, but there's no record of who each IP belongs to +- **Scaling** — managing 10+ users across multiple access lists gets tedious + +## How It Works + +``` +Browser → NPM (SSL) → Auth Gateway → Auth Provider + ↓ + NPM REST API (:81) + auto-add IP to access lists +``` + +1. Admin creates a user by email +2. Admin assigns access — a table of proxy hosts with checkboxes showing which hosts each access list protects +3. User logs in → IP detected → automatically added to their assigned NPM access lists +4. User's IP changes → they log in again → new IP auto-added +5. Admin revokes access → user's IPs removed from all access lists + +## Key Features + +- **Auto IP whitelisting** on login +- **Per-host access control** with checkboxes (not abstract groups) +- **Admin/user roles** — admin sees everything, users see only their assigned hosts +- **Personalized dashboard** — users see their services as clickable cards +- **Login logging** with IP history per user +- **One-click revoke** — removes user's IPs from all access lists +- **Searchable tables** for proxy hosts and users +- **Survives gateway failure** — NPM keeps enforcing existing whitelists + +## Architecture + +| Responsibility | Who Handles It | +|---|---| +| SSL termination | **NPM** | +| Proxy host configuration | **NPM** | +| Access list enforcement | **NPM** | +| IP whitelisting | **NPM** | +| User identity | **Auth Provider** | +| User → access list mapping | **Gateway** | +| Auto IP whitelisting | **Gateway** | + +**No database required.** NPM stores all proxy/ACL config. User metadata lives in the auth provider. Zero state duplication. + +## NPM API Endpoints Used + +| Endpoint | Purpose | +|---|---| +| `POST /api/tokens` | Authentication | +| `GET /api/nginx/proxy-hosts` | List proxy hosts | +| `GET /api/nginx/access-lists` | List access lists | +| `PUT /api/nginx/access-lists/:id` | Update access list IPs | +| `POST /api/nginx/access-lists` | Create access list | +| `GET /api/nginx/certificates` | List SSL certificates | + +## Setup + +See the [repository README](https://github.com/Mark0025/npm-auth-gateway) for Docker deployment instructions. + +## Tech Stack + +Next.js / React / TypeScript / Docker + +The auth provider is swappable — the proof of concept uses Clerk, but any OIDC provider works (Auth0, Keycloak, Authentik, etc.). diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index e5cf8d73..bfc7951a 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -231,7 +231,7 @@ "defaultMessage": "Type de clé" }, "certificates.key-type-description": { - "defaultMessage": "RSA est largement compatible, ECDSA est plus rapide et plus sécurisé mais peut ne pas être pris en charge par les anciens systèmes" + "defaultMessage": "RSA est largement répandu, ECDSA est plus rapide et plus sécurisé, mais peut ne pas être supporté par les systèmes plus anciens" }, "certificates.key-type-ecdsa": { "defaultMessage": "ECDSA 256" diff --git a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx index 68af43e1..5d6602e2 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx +++ b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx @@ -99,14 +99,27 @@ export default function TableWrapper() { isFiltered={!!search} isFetching={isFetching} onEdit={(id: number) => showProxyHostModal(id)} - onDelete={(id: number) => + onDelete={(id: number) => { + const host = data?.find((h) => h.id === id); showDeleteConfirmModal({ title: , onConfirm: () => handleDelete(id), invalidations: [["proxy-hosts"], ["proxy-host", id]], - children: , - }) - } + children: ( + <> + + {host?.domainNames?.length ? ( +
{host.domainNames.join(", ")}
+ ) : null} + {host?.forwardHost ? ( +
+ ({host.forwardScheme}://{host.forwardHost}:{host.forwardPort}) +
+ ) : null} + + ), + }); + }} onDisableToggle={handleDisableToggle} onNew={() => showProxyHostModal("new")} /> diff --git a/test/cypress/Dockerfile b/test/cypress/Dockerfile index bd821085..cf071baf 100644 --- a/test/cypress/Dockerfile +++ b/test/cypress/Dockerfile @@ -1,4 +1,4 @@ -FROM cypress/included:15.11.0 +FROM cypress/included:15.15.0 # Disable Cypress CLI colors ENV FORCE_COLOR=0 diff --git a/test/cypress/config/ci.mjs b/test/cypress/config/ci.mjs index 2597c194..ea203a89 100644 --- a/test/cypress/config/ci.mjs +++ b/test/cypress/config/ci.mjs @@ -2,6 +2,7 @@ import { defineConfig } from 'cypress'; import pluginSetup from '../plugins/index.mjs'; export default defineConfig({ + allowCypressEnv: false, requestTimeout: 30000, defaultCommandTimeout: 20000, reporter: "cypress-multi-reporters", diff --git a/test/cypress/config/dev.mjs b/test/cypress/config/dev.mjs index f32d973c..636c3095 100644 --- a/test/cypress/config/dev.mjs +++ b/test/cypress/config/dev.mjs @@ -2,6 +2,7 @@ import { defineConfig } from 'cypress'; import pluginSetup from '../plugins/index.mjs'; export default defineConfig({ + allowCypressEnv: false, requestTimeout: 30000, defaultCommandTimeout: 20000, reporter: "cypress-multi-reporters", diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 00da38b2..ce4200fb 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -5,6 +5,7 @@ describe('Certificates endpoints', () => { let certID; before(() => { + cy.createCustomCerts(); cy.resetUsers(); cy.getToken().then((tok) => { token = tok; diff --git a/test/cypress/e2e/api/Ldap.cy.js b/test/cypress/e2e/api/Ldap.cy.js deleted file mode 100644 index 64e17730..00000000 --- a/test/cypress/e2e/api/Ldap.cy.js +++ /dev/null @@ -1,65 +0,0 @@ -/// - -describe('LDAP with Authentik', () => { - let _token; - if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { - - before(() => { - cy.resetUsers(); - cy.getToken().then((tok) => { - _token = tok; - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/ldap-auth', - // data: { - // value: { - // host: 'authentik-ldap:3389', - // base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io', - // user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io', - // email_property: 'mail', - // name_property: 'sn', - // self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))', - // auto_create_user: true - // } - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/auth-methods', - // data: { - // value: [ - // 'local', - // 'ldap' - // ] - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - }); - }); - - it.skip('Should log in with LDAP', () => { - // cy.task('backendApiPost', { - // token: token, - // path: '/api/auth', - // data: { - // // Authentik LDAP creds: - // type: 'ldap', - // identity: 'cypress', - // secret: 'fqXBfUYqHvYqiwBHWW7f' - // } - // }).then((data) => { - // cy.validateSwaggerSchema('post', 200, '/auth', data); - // expect(data.result).to.have.property('token'); - // }); - }); - } -}); diff --git a/test/cypress/e2e/api/OAuth.cy.js b/test/cypress/e2e/api/OAuth.cy.js deleted file mode 100644 index c5c819f9..00000000 --- a/test/cypress/e2e/api/OAuth.cy.js +++ /dev/null @@ -1,97 +0,0 @@ -/// - -describe('OAuth with Authentik', () => { - let _token; - if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { - - before(() => { - cy.getToken().then((tok) => { - _token = tok; - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/oauth-auth', - // data: { - // value: { - // client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU', - // client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq', - // authorization_url: 'http://authentik:9000/application/o/authorize/', - // resource_url: 'http://authentik:9000/application/o/userinfo/', - // token_url: 'http://authentik:9000/application/o/token/', - // logout_url: 'http://authentik:9000/application/o/npm/end-session/', - // identifier: 'preferred_username', - // scopes: [], - // auto_create_user: true - // } - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/auth-methods', - // data: { - // value: [ - // 'local', - // 'oauth' - // ] - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - }); - }); - - it.skip('Should log in with OAuth', () => { - // cy.task('backendApiGet', { - // path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')), - // }).then((data) => { - // expect(data).to.have.property('result'); - - // cy.origin('http://authentik:9000', {args: data.result}, (url) => { - // cy.visit(url); - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-identification') - // .shadow() - // .find('input[name="uidField"]', { visible: true }) - // .type('cypress'); - - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-identification') - // .shadow() - // .find('button[type="submit"]', { visible: true }) - // .click(); - - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-password') - // .shadow() - // .find('input[name="password"]', { visible: true }) - // .type('fqXBfUYqHvYqiwBHWW7f'); - - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-password') - // .shadow() - // .find('button[type="submit"]', { visible: true }) - // .click(); - // }) - - // // we should be logged in - // cy.get('#root p.chakra-text') - // .first() - // .should('have.text', 'Nginx Proxy Manager'); - - // // logout: - // cy.clearLocalStorage(); - // }); - }); - } -}); diff --git a/test/cypress/e2e/api/Streams.cy.js b/test/cypress/e2e/api/Streams.cy.js index 10809f89..b2c97b44 100644 --- a/test/cypress/e2e/api/Streams.cy.js +++ b/test/cypress/e2e/api/Streams.cy.js @@ -23,11 +23,13 @@ describe('Streams', () => { }); // Create a custom cert pair - cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => { - expect(result.exitCode).to.eq(0); - // Install CA - cy.exec('mkcert -install').then((result) => { + cy.task('getFixturesFolder').then((fixturesFolder) => { + cy.exec(`mkcert -cert-file=${fixturesFolder}/website1.pem -key-file=${fixturesFolder}/website1.key.pem website1.example.com`).then((result) => { expect(result.exitCode).to.eq(0); + // Install CA + cy.exec('mkcert -install').then((result) => { + expect(result.exitCode).to.eq(0); + }); }); }); diff --git a/test/cypress/fixtures/test.example.com-key.pem b/test/cypress/fixtures/test.example.com-key.pem deleted file mode 100644 index 307cdc30..00000000 --- a/test/cypress/fixtures/test.example.com-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd -qACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w -rbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge -Yz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ -oxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z -Eo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X -zGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU -ia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6 -YHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe -a0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu -W0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw -o72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW -H8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+ -N+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh -ELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU -MDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31 -qjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq -cMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9 -vMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO -utTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V -g0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1 -mJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq -YatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8 -EQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk -8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM -RrKmPK/msHKK/sVHiL+NFqo= ------END PRIVATE KEY----- diff --git a/test/cypress/fixtures/test.example.com.pem b/test/cypress/fixtures/test.example.com.pem deleted file mode 100644 index 16340cdf..00000000 --- a/test/cypress/fixtures/test.example.com.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw -gZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1 -cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD -DDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu -b3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe -bWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93 -QEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu -GBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+ -2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU -QJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB -Ia5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE -OPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G -A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB -/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t -MA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy -l8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s -VXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn -ATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt -IImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u -m+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV -XxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp -1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw -hp8bJUp/QN7pnOVCDbjTQ+HVMXw= ------END CERTIFICATE----- diff --git a/test/cypress/plugins/index.mjs b/test/cypress/plugins/index.mjs index 1058d633..dc09527d 100644 --- a/test/cypress/plugins/index.mjs +++ b/test/cypress/plugins/index.mjs @@ -22,6 +22,11 @@ export default (on, config) => { return null; }, }); + on('task', { + getFixturesFolder() { + return config.fixturesFolder + }, + }); return config; }; diff --git a/test/cypress/support/commands.mjs b/test/cypress/support/commands.mjs index 7d9224d0..0001ded8 100644 --- a/test/cypress/support/commands.mjs +++ b/test/cypress/support/commands.mjs @@ -48,14 +48,16 @@ Cypress.Commands.add("validateSwaggerFile", (url, savePath) => { * @param {*} data The API response data to check against the swagger schema */ Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => { - cy.task('validateSwaggerSchema', { - file: Cypress.env('swaggerBase'), - endpoint: path, - method: method, - statusCode: code, - responseSchema: data, - verbose: true - }).should('equal', null); + cy.env(['swaggerBase']).then(({ swaggerBase }) => { + cy.task('validateSwaggerSchema', { + file: swaggerBase, + endpoint: path, + method: method, + statusCode: code, + responseSchema: data, + verbose: true + }).should('equal', null); + }); }); Cypress.Commands.add('createInitialUser', (defaultUser) => { @@ -151,3 +153,14 @@ Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeo interval: 5000 }); }); + +// Creates CA files for testing, if they already exist they will be deleted +// and recreated with the same content. This is to ensure that the files exist +// for testing and are in a known state. +Cypress.Commands.add('createCustomCerts', () => { + cy.task('getFixturesFolder').then((fixturesFolder) => { + cy.exec('mkcert -install', {failOnNonZeroExit: false}).then(() => { + cy.exec(`mkcert -cert-file=${fixturesFolder}/test.example.com.pem -key-file=${fixturesFolder}/test.example.com-key.pem test.example.com`, {failOnNonZeroExit: false}); + }); + }); +}); diff --git a/test/cypress/support/task.mjs b/test/cypress/support/task.mjs new file mode 100644 index 00000000..edc5015c --- /dev/null +++ b/test/cypress/support/task.mjs @@ -0,0 +1,100 @@ +import fs from "node:fs"; +import FormData from "form-data"; +import Client from "./client.mjs"; +import logger from "./logger.mjs"; + +export default (config) => { + logger("Client Ready using", config.baseUrl); + + return { + /** + * @param {object} options + * @param {string} options.path API path + * @param {string} [options.token] JWT + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiGet: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request("get", options.path, options.returnOnError || false); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.data + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPost: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "post", + options.path, + options.returnOnError || false, + options.data, + ); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.files + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPostFiles: (options) => { + const api = new Client(config); + api.setToken(options.token); + + const form = new FormData(); + for (const [key, value] of Object.entries(options.files)) { + form.append( + key, + fs.createReadStream(`${config.fixturesFolder}/${value}`), + ); + } + return api.postForm(options.path, form, options.returnOnError || false); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.data + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPut: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "put", + options.path, + options.returnOnError || false, + options.data, + ); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiDelete: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "delete", + options.path, + options.returnOnError || false, + ); + }, + }; +};