Compare commits

...

12 Commits

Author SHA1 Message Date
Jamie Curnow
8eba31913f Remove pebble certs, they removed the dockerhub image that had armv7 support.
The ghcr image doesn't have it, so it was causing builds to fail.
2025-11-07 11:18:53 +10:00
Jamie Curnow
e4e3415120 Safer handling of backend date formats
and add frontend testing
2025-11-07 11:15:15 +10:00
Jamie Curnow
a03bb7ebce Remove Jenkinsfile, managed in other repo now 2025-11-07 10:54:21 +10:00
Jamie Curnow
51e25d1a40 Attempt to fix race condition with database instantiation 2025-11-07 09:46:00 +10:00
Jamie Curnow
4cb85f6480 Fix #4833 supports the usual proxy env vars for outgoing admin related requests
All checks were successful
Close stale issues and PRs / stale (push) Successful in 20s
2025-11-05 15:16:42 +10:00
jc21
df7dea2d16 Merge branch 'master' into develop 2025-11-05 12:35:06 +10:00
Jamie Curnow
23f4948bde Bump version 2025-11-05 12:33:59 +10:00
Jamie Curnow
0ceb7d0892 Fix #4838 when showing avatars of deleted users 2025-11-05 12:33:13 +10:00
Jamie Curnow
f35671db21 Fix #4837 for those with older config 2025-11-05 10:56:23 +10:00
Jamie Curnow
a3a0614948 Fix #4828 showing incorrect certicificate value 2025-11-05 10:21:55 +10:00
Jamie Curnow
06b67ed4bc Remove user name column from audit log
All checks were successful
Close stale issues and PRs / stale (push) Successful in 20s
2025-11-04 14:57:10 +10:00
Jamie Curnow
4a0e27572e Fix missing translation for renew cert dialog 2025-11-04 14:54:02 +10:00
43 changed files with 364 additions and 358 deletions

View File

@@ -1 +1 @@
2.13.0
2.13.1

285
Jenkinsfile vendored
View File

@@ -1,285 +0,0 @@
import groovy.transform.Field
@Field
def shOutput = ""
def buildxPushTags = ""
pipeline {
agent {
label 'docker-multiarch'
}
options {
buildDiscarder(logRotator(numToKeepStr: '5'))
disableConcurrentBuilds()
ansiColor('xterm')
}
environment {
IMAGE = 'nginx-proxy-manager'
BUILD_VERSION = getVersion()
MAJOR_VERSION = '2'
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}"
BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
COMPOSE_INTERACTIVE_NO_CLI = 1
}
stages {
stage('Environment') {
parallel {
stage('Master') {
when {
branch 'master'
}
steps {
script {
buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest"
}
}
}
stage('Other') {
when {
not {
branch 'master'
}
}
steps {
script {
// Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}"
}
}
}
stage('Versions') {
steps {
sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json'
sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"'
sh 'cat backend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge backend/package.json'
sh 'echo -e "\\E[1;36mBackend Version is:\\E[1;33m $(cat backend/package.json | jq -r .version)\\E[0m"'
sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
}
}
stage('Docker Login') {
steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"'
}
}
}
}
}
stage('Builds') {
parallel {
stage('Project') {
steps {
script {
// Frontend and Backend
def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: '''
set -e
./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1
./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1
''')
shOutput = readFile "${env.WORKSPACE}/tmp-sh-build"
if (shStatusCode != 0) {
error "${shOutput}"
}
}
}
post {
always {
sh 'rm -f ${WORKSPACE}/tmp-sh-build'
}
failure {
npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true)
}
}
}
stage('Docs') {
steps {
dir(path: 'docs') {
sh 'yarn install'
sh 'yarn build'
}
}
}
}
}
stage('Test Sqlite') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.sqlite.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/sqlite'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/sqlite/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/sqlite/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/sqlite/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/sqlite/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/sqlite/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Mysql') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_mysql"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.mysql.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/mysql'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/mysql/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/mysql/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/mysql/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/mysql/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/mysql/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Postgres') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/postgres'
sh 'docker logs $(docker compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1'
sh 'docker logs $(docker compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
sh 'docker logs $(docker compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
sh 'docker logs $(docker compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1'
sh 'docker logs $(docker compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
sh 'docker logs $(docker compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
sh 'docker logs $(docke rcompose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
junit 'test/results/junit/*'
sh 'docker compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('MultiArch Build') {
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh "./scripts/buildx --push ${buildxPushTags}"
}
}
stage('Docs / Comment') {
parallel {
stage('Docs Job') {
when {
allOf {
branch pattern: "^(develop|master)\$", comparator: "REGEXP"
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")]
}
}
stage('PR Comment') {
when {
allOf {
changeRequest()
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
script {
npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev):
```
nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}
```
> [!NOTE]
> Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
> This is a different docker image namespace than the official image.
> [!WARNING]
> Changes and additions to DNS Providers require verification by at least 2 members of the community!
""", true)
}
}
}
}
}
}
post {
always {
sh 'echo Reverting ownership'
sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data'
printResult(true)
}
failure {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
}
unstable {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true)
}
}
}
def getVersion() {
ver = sh(script: 'cat .version', returnStdout: true)
return ver.trim()
}
def getCommit() {
ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true)
return ver.trim()
}

View File

@@ -1,7 +1,7 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.13.0-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.13.1-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>

View File

@@ -1,6 +1,8 @@
import knex from "knex";
import {configGet, configHas} from "./lib/config.js";
let instance = null;
const generateDbConfig = () => {
if (!configHas("database")) {
throw new Error(
@@ -30,4 +32,11 @@ const generateDbConfig = () => {
};
};
export default knex(generateDbConfig());
const getInstance = () => {
if (!instance) {
instance = knex(generateDbConfig());
}
return instance;
}
export default getInstance;

View File

@@ -4,6 +4,7 @@ import path from "path";
import archiver from "archiver";
import _ from "lodash";
import moment from "moment";
import { ProxyAgent } from "proxy-agent";
import tempWrite from "temp-write";
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
import { installPlugin } from "../lib/certbot.js";
@@ -1114,6 +1115,7 @@ const internalCertificate = {
performTestForDomain: async (domain) => {
logger.info(`Testing http challenge for ${domain}`);
const agent = new ProxyAgent();
const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
const options = {
@@ -1123,6 +1125,7 @@ const internalCertificate = {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": Buffer.byteLength(formBody),
},
agent,
};
const result = await new Promise((resolve) => {

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import https from "node:https";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { ProxyAgent } from "proxy-agent";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { ipRanges as logger } from "../logger.js";
@@ -29,10 +30,11 @@ const internalIpRanges = {
},
fetchUrl: (url) => {
const agent = new ProxyAgent();
return new Promise((resolve, reject) => {
logger.info(`Fetching ${url}`);
return https
.get(url, (res) => {
.get(url, { agent }, (res) => {
res.setEncoding("utf8");
let raw_data = "";
res.on("data", (chunk) => {

View File

@@ -25,6 +25,12 @@ const configure = () => {
if (configData?.database) {
logger.info(`Using configuration from file: ${filename}`);
// Migrate those who have "mysql" engine to "mysql2"
if (configData.database.engine === "mysql") {
configData.database.engine = mysqlEngine;
}
instance = configData;
instance.keys = getKeys();
return;
@@ -33,12 +39,12 @@ const configure = () => {
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || '').trim());
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
const envMysqlSSLRejectUnauthorized = process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
const envMysqlSSLRejectUnauthorized = process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined ? true : toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
const envMysqlSSLVerifyIdentity = process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined ? true : toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration");

View File

@@ -2,9 +2,9 @@ import db from "./db.js";
import { migrate as logger } from "./logger.js";
const migrateUp = async () => {
const version = await db.migrate.currentVersion();
const version = await db().migrate.currentVersion();
logger.info("Current database version:", version);
return await db.migrate.latest({
return await db().migrate.latest({
tableName: "migrations",
directory: "migrations",
});

View File

@@ -10,7 +10,7 @@ import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];

View File

@@ -6,7 +6,7 @@ import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class AccessListAuth extends Model {
$beforeInsert() {

View File

@@ -6,7 +6,7 @@ import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class AccessListClient extends Model {
$beforeInsert() {

View File

@@ -6,7 +6,7 @@ import db from "../db.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
class AuditLog extends Model {
$beforeInsert() {

View File

@@ -8,7 +8,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted"];

View File

@@ -11,7 +11,7 @@ import redirectionHostModel from "./redirection_host.js";
import streamModel from "./stream.js";
import userModel from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted"];

View File

@@ -8,7 +8,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];

View File

@@ -2,7 +2,7 @@ import { Model } from "objection";
import db from "../db.js";
import { isSqlite } from "../lib/config.js";
Model.knex(db);
Model.knex(db());
export default () => {
if (isSqlite()) {

View File

@@ -9,7 +9,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = [
"is_deleted",

View File

@@ -8,7 +8,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = [
"is_deleted",

View File

@@ -4,7 +4,7 @@
import { Model } from "objection";
import db from "../db.js";
Model.knex(db);
Model.knex(db());
class Setting extends Model {
$beforeInsert () {

View File

@@ -5,7 +5,7 @@ import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];

View File

@@ -7,7 +7,7 @@ import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.j
import now from "./now_helper.js";
import UserPermission from "./user_permission.js";
Model.knex(db);
Model.knex(db());
const boolFields = ["is_deleted", "is_disabled"];

View File

@@ -5,7 +5,7 @@ import { Model } from "objection";
import db from "../db.js";
import now from "./now_helper.js";
Model.knex(db);
Model.knex(db());
class UserPermission extends Model {
$beforeInsert () {

View File

@@ -32,6 +32,7 @@
"objection": "3.0.1",
"path": "^0.12.7",
"pg": "^8.16.3",
"proxy-agent": "^6.5.0",
"signale": "1.4.0",
"sqlite3": "^5.1.7",
"temp-write": "^4.0.0"

View File

@@ -143,6 +143,11 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
"@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@@ -168,6 +173,11 @@ agent-base@6, agent-base@^6.0.2:
dependencies:
debug "4"
agent-base@^7.1.0, agent-base@^7.1.2:
version "7.1.4"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
agentkeepalive@^4.1.3:
version "4.6.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a"
@@ -308,6 +318,13 @@ asn1@^0.2.4:
dependencies:
safer-buffer "~2.1.0"
ast-types@^0.13.4:
version "0.13.4"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782"
integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==
dependencies:
tslib "^2.0.1"
async@^3.2.4:
version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
@@ -328,6 +345,11 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
basic-ftp@^5.0.2:
version "5.0.5"
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
batchflow@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/batchflow/-/batchflow-0.4.0.tgz#7d419df79b6b7587b06f9ea34f96ccef6f74e5b5"
@@ -667,6 +689,11 @@ crc32-stream@^4.0.2:
crc-32 "^1.2.0"
readable-stream "^3.4.0"
data-uri-to-buffer@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b"
integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==
db-errors@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/db-errors/-/db-errors-0.2.3.tgz#a6a38952e00b20e790f2695a6446b3c65497ffa2"
@@ -700,6 +727,13 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@^4.3.4:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
dependencies:
ms "^2.1.3"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -717,6 +751,15 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
degenerator@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5"
integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==
dependencies:
ast-types "^0.13.4"
escodegen "^2.1.0"
esprima "^4.0.1"
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -846,11 +889,37 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
escodegen@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
dependencies:
esprima "^4.0.1"
estraverse "^5.2.0"
esutils "^2.0.2"
optionalDependencies:
source-map "~0.6.1"
esm@^3.2.25:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
@@ -1069,6 +1138,15 @@ get-proto@^1.0.1:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-uri@^6.0.1:
version "6.0.5"
resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16"
integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==
dependencies:
basic-ftp "^5.0.2"
data-uri-to-buffer "^6.0.2"
debug "^4.3.4"
getopts@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4"
@@ -1170,6 +1248,14 @@ http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1:
version "7.0.2"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
dependencies:
agent-base "^7.1.0"
debug "^4.3.4"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@@ -1178,6 +1264,14 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
https-proxy-agent@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
dependencies:
agent-base "^7.1.2"
debug "4"
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@@ -1747,6 +1841,11 @@ negotiator@^0.6.2, negotiator@~0.6.4:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
netmask@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
node-abi@^3.3.0:
version "3.78.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba"
@@ -1924,6 +2023,28 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pac-proxy-agent@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df"
integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
dependencies:
"@tootallnate/quickjs-emscripten" "^0.23.0"
agent-base "^7.1.2"
debug "^4.3.4"
get-uri "^6.0.1"
http-proxy-agent "^7.0.0"
https-proxy-agent "^7.0.6"
pac-resolver "^7.0.1"
socks-proxy-agent "^8.0.5"
pac-resolver@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6"
integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==
dependencies:
degenerator "^5.0.0"
netmask "^2.0.2"
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -2120,6 +2241,25 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"
proxy-agent@^6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d"
integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==
dependencies:
agent-base "^7.1.2"
debug "^4.3.4"
http-proxy-agent "^7.0.1"
https-proxy-agent "^7.0.6"
lru-cache "^7.14.1"
pac-proxy-agent "^7.1.0"
proxy-from-env "^1.1.0"
socks-proxy-agent "^8.0.5"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
pstree.remy@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
@@ -2422,7 +2562,16 @@ socks-proxy-agent@^6.0.0:
debug "^4.3.3"
socks "^2.6.2"
socks@^2.6.2:
socks-proxy-agent@^8.0.5:
version "8.0.5"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee"
integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==
dependencies:
agent-base "^7.1.2"
debug "^4.3.4"
socks "^2.8.3"
socks@^2.6.2, socks@^2.8.3:
version "2.8.7"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea"
integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==
@@ -2430,6 +2579,11 @@ socks@^2.6.2:
ip-address "^10.0.1"
smart-buffer "^4.2.0"
source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
split2@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
@@ -2609,6 +2763,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tslib@^2.0.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"

View File

@@ -4,7 +4,6 @@
# This file assumes that the frontend has been built using ./scripts/frontend-build
FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node
ARG TARGETPLATFORM
@@ -46,7 +45,6 @@ RUN yarn install \
# add late to limit cache-busting by modifications
COPY docker/rootfs /
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
# Remove frontend service not required for prod, dev nginx config as well

View File

@@ -1,5 +1,4 @@
FROM nginxproxymanager/testca AS testca
FROM letsencrypt/pebble AS pebbleca
FROM nginxproxymanager/nginx-full:certbot-node
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
@@ -33,7 +32,6 @@ RUN rm -f /etc/nginx/conf.d/production.conf \
&& chmod 644 -R /root/.cache
# Certs for testing purposes
COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
EXPOSE 80 81 443

View File

@@ -1,12 +0,0 @@
{
"pebble": {
"listenAddress": "0.0.0.0:443",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "test/certs/localhost/cert.pem",
"privateKey": "test/certs/localhost/key.pem",
"httpPort": 80,
"tlsPort": 443,
"ocspResponderURL": "",
"externalAccountBindingRequired": false
}
}

View File

@@ -5,5 +5,14 @@ interface Props {
certificate?: Certificate;
}
export function CertificateFormatter({ certificate }: Props) {
return <T id={certificate ? "lets-encrypt" : "http-only"} />;
let translation = "http-only";
if (certificate) {
translation = certificate.provider;
if (translation === "letsencrypt") {
translation = "lets-encrypt";
} else if (translation === "other") {
translation = "certificates.custom";
}
}
return <T id={translation} />;
}

View File

@@ -0,0 +1,77 @@
import { DateTimeFormat } from "src/locale";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
describe("DateFormatter", () => {
// Keep a reference to the real Intl to restore later
const RealIntl = global.Intl;
const desiredTimeZone = "Europe/London";
const desiredLocale = "en-GB";
beforeAll(() => {
// Ensure Node-based libs using TZ behave deterministically
try {
process.env.TZ = desiredTimeZone;
} catch {
// ignore if not available
}
// Mock Intl.DateTimeFormat so formatting is stable regardless of host
const MockedDateTimeFormat = class extends RealIntl.DateTimeFormat {
constructor(
_locales?: string | string[],
options?: Intl.DateTimeFormatOptions,
) {
super(desiredLocale, {
...options,
timeZone: desiredTimeZone,
});
}
} as unknown as typeof Intl.DateTimeFormat;
global.Intl = {
...RealIntl,
DateTimeFormat: MockedDateTimeFormat,
};
});
afterAll(() => {
// Restore original Intl after tests
global.Intl = RealIntl;
});
it("format date from iso date", () => {
const value = "2024-01-01T00:00:00.000Z";
const text = DateTimeFormat(value);
expect(text).toBe("Monday, 01/01/2024, 12:00:00 am");
});
it("format date from unix timestamp number", () => {
const value = 1762476112;
const text = DateTimeFormat(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
});
it("format date from unix timestamp string", () => {
const value = "1762476112";
const text = DateTimeFormat(value);
expect(text).toBe("Friday, 07/11/2025, 12:41:52 am");
});
it("catch bad format from string", () => {
const value = "this is not a good date";
const text = DateTimeFormat(value);
expect(text).toBe("this is not a good date");
});
it("catch bad format from number", () => {
const value = -100;
const text = DateTimeFormat(value);
expect(text).toBe("-100");
});
it("catch bad format from number as string", () => {
const value = "-100";
const text = DateTimeFormat(value);
expect(text).toBe("-100");
});
});

View File

@@ -1,5 +1,7 @@
const defaultImg = "/images/default-avatar.jpg";
interface Props {
url: string;
url?: string;
name?: string;
}
export function GravatarFormatter({ url, name }: Props) {
@@ -9,7 +11,7 @@ export function GravatarFormatter({ url, name }: Props) {
title={name}
className="avatar avatar-2 me-2"
style={{
backgroundImage: `url(${url})`,
backgroundImage: `url(${url || defaultImg})`,
}}
/>
</div>

View File

@@ -1,15 +1,36 @@
import { intlFormat, parseISO } from "date-fns";
import { fromUnixTime, intlFormat, parseISO } from "date-fns";
const DateTimeFormat = (isoDate: string) =>
intlFormat(parseISO(isoDate), {
weekday: "long",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
});
const isUnixTimestamp = (value: unknown): boolean => {
if (typeof value !== "number" && typeof value !== "string") return false;
const num = Number(value);
if (!Number.isFinite(num)) return false;
// Check plausible Unix timestamp range: from 1970 to ~year 3000
// Support both seconds and milliseconds
if (num > 0 && num < 10000000000) return true; // seconds (<= 10 digits)
if (num >= 10000000000 && num < 32503680000000) return true; // milliseconds (<= 13 digits)
return false;
};
const DateTimeFormat = (value: string | number): string => {
if (typeof value !== "number" && typeof value !== "string") return `${value}`;
try {
const d = isUnixTimestamp(value)
? fromUnixTime(+value)
: parseISO(`${value}`);
return intlFormat(d, {
weekday: "long",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
});
} catch {
return `${value}`;
}
};
export { DateTimeFormat };

View File

@@ -30,13 +30,20 @@ const getLocale = (short = false) => {
if (short) {
return loc.slice(0, 2);
}
// finally, fallback
if (!loc) {
loc = "en";
}
return loc;
};
const cache = createIntlCache();
const initialMessages = loadMessages(getLocale());
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
let intl = createIntl(
{ locale: getLocale(), messages: initialMessages },
cache,
);
const changeLocale = (locale: string): void => {
const messages = loadMessages(locale);
@@ -76,4 +83,12 @@ const T = ({
);
};
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
export {
localeOptions,
getFlagCodeForLocale,
getLocale,
createIntl,
changeLocale,
intl,
T,
};

View File

@@ -32,6 +32,7 @@
"certificate.none.subtitle.for-http": "This host will not use HTTPS",
"certificate.none.title": "None",
"certificate.not-in-use": "Not Used",
"certificate.renew": "Renew Certificate",
"certificates": "Certificates",
"certificates.custom": "Custom Certificate",
"certificates.custom.warning": "Key files protected with a passphrase are not supported.",

View File

@@ -98,6 +98,9 @@
"certificate.not-in-use": {
"defaultMessage": "Not Used"
},
"certificate.renew": {
"defaultMessage": "Renew Certificate"
},
"certificates": {
"defaultMessage": "Certificates"
},

View File

@@ -47,7 +47,7 @@ const RenewCertificateModal = EasyModal.create(({ id, visible, remove }: Props)
<Modal show={visible} onHide={isSubmitting ? undefined : remove}>
<Modal.Header closeButton={!isSubmitting}>
<Modal.Title>
<T id="renew-certificate" />
<T id="certificate.renew" />
</Modal.Title>
</Modal.Header>
<Modal.Body>

View File

@@ -21,7 +21,10 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
() => [
columnHelper.accessor((row: any) => row.owner, {
id: "owner",
cell: (info: any) => <GravatarFormatter url={info.getValue().avatar} name={info.getValue().name} />,
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",
},

View File

@@ -18,16 +18,12 @@ export default function Table({ data, isFetching, onSelectItem }: Props) {
id: "user.avatar",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value.avatar} name={value.name} />;
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",
},
}),
columnHelper.accessor((row: AuditLog) => row.user?.name, {
id: "user.name",
header: intl.formatMessage({ id: "column.name" }),
}),
columnHelper.accessor((row: AuditLog) => row, {
id: "objectType",
header: intl.formatMessage({ id: "column.event" }),

View File

@@ -31,7 +31,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value.avatar} name={value.name} />;
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",

View File

@@ -31,7 +31,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value.avatar} name={value.name} />;
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",

View File

@@ -32,7 +32,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value.avatar} name={value.name} />;
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",

View File

@@ -31,7 +31,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value.avatar} name={value.name} />;
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",

View File

@@ -31,7 +31,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
id: "owner",
cell: (info: any) => {
const value = info.getValue();
return <GravatarFormatter url={value.avatar} name={value.name} />;
return <GravatarFormatter url={value ? value.avatar : ""} name={value ? value.name : ""} />;
},
meta: {
className: "w-1",

View File

@@ -16,7 +16,7 @@ if hash docker 2>/dev/null; then
-e NODE_OPTIONS=--openssl-legacy-provider \
-v "$(pwd)/frontend:/app/frontend" \
-w /app/frontend "${DOCKER_IMAGE}" \
sh -c "yarn install && yarn lint && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
sh -c "yarn install && yarn lint && yarn vitest run && yarn build && chown -R $(id -u):$(id -g) /app/frontend"
echo -e "${BLUE} ${GREEN}Building Frontend Complete${RESET}"
else