mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-08-28 04:54:04 +00:00
Merge branch 'NginxProxyManager:develop' into feature/dynamic_upstream_resolve
This commit is contained in:
commit
4e91d890a9
23 changed files with 273 additions and 262 deletions
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
4
docs/src/third-party/index.md
vendored
4
docs/src/third-party/index.md
vendored
|
|
@ -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=)
|
||||
83
docs/src/third-party/npm-auth-gateway.md
vendored
Normal file
83
docs/src/third-party/npm-auth-gateway.md
vendored
Normal file
|
|
@ -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.).
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: <T id="object.delete" tData={{ object: "proxy-host" }} />,
|
||||
onConfirm: () => handleDelete(id),
|
||||
invalidations: [["proxy-hosts"], ["proxy-host", id]],
|
||||
children: <T id="object.delete.content" tData={{ object: "proxy-host" }} />,
|
||||
})
|
||||
}
|
||||
children: (
|
||||
<>
|
||||
<T id="object.delete.content" tData={{ object: "proxy-host" }} />
|
||||
{host?.domainNames?.length ? (
|
||||
<div className="mt-2 fw-bold text-break">{host.domainNames.join(", ")}</div>
|
||||
) : null}
|
||||
{host?.forwardHost ? (
|
||||
<div className="mt-1 text-muted small">
|
||||
({host.forwardScheme}://{host.forwardHost}:{host.forwardPort})
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
onDisableToggle={handleDisableToggle}
|
||||
onNew={() => showProxyHostModal("new")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM cypress/included:15.11.0
|
||||
FROM cypress/included:15.15.0
|
||||
|
||||
# Disable Cypress CLI colors
|
||||
ENV FORCE_COLOR=0
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ describe('Certificates endpoints', () => {
|
|||
let certID;
|
||||
|
||||
before(() => {
|
||||
cy.createCustomCerts();
|
||||
cy.resetUsers();
|
||||
cy.getToken().then((tok) => {
|
||||
token = tok;
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
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');
|
||||
// });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
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();
|
||||
// });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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-----
|
||||
|
|
@ -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-----
|
||||
|
|
@ -22,6 +22,11 @@ export default (on, config) => {
|
|||
return null;
|
||||
},
|
||||
});
|
||||
on('task', {
|
||||
getFixturesFolder() {
|
||||
return config.fixturesFolder
|
||||
},
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
100
test/cypress/support/task.mjs
Normal file
100
test/cypress/support/task.mjs
Normal file
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue