404 hosts polish

This commit is contained in:
Jamie Curnow
2025-09-24 19:45:00 +10:00
parent 18537b9288
commit da68fe29ac
10 changed files with 95 additions and 25 deletions

View File

@@ -88,14 +88,6 @@ Sometimes this can take a little bit because of the entropy of keys.
[http://127.0.0.1:81](http://127.0.0.1:81) [http://127.0.0.1:81](http://127.0.0.1:81)
Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributing ## Contributing

View File

@@ -63,7 +63,7 @@ const internalDeadHost = {
action: "created", action: "created",
object_type: "dead-host", object_type: "dead-host",
object_id: row.id, object_id: row.id,
meta: _.assign({}, data.meta || {}, row.meta), meta: thisData,
}); });
if (createCertificate) { if (createCertificate) {
@@ -240,6 +240,7 @@ const internalDeadHost = {
// Delete Nginx Config // Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row); await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload(); await internalNginx.reload();
// Add to audit log // Add to audit log
await internalAuditLog.add(access, { await internalAuditLog.add(access, {
action: "deleted", action: "deleted",
@@ -247,6 +248,7 @@ const internalDeadHost = {
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions()),
}); });
return true;
}, },
/** /**

View File

@@ -301,8 +301,11 @@ const internalNginx = {
* @param {String} filename * @param {String} filename
*/ */
deleteFile: (filename) => { deleteFile: (filename) => {
logger.debug(`Deleting file: ${filename}`); if (!fs.existsSync(filename)) {
return;
}
try { try {
logger.debug(`Deleting file: ${filename}`);
fs.unlinkSync(filename); fs.unlinkSync(filename);
} catch (err) { } catch (err) {
logger.debug("Could not delete file:", JSON.stringify(err, null, 2)); logger.debug("Could not delete file:", JSON.stringify(err, null, 2));

View File

@@ -121,7 +121,7 @@ router
/** /**
* PUT /api/nginx/dead-hosts/123 * PUT /api/nginx/dead-hosts/123
* *
* Update and existing dead-host * Update an existing dead-host
*/ */
.put(async (req, res, next) => { .put(async (req, res, next) => {
try { try {
@@ -138,7 +138,7 @@ router
/** /**
* DELETE /api/nginx/dead-hosts/123 * DELETE /api/nginx/dead-hosts/123
* *
* Update and existing dead-host * Delete a dead-host
*/ */
.delete(async (req, res, next) => { .delete(async (req, res, next) => {
try { try {

View File

@@ -4,14 +4,32 @@ interface Props {
domains: string[]; domains: string[];
createdOn?: string; createdOn?: string;
} }
const DomainLink = ({ domain }: { domain: string }) => {
// when domain contains a wildcard, make the link go nowhere.
let onClick: ((e: React.MouseEvent) => void) | undefined;
if (domain.includes("*")) {
onClick = (e: React.MouseEvent) => e.preventDefault();
}
return (
<a
key={domain}
href={`http://${domain}`}
target="_blank"
onClick={onClick}
className="badge bg-yellow-lt domain-name me-2"
>
{domain}
</a>
);
};
export function DomainsFormatter({ domains, createdOn }: Props) { export function DomainsFormatter({ domains, createdOn }: Props) {
return ( return (
<div className="flex-fill"> <div className="flex-fill">
<div className="font-weight-medium"> <div className="font-weight-medium">
{domains.map((domain: string) => ( {domains.map((domain: string) => (
<a key={domain} href={`http://${domain}`} className="badge bg-yellow-lt domain-name"> <DomainLink key={domain} domain={domain} />
{domain}
</a>
))} ))}
</div> </div>
{createdOn ? ( {createdOn ? (

View File

@@ -10,6 +10,8 @@ const getEventValue = (event: AuditLog) => {
switch (event.objectType) { switch (event.objectType) {
case "user": case "user":
return event.meta?.name; return event.meta?.name;
case "dead-host":
return event.meta?.domainNames?.join(", ") || "N/A";
default: default:
return `UNKNOWN EVENT TYPE: ${event.objectType}`; return `UNKNOWN EVENT TYPE: ${event.objectType}`;
} }

View File

@@ -41,6 +41,8 @@
"column.status": "Status", "column.status": "Status",
"created-on": "Created: {date}", "created-on": "Created: {date}",
"dashboard.title": "Dashboard", "dashboard.title": "Dashboard",
"dead-host.delete.content": "Are you sure you want to delete this 404 host?",
"dead-host.delete.title": "Delete 404 Host",
"dead-host.edit": "Edit 404 Host", "dead-host.edit": "Edit 404 Host",
"dead-host.new": "New 404 Host", "dead-host.new": "New 404 Host",
"dead-hosts.actions-title": "404 Host #{id}", "dead-hosts.actions-title": "404 Host #{id}",
@@ -67,8 +69,11 @@
"error.max-domains": "Too many domains, max is {max}", "error.max-domains": "Too many domains, max is {max}",
"error.passwords-must-match": "Passwords must match", "error.passwords-must-match": "Passwords must match",
"error.required": "This is required", "error.required": "This is required",
"event.created-dead-host": "Created 404 Host",
"event.created-user": "Created User", "event.created-user": "Created User",
"event.deleted-user": "Deleted User", "event.deleted-user": "Deleted User",
"event.disabled-dead-host": "Disabled 404 Host",
"event.enabled-dead-host": "Enabled 404 Host",
"event.updated-user": "Updated User", "event.updated-user": "Updated User",
"footer.github-fork": "Fork me on Github", "footer.github-fork": "Fork me on Github",
"hosts.title": "Hosts", "hosts.title": "Hosts",
@@ -84,6 +89,9 @@
"notfound.title": "Oops… You just found an error page", "notfound.title": "Oops… You just found an error page",
"notification.dead-host-saved": "404 Host has been saved", "notification.dead-host-saved": "404 Host has been saved",
"notification.error": "Error", "notification.error": "Error",
"notification.host-deleted": "Host has been deleted",
"notification.host-disabled": "Host has been disabled",
"notification.host-enabled": "Host has been enabled",
"notification.success": "Success", "notification.success": "Success",
"notification.user-deleted": "User has been deleted", "notification.user-deleted": "User has been deleted",
"notification.user-saved": "User has been saved", "notification.user-saved": "User has been saved",

View File

@@ -134,12 +134,18 @@
"dead-hosts.count": { "dead-hosts.count": {
"defaultMessage": "{count} 404 Hosts" "defaultMessage": "{count} 404 Hosts"
}, },
"dead-host.edit": {
"defaultMessage": "Edit 404 Host"
},
"dead-hosts.empty": { "dead-hosts.empty": {
"defaultMessage": "There are no 404 Hosts" "defaultMessage": "There are no 404 Hosts"
}, },
"dead-host.delete.content": {
"defaultMessage": "Are you sure you want to delete this 404 host?"
},
"dead-host.delete.title": {
"defaultMessage": "Delete 404 Host"
},
"dead-host.edit": {
"defaultMessage": "Edit 404 Host"
},
"dead-host.new": { "dead-host.new": {
"defaultMessage": "New 404 Host" "defaultMessage": "New 404 Host"
}, },
@@ -200,12 +206,21 @@
"error.required": { "error.required": {
"defaultMessage": "This is required" "defaultMessage": "This is required"
}, },
"event.created-dead-host": {
"defaultMessage": "Created 404 Host"
},
"event.created-user": { "event.created-user": {
"defaultMessage": "Created User" "defaultMessage": "Created User"
}, },
"event.deleted-user": { "event.deleted-user": {
"defaultMessage": "Deleted User" "defaultMessage": "Deleted User"
}, },
"event.disabled-dead-host": {
"defaultMessage": "Disabled 404 Host"
},
"event.enabled-dead-host": {
"defaultMessage": "Enabled 404 Host"
},
"event.updated-user": { "event.updated-user": {
"defaultMessage": "Updated User" "defaultMessage": "Updated User"
}, },
@@ -254,9 +269,18 @@
"notification.error": { "notification.error": {
"defaultMessage": "Error" "defaultMessage": "Error"
}, },
"notification.host-deleted": {
"defaultMessage": "Host has been deleted"
},
"notification.user-deleted": { "notification.user-deleted": {
"defaultMessage": "User has been deleted" "defaultMessage": "User has been deleted"
}, },
"notification.host-disabled": {
"defaultMessage": "Host has been disabled"
},
"notification.host-enabled": {
"defaultMessage": "Host has been enabled"
},
"notification.user-saved": { "notification.user-saved": {
"defaultMessage": "User has been saved" "defaultMessage": "User has been saved"
}, },

View File

@@ -12,9 +12,10 @@ interface Props {
isFetching?: boolean; isFetching?: boolean;
onEdit?: (id: number) => void; onEdit?: (id: number) => void;
onDelete?: (id: number) => void; onDelete?: (id: number) => void;
onDisableToggle?: (id: number, enabled: boolean) => void;
onNew?: () => void; onNew?: () => void;
} }
export default function Table({ data, isFetching, onEdit, onDelete, onNew }: Props) { export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew }: Props) {
const columnHelper = createColumnHelper<DeadHost>(); const columnHelper = createColumnHelper<DeadHost>();
const columns = useMemo( const columns = useMemo(
() => [ () => [
@@ -83,9 +84,18 @@ export default function Table({ data, isFetching, onEdit, onDelete, onNew }: Pro
<IconEdit size={16} /> <IconEdit size={16} />
{intl.formatMessage({ id: "action.edit" })} {intl.formatMessage({ id: "action.edit" })}
</a> </a>
<a className="dropdown-item" href="#"> <a
className="dropdown-item"
href="#"
onClick={(e) => {
e.preventDefault();
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
}}
>
<IconPower size={16} /> <IconPower size={16} />
{intl.formatMessage({ id: "action.disable" })} {intl.formatMessage({
id: info.row.original.enabled ? "action.disable" : "action.enable",
})}
</a> </a>
<div className="dropdown-divider" /> <div className="dropdown-divider" />
<a <a
@@ -108,7 +118,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onNew }: Pro
}, },
}), }),
], ],
[columnHelper, onDelete, onEdit], [columnHelper, onDelete, onEdit, onDisableToggle],
); );
const tableInstance = useReactTable<DeadHost>({ const tableInstance = useReactTable<DeadHost>({

View File

@@ -1,6 +1,8 @@
import { IconSearch } from "@tabler/icons-react"; import { IconSearch } from "@tabler/icons-react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import Alert from "react-bootstrap/Alert"; import Alert from "react-bootstrap/Alert";
import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
import { Button, LoadingPage } from "src/components"; import { Button, LoadingPage } from "src/components";
import { useDeadHosts } from "src/hooks"; import { useDeadHosts } from "src/hooks";
import { intl } from "src/locale"; import { intl } from "src/locale";
@@ -9,6 +11,7 @@ import { showSuccess } from "src/notifications";
import Table from "./Table"; import Table from "./Table";
export default function TableWrapper() { export default function TableWrapper() {
const queryClient = useQueryClient();
const [deleteId, setDeleteId] = useState(0); const [deleteId, setDeleteId] = useState(0);
const [editId, setEditId] = useState(0 as number | "new"); const [editId, setEditId] = useState(0 as number | "new");
const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]);
@@ -22,10 +25,17 @@ export default function TableWrapper() {
} }
const handleDelete = async () => { const handleDelete = async () => {
// await deleteUser(deleteId); await deleteDeadHost(deleteId);
showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
}; };
const handleDisableToggle = async (id: number, enabled: boolean) => {
await toggleDeadHost(id, enabled);
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
showSuccess(intl.formatMessage({ id: enabled ? "notification.host-enabled" : "notification.host-disabled" }));
};
return ( return (
<div className="card mt-4"> <div className="card mt-4">
<div className="card-status-top bg-red" /> <div className="card-status-top bg-red" />
@@ -60,17 +70,18 @@ export default function TableWrapper() {
isFetching={isFetching} isFetching={isFetching}
onEdit={(id: number) => setEditId(id)} onEdit={(id: number) => setEditId(id)}
onDelete={(id: number) => setDeleteId(id)} onDelete={(id: number) => setDeleteId(id)}
onDisableToggle={handleDisableToggle}
onNew={() => setEditId("new")} onNew={() => setEditId("new")}
/> />
{editId ? <DeadHostModal id={editId} onClose={() => setEditId(0)} /> : null} {editId ? <DeadHostModal id={editId} onClose={() => setEditId(0)} /> : null}
{deleteId ? ( {deleteId ? (
<DeleteConfirmModal <DeleteConfirmModal
title={intl.formatMessage({ id: "user.delete.title" })} title={intl.formatMessage({ id: "dead-host.delete.title" })}
onConfirm={handleDelete} onConfirm={handleDelete}
onClose={() => setDeleteId(0)} onClose={() => setDeleteId(0)}
invalidations={[["dead-hosts"], ["dead-host", deleteId]]} invalidations={[["dead-hosts"], ["dead-host", deleteId]]}
> >
{intl.formatMessage({ id: "user.delete.content" })} {intl.formatMessage({ id: "dead-host.delete.content" })}
</DeleteConfirmModal> </DeleteConfirmModal>
) : null} ) : null}
</div> </div>