mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-23 03:43:33 +00:00
Access list modal polish
This commit is contained in:
@@ -74,3 +74,24 @@
|
|||||||
label.row {
|
label.row {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: var(--tblr-gray-500);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: var(--tblr-bg-surface-secondary);
|
||||||
|
border: var(--tblr-border-width) solid var(--tblr-border-color);
|
||||||
|
border-radius: var(--tblr-border-radius);
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--tblr-bg-surface-secondary);
|
||||||
|
border-radius: var(--tblr-border-radius) 0 0 var(--tblr-border-radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -67,8 +67,8 @@ export interface AccessListItem {
|
|||||||
accessListId?: number;
|
accessListId?: number;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
meta: Record<string, any>;
|
meta?: Record<string, any>;
|
||||||
hint: string;
|
hint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccessListClient = {
|
export type AccessListClient = {
|
||||||
@@ -78,7 +78,7 @@ export type AccessListClient = {
|
|||||||
accessListId?: number;
|
accessListId?: number;
|
||||||
address: string;
|
address: string;
|
||||||
directive: "allow" | "deny";
|
directive: "allow" | "deny";
|
||||||
meta: Record<string, any>;
|
meta?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Certificate {
|
export interface Certificate {
|
||||||
|
131
frontend/src/components/Form/AccessClientFields.tsx
Normal file
131
frontend/src/components/Form/AccessClientFields.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { IconX } from "@tabler/icons-react";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { useFormikContext } from "formik";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { AccessListClient } from "src/api/backend";
|
||||||
|
import { T } from "src/locale";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialValues: AccessListClient[];
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
export function AccessClientFields({ initialValues, name = "clients" }: Props) {
|
||||||
|
const [values, setValues] = useState<AccessListClient[]>(initialValues || []);
|
||||||
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
|
const blankClient: AccessListClient = { directive: "allow", address: "" };
|
||||||
|
|
||||||
|
if (values?.length === 0) {
|
||||||
|
setValues([blankClient]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setValues([...values, blankClient]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (idx: number) => {
|
||||||
|
const newValues = values.filter((_: AccessListClient, i: number) => i !== idx);
|
||||||
|
if (newValues.length === 0) {
|
||||||
|
newValues.push(blankClient);
|
||||||
|
}
|
||||||
|
setValues(newValues);
|
||||||
|
setFormField(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (idx: number, field: string, fieldValue: string) => {
|
||||||
|
const newValues = values.map((v: AccessListClient, i: number) =>
|
||||||
|
i === idx ? { ...v, [field]: fieldValue } : v,
|
||||||
|
);
|
||||||
|
setValues(newValues);
|
||||||
|
setFormField(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFormField = (newValues: AccessListClient[]) => {
|
||||||
|
const filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== "");
|
||||||
|
setFieldValue(name, filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="text-muted">
|
||||||
|
<T id="access.help.rules-order" />
|
||||||
|
</p>
|
||||||
|
{values.map((client: AccessListClient, idx: number) => (
|
||||||
|
<div className="row mb-1" key={idx}>
|
||||||
|
<div className="col-11">
|
||||||
|
<div className="input-group mb-2">
|
||||||
|
<span className="input-group-select">
|
||||||
|
<select
|
||||||
|
className={cn(
|
||||||
|
"form-select",
|
||||||
|
"m-0",
|
||||||
|
client.directive === "allow" ? "bg-lime-lt" : "bg-orange-lt",
|
||||||
|
)}
|
||||||
|
name={`clients[${idx}].directive`}
|
||||||
|
value={client.directive}
|
||||||
|
onChange={(e) => handleChange(idx, "directive", e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="allow">Allow</option>
|
||||||
|
<option value="deny">Deny</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
name={`clients[${idx}].address`}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
autoComplete="off"
|
||||||
|
value={client.address}
|
||||||
|
onChange={(e) => handleChange(idx, "address", e.target.value)}
|
||||||
|
placeholder="192.168.1.100 or 192.168.1.0/24 or 2001:0db8::/32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-1">
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
className="btn btn-ghost btn-danger p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRemove(idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="mb-3">
|
||||||
|
<button type="button" className="btn btn-sm" onClick={handleAdd}>
|
||||||
|
<T id="action.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="row mb-3">
|
||||||
|
<p className="text-muted">
|
||||||
|
<T id="access.help-rules-last" />
|
||||||
|
</p>
|
||||||
|
<div className="col-11">
|
||||||
|
<div className="input-group mb-2">
|
||||||
|
<span className="input-group-select">
|
||||||
|
<select
|
||||||
|
className="form-select m-0 bg-orange-lt"
|
||||||
|
name="clients[last].directive"
|
||||||
|
value="deny"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<option value="deny">Deny</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
name="clients[last].address"
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
autoComplete="off"
|
||||||
|
value="all"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -32,7 +32,7 @@ interface Props {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
|
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
|
||||||
const { isLoading, isError, error, data } = useAccessLists();
|
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
|
||||||
const { setFieldValue } = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
const handleChange = (newValue: any, _actionMeta: ActionMeta<AccessOption>) => {
|
const handleChange = (newValue: any, _actionMeta: ActionMeta<AccessOption>) => {
|
||||||
|
@@ -1,36 +0,0 @@
|
|||||||
import { useFormikContext } from "formik";
|
|
||||||
import { T } from "src/locale";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
export function BasicAuthField({ name = "items", id = "items" }: Props) {
|
|
||||||
const { setFieldValue } = useFormikContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-6">
|
|
||||||
<label className="form-label" htmlFor="...">
|
|
||||||
<T id="username" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-6">
|
|
||||||
<label className="form-label" htmlFor="...">
|
|
||||||
<T id="password" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row mb-3">
|
|
||||||
<div className="col-6">
|
|
||||||
<input id="name" type="text" required autoComplete="off" className="form-control" />
|
|
||||||
</div>
|
|
||||||
<div className="col-6">
|
|
||||||
<input id="pw" type="password" required autoComplete="off" className="form-control" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button className="btn">+</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
105
frontend/src/components/Form/BasicAuthFields.tsx
Normal file
105
frontend/src/components/Form/BasicAuthFields.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { IconX } from "@tabler/icons-react";
|
||||||
|
import { useFormikContext } from "formik";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { AccessListItem } from "src/api/backend";
|
||||||
|
import { T } from "src/locale";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialValues: AccessListItem[];
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
export function BasicAuthFields({ initialValues, name = "items" }: Props) {
|
||||||
|
const [values, setValues] = useState<AccessListItem[]>(initialValues || []);
|
||||||
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
|
const blankItem: AccessListItem = { username: "", password: "" };
|
||||||
|
|
||||||
|
if (values?.length === 0) {
|
||||||
|
setValues([blankItem]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setValues([...values, blankItem]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (idx: number) => {
|
||||||
|
const newValues = values.filter((_: AccessListItem, i: number) => i !== idx);
|
||||||
|
if (newValues.length === 0) {
|
||||||
|
newValues.push(blankItem);
|
||||||
|
}
|
||||||
|
setValues(newValues);
|
||||||
|
setFormField(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (idx: number, field: string, fieldValue: string) => {
|
||||||
|
const newValues = values.map((v: AccessListItem, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
|
||||||
|
setValues(newValues);
|
||||||
|
setFormField(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFormField = (newValues: AccessListItem[]) => {
|
||||||
|
const filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== "");
|
||||||
|
setFieldValue(name, filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-6">
|
||||||
|
<label className="form-label" htmlFor="...">
|
||||||
|
<T id="username" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="col-6">
|
||||||
|
<label className="form-label" htmlFor="...">
|
||||||
|
<T id="password" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{values.map((item: AccessListItem, idx: number) => (
|
||||||
|
<div className="row mb-3" key={idx}>
|
||||||
|
<div className="col-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
className="form-control input-sm"
|
||||||
|
value={item.username}
|
||||||
|
onChange={(e) => handleChange(idx, "username", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-5">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
className="form-control"
|
||||||
|
value={item.password}
|
||||||
|
placeholder={
|
||||||
|
initialValues.filter((iv: AccessListItem) => iv.username === item.username).length > 0
|
||||||
|
? "••••••••"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onChange={(e) => handleChange(idx, "password", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-1">
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
className="btn btn-ghost btn-danger p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRemove(idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<button type="button" className="btn btn-sm" onClick={handleAdd}>
|
||||||
|
<T id="action.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
|
export * from "./AccessClientFields";
|
||||||
export * from "./AccessField";
|
export * from "./AccessField";
|
||||||
export * from "./BasicAuthField";
|
export * from "./BasicAuthFields";
|
||||||
export * from "./DNSProviderFields";
|
export * from "./DNSProviderFields";
|
||||||
export * from "./DomainNamesField";
|
export * from "./DomainNamesField";
|
||||||
export * from "./NginxConfigField";
|
export * from "./NginxConfigField";
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { type AccessList, createAccessList, getAccessList, updateAccessList } from "src/api/backend";
|
import {
|
||||||
|
type AccessList,
|
||||||
|
type AccessListExpansion,
|
||||||
|
createAccessList,
|
||||||
|
getAccessList,
|
||||||
|
updateAccessList,
|
||||||
|
} from "src/api/backend";
|
||||||
|
|
||||||
const fetchAccessList = (id: number | "new") => {
|
const fetchAccessList = (id: number | "new", expand: AccessListExpansion[] = ["owner"]) => {
|
||||||
if (id === "new") {
|
if (id === "new") {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -14,13 +20,13 @@ const fetchAccessList = (id: number | "new") => {
|
|||||||
meta: {},
|
meta: {},
|
||||||
} as AccessList);
|
} as AccessList);
|
||||||
}
|
}
|
||||||
return getAccessList(id, ["owner"]);
|
return getAccessList(id, expand);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useAccessList = (id: number | "new", options = {}) => {
|
const useAccessList = (id: number | "new", expand?: AccessListExpansion[], options = {}) => {
|
||||||
return useQuery<AccessList, Error>({
|
return useQuery<AccessList, Error>({
|
||||||
queryKey: ["access-list", id],
|
queryKey: ["access-list", id, expand],
|
||||||
queryFn: () => fetchAccessList(id),
|
queryFn: () => fetchAccessList(id, expand),
|
||||||
staleTime: 60 * 1000, // 1 minute
|
staleTime: 60 * 1000, // 1 minute
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
@@ -44,7 +50,7 @@ const useSetAccessList = () => {
|
|||||||
onError: (_, __, rollback: any) => rollback(),
|
onError: (_, __, rollback: any) => rollback(),
|
||||||
onSuccess: async ({ id }: AccessList) => {
|
onSuccess: async ({ id }: AccessList) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["access-list", id] });
|
queryClient.invalidateQueries({ queryKey: ["access-list", id] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["access-list"] });
|
queryClient.invalidateQueries({ queryKey: ["access-lists"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -51,6 +51,7 @@ const useSetDeadHost = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
|
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
|
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["host-report"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -58,6 +58,7 @@ const useSetProxyHost = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["proxy-host", id] });
|
queryClient.invalidateQueries({ queryKey: ["proxy-host", id] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
|
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["host-report"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -62,6 +62,7 @@ const useSetRedirectionHost = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["redirection-host", id] });
|
queryClient.invalidateQueries({ queryKey: ["redirection-host", id] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
|
queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["host-report"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -47,6 +47,7 @@ const useSetStream = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["stream", id] });
|
queryClient.invalidateQueries({ queryKey: ["stream", id] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["streams"] });
|
queryClient.invalidateQueries({ queryKey: ["streams"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["host-report"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"access.access-count": "{count} Rules",
|
"access.access-count": "{count} {count, plural, one {Rule} other {Rules}}",
|
||||||
"access.actions-title": "Access List #{id}",
|
"access.actions-title": "Access List #{id}",
|
||||||
"access.add": "Add Access List",
|
"access.add": "Add Access List",
|
||||||
"access.auth-count": "{count} Users",
|
"access.auth-count": "{count} {count, plural, one {User} other {Users}}",
|
||||||
"access.edit": "Edit Access",
|
"access.edit": "Edit Access",
|
||||||
"access.empty": "There are no Access Lists",
|
"access.empty": "There are no Access Lists",
|
||||||
|
"access.help-rules-last": "When at least 1 rule exists, this deny all rule will be added last",
|
||||||
|
"access.help.rules-order": "Note that the allow and deny directives will be applied in the order they are defined.",
|
||||||
"access.new": "New Access",
|
"access.new": "New Access",
|
||||||
"access.pass-auth": "Pass Auth to Upstream",
|
"access.pass-auth": "Pass Auth to Upstream",
|
||||||
"access.public": "Publicly Accessible",
|
"access.public": "Publicly Accessible",
|
||||||
"access.satisfy-any": "Satisfy Any",
|
"access.satisfy-any": "Satisfy Any",
|
||||||
"access.subtitle": "{users} User, {rules} Rules - Created: {date}",
|
"access.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}",
|
||||||
"access.title": "Access",
|
"access.title": "Access",
|
||||||
|
"action.add": "Add",
|
||||||
"action.delete": "Delete",
|
"action.delete": "Delete",
|
||||||
"action.disable": "Disable",
|
"action.disable": "Disable",
|
||||||
"action.edit": "Edit",
|
"action.edit": "Edit",
|
||||||
@@ -56,7 +59,7 @@
|
|||||||
"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}",
|
||||||
"dead-hosts.add": "Add 404 Host",
|
"dead-hosts.add": "Add 404 Host",
|
||||||
"dead-hosts.count": "{count} 404 Hosts",
|
"dead-hosts.count": "{count} {count, plural, one {404 Host} other {404 Hosts}}",
|
||||||
"dead-hosts.empty": "There are no 404 Hosts",
|
"dead-hosts.empty": "There are no 404 Hosts",
|
||||||
"dead-hosts.title": "404 Hosts",
|
"dead-hosts.title": "404 Hosts",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
@@ -74,6 +77,8 @@
|
|||||||
"empty-search": "No results found",
|
"empty-search": "No results found",
|
||||||
"empty-subtitle": "Why don't you create one?",
|
"empty-subtitle": "Why don't you create one?",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
|
"error.access.at-least-one": "Either one Authorization or one Access Rule is required",
|
||||||
|
"error.access.duplicate-usernames": "Authorization Usernames must be unique",
|
||||||
"error.invalid-auth": "Invalid email or password",
|
"error.invalid-auth": "Invalid email or password",
|
||||||
"error.invalid-domain": "Invalid domain: {domain}",
|
"error.invalid-domain": "Invalid domain: {domain}",
|
||||||
"error.invalid-email": "Invalid email address",
|
"error.invalid-email": "Invalid email address",
|
||||||
@@ -115,6 +120,7 @@
|
|||||||
"notfound.action": "Take me home",
|
"notfound.action": "Take me home",
|
||||||
"notfound.text": "We are sorry but the page you are looking for was not found",
|
"notfound.text": "We are sorry but the page you are looking for was not found",
|
||||||
"notfound.title": "Oops… You just found an error page",
|
"notfound.title": "Oops… You just found an error page",
|
||||||
|
"notification.access-deleted": "Access has been deleted",
|
||||||
"notification.access-saved": "Access has been saved",
|
"notification.access-saved": "Access has been saved",
|
||||||
"notification.dead-host-saved": "404 Host has been saved",
|
"notification.dead-host-saved": "404 Host has been saved",
|
||||||
"notification.error": "Error",
|
"notification.error": "Error",
|
||||||
@@ -146,7 +152,7 @@
|
|||||||
"proxy-host.new": "New Proxy Host",
|
"proxy-host.new": "New Proxy Host",
|
||||||
"proxy-hosts.actions-title": "Proxy Host #{id}",
|
"proxy-hosts.actions-title": "Proxy Host #{id}",
|
||||||
"proxy-hosts.add": "Add Proxy Host",
|
"proxy-hosts.add": "Add Proxy Host",
|
||||||
"proxy-hosts.count": "{count} Proxy Hosts",
|
"proxy-hosts.count": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}",
|
||||||
"proxy-hosts.empty": "There are no Proxy Hosts",
|
"proxy-hosts.empty": "There are no Proxy Hosts",
|
||||||
"proxy-hosts.title": "Proxy Hosts",
|
"proxy-hosts.title": "Proxy Hosts",
|
||||||
"redirection-host.delete.content": "Are you sure you want to delete this Redirection host?",
|
"redirection-host.delete.content": "Are you sure you want to delete this Redirection host?",
|
||||||
@@ -155,7 +161,7 @@
|
|||||||
"redirection-host.new": "New Redirection Host",
|
"redirection-host.new": "New Redirection Host",
|
||||||
"redirection-hosts.actions-title": "Redirection Host #{id}",
|
"redirection-hosts.actions-title": "Redirection Host #{id}",
|
||||||
"redirection-hosts.add": "Add Redirection Host",
|
"redirection-hosts.add": "Add Redirection Host",
|
||||||
"redirection-hosts.count": "{count} Redirection Hosts",
|
"redirection-hosts.count": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}",
|
||||||
"redirection-hosts.empty": "There are no Redirection Hosts",
|
"redirection-hosts.empty": "There are no Redirection Hosts",
|
||||||
"redirection-hosts.title": "Redirection Hosts",
|
"redirection-hosts.title": "Redirection Hosts",
|
||||||
"role.admin": "Administrator",
|
"role.admin": "Administrator",
|
||||||
@@ -174,7 +180,7 @@
|
|||||||
"stream.new": "New Stream",
|
"stream.new": "New Stream",
|
||||||
"streams.actions-title": "Stream #{id}",
|
"streams.actions-title": "Stream #{id}",
|
||||||
"streams.add": "Add Stream",
|
"streams.add": "Add Stream",
|
||||||
"streams.count": "{count} Streams",
|
"streams.count": "{count} {count, plural, one {Stream} other {Streams}}",
|
||||||
"streams.empty": "There are no Streams",
|
"streams.empty": "There are no Streams",
|
||||||
"streams.tcp": "TCP",
|
"streams.tcp": "TCP",
|
||||||
"streams.title": "Streams",
|
"streams.title": "Streams",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"access.access-count": {
|
"access.access-count": {
|
||||||
"defaultMessage": "{count} Rules"
|
"defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}"
|
||||||
},
|
},
|
||||||
"access.actions-title": {
|
"access.actions-title": {
|
||||||
"defaultMessage": "Access List #{id}"
|
"defaultMessage": "Access List #{id}"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"defaultMessage": "Add Access List"
|
"defaultMessage": "Add Access List"
|
||||||
},
|
},
|
||||||
"access.auth-count": {
|
"access.auth-count": {
|
||||||
"defaultMessage": "{count} Users"
|
"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
|
||||||
},
|
},
|
||||||
"access.edit": {
|
"access.edit": {
|
||||||
"defaultMessage": "Edit Access"
|
"defaultMessage": "Edit Access"
|
||||||
@@ -17,6 +17,12 @@
|
|||||||
"access.empty": {
|
"access.empty": {
|
||||||
"defaultMessage": "There are no Access Lists"
|
"defaultMessage": "There are no Access Lists"
|
||||||
},
|
},
|
||||||
|
"access.help-rules-last": {
|
||||||
|
"defaultMessage": "When at least 1 rule exists, this deny all rule will be added last"
|
||||||
|
},
|
||||||
|
"access.help.rules-order": {
|
||||||
|
"defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined."
|
||||||
|
},
|
||||||
"access.new": {
|
"access.new": {
|
||||||
"defaultMessage": "New Access"
|
"defaultMessage": "New Access"
|
||||||
},
|
},
|
||||||
@@ -30,11 +36,14 @@
|
|||||||
"defaultMessage": "Satisfy Any"
|
"defaultMessage": "Satisfy Any"
|
||||||
},
|
},
|
||||||
"access.subtitle": {
|
"access.subtitle": {
|
||||||
"defaultMessage": "{users} User, {rules} Rules - Created: {date}"
|
"defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}"
|
||||||
},
|
},
|
||||||
"access.title": {
|
"access.title": {
|
||||||
"defaultMessage": "Access"
|
"defaultMessage": "Access"
|
||||||
},
|
},
|
||||||
|
"action.add": {
|
||||||
|
"defaultMessage": "Add"
|
||||||
|
},
|
||||||
"action.delete": {
|
"action.delete": {
|
||||||
"defaultMessage": "Delete"
|
"defaultMessage": "Delete"
|
||||||
},
|
},
|
||||||
@@ -171,7 +180,7 @@
|
|||||||
"defaultMessage": "Add 404 Host"
|
"defaultMessage": "Add 404 Host"
|
||||||
},
|
},
|
||||||
"dead-hosts.count": {
|
"dead-hosts.count": {
|
||||||
"defaultMessage": "{count} 404 Hosts"
|
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
|
||||||
},
|
},
|
||||||
"dead-hosts.empty": {
|
"dead-hosts.empty": {
|
||||||
"defaultMessage": "There are no 404 Hosts"
|
"defaultMessage": "There are no 404 Hosts"
|
||||||
@@ -224,6 +233,12 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"defaultMessage": "Enabled"
|
"defaultMessage": "Enabled"
|
||||||
},
|
},
|
||||||
|
"error.access.at-least-one": {
|
||||||
|
"defaultMessage": "Either one Authorization or one Access Rule is required"
|
||||||
|
},
|
||||||
|
"error.access.duplicate-usernames": {
|
||||||
|
"defaultMessage": "Authorization Usernames must be unique"
|
||||||
|
},
|
||||||
"error.invalid-auth": {
|
"error.invalid-auth": {
|
||||||
"defaultMessage": "Invalid email or password"
|
"defaultMessage": "Invalid email or password"
|
||||||
},
|
},
|
||||||
@@ -347,6 +362,9 @@
|
|||||||
"notfound.title": {
|
"notfound.title": {
|
||||||
"defaultMessage": "Oops… You just found an error page"
|
"defaultMessage": "Oops… You just found an error page"
|
||||||
},
|
},
|
||||||
|
"notification.access-deleted": {
|
||||||
|
"defaultMessage": "Access has been deleted"
|
||||||
|
},
|
||||||
"notification.access-saved": {
|
"notification.access-saved": {
|
||||||
"defaultMessage": "Access has been saved"
|
"defaultMessage": "Access has been saved"
|
||||||
},
|
},
|
||||||
@@ -441,7 +459,7 @@
|
|||||||
"defaultMessage": "Add Proxy Host"
|
"defaultMessage": "Add Proxy Host"
|
||||||
},
|
},
|
||||||
"proxy-hosts.count": {
|
"proxy-hosts.count": {
|
||||||
"defaultMessage": "{count} Proxy Hosts"
|
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
|
||||||
},
|
},
|
||||||
"proxy-hosts.empty": {
|
"proxy-hosts.empty": {
|
||||||
"defaultMessage": "There are no Proxy Hosts"
|
"defaultMessage": "There are no Proxy Hosts"
|
||||||
@@ -468,7 +486,7 @@
|
|||||||
"defaultMessage": "Add Redirection Host"
|
"defaultMessage": "Add Redirection Host"
|
||||||
},
|
},
|
||||||
"redirection-hosts.count": {
|
"redirection-hosts.count": {
|
||||||
"defaultMessage": "{count} Redirection Hosts"
|
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
|
||||||
},
|
},
|
||||||
"redirection-hosts.empty": {
|
"redirection-hosts.empty": {
|
||||||
"defaultMessage": "There are no Redirection Hosts"
|
"defaultMessage": "There are no Redirection Hosts"
|
||||||
@@ -525,7 +543,7 @@
|
|||||||
"defaultMessage": "Add Stream"
|
"defaultMessage": "Add Stream"
|
||||||
},
|
},
|
||||||
"streams.count": {
|
"streams.count": {
|
||||||
"defaultMessage": "{count} Streams"
|
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
|
||||||
},
|
},
|
||||||
"streams.empty": {
|
"streams.empty": {
|
||||||
"defaultMessage": "There are no Streams"
|
"defaultMessage": "There are no Streams"
|
||||||
|
@@ -3,7 +3,8 @@ import { Field, Form, Formik } from "formik";
|
|||||||
import { type ReactNode, useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { Alert } from "react-bootstrap";
|
import { Alert } from "react-bootstrap";
|
||||||
import Modal from "react-bootstrap/Modal";
|
import Modal from "react-bootstrap/Modal";
|
||||||
import { BasicAuthField, Button, Loading } from "src/components";
|
import type { AccessList, AccessListClient, AccessListItem } from "src/api/backend";
|
||||||
|
import { AccessClientFields, BasicAuthFields, Button, Loading } from "src/components";
|
||||||
import { useAccessList, useSetAccessList } from "src/hooks";
|
import { useAccessList, useSetAccessList } from "src/hooks";
|
||||||
import { intl, T } from "src/locale";
|
import { intl, T } from "src/locale";
|
||||||
import { validateString } from "src/modules/Validations";
|
import { validateString } from "src/modules/Validations";
|
||||||
@@ -14,13 +15,36 @@ interface Props {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export function AccessListModal({ id, onClose }: Props) {
|
export function AccessListModal({ id, onClose }: Props) {
|
||||||
const { data, isLoading, error } = useAccessList(id);
|
const { data, isLoading, error } = useAccessList(id, ["items", "clients"]);
|
||||||
const { mutate: setAccessList } = useSetAccessList();
|
const { mutate: setAccessList } = useSetAccessList();
|
||||||
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
|
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const validate = (values: any): string | null => {
|
||||||
|
// either Auths or Clients must be defined
|
||||||
|
if (values.items?.length === 0 && values.clients?.length === 0) {
|
||||||
|
return intl.formatMessage({ id: "error.access.at-least-one" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the items don't contain the same username twice
|
||||||
|
const usernames = values.items.map((i: any) => i.username);
|
||||||
|
const uniqueUsernames = Array.from(new Set(usernames));
|
||||||
|
if (usernames.length !== uniqueUsernames.length) {
|
||||||
|
return intl.formatMessage({ id: "error.access.duplicate-usernames" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
const onSubmit = async (values: any, { setSubmitting }: any) => {
|
||||||
if (isSubmitting) return;
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
const vErr = validate(values);
|
||||||
|
if (vErr) {
|
||||||
|
setErrorMsg(vErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setErrorMsg(null);
|
setErrorMsg(null);
|
||||||
|
|
||||||
@@ -29,6 +53,18 @@ export function AccessListModal({ id, onClose }: Props) {
|
|||||||
...values,
|
...values,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter out "items" to only use the "username" and "password" fields
|
||||||
|
payload.items = (values.items || []).map((i: AccessListItem) => ({
|
||||||
|
username: i.username,
|
||||||
|
password: i.password,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Filter out "clients" to only use the "directive" and "address" fields
|
||||||
|
payload.clients = (values.clients || []).map((i: AccessListClient) => ({
|
||||||
|
directive: i.directive,
|
||||||
|
address: i.address,
|
||||||
|
}));
|
||||||
|
|
||||||
setAccessList(payload, {
|
setAccessList(payload, {
|
||||||
onError: (err: any) => setErrorMsg(<T id={err.message} />),
|
onError: (err: any) => setErrorMsg(<T id={err.message} />),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -60,9 +96,9 @@ export function AccessListModal({ id, onClose }: Props) {
|
|||||||
name: data?.name,
|
name: data?.name,
|
||||||
satisfyAny: data?.satisfyAny,
|
satisfyAny: data?.satisfyAny,
|
||||||
passAuth: data?.passAuth,
|
passAuth: data?.passAuth,
|
||||||
// todo: more? there's stuff missing here?
|
items: data?.items || [],
|
||||||
meta: data?.meta || {},
|
clients: data?.clients || [],
|
||||||
} as any
|
} as AccessList
|
||||||
}
|
}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
@@ -105,7 +141,7 @@ export function AccessListModal({ id, onClose }: Props) {
|
|||||||
</li>
|
</li>
|
||||||
<li className="nav-item" role="presentation">
|
<li className="nav-item" role="presentation">
|
||||||
<a
|
<a
|
||||||
href="#tab-access"
|
href="#tab-rules"
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
aria-selected="false"
|
aria-selected="false"
|
||||||
@@ -120,8 +156,8 @@ export function AccessListModal({ id, onClose }: Props) {
|
|||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
<div className="tab-pane active show" id="tab-details" role="tabpanel">
|
<div className="tab-pane active show" id="tab-details" role="tabpanel">
|
||||||
<Field name="name" validate={validateString(8, 255)}>
|
<Field name="name" validate={validateString(1, 255)}>
|
||||||
{({ field }: any) => (
|
{({ field, form }: any) => (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="form-label">
|
<label htmlFor="name" className="form-label">
|
||||||
<T id="column.name" />
|
<T id="column.name" />
|
||||||
@@ -134,6 +170,13 @@ export function AccessListModal({ id, onClose }: Props) {
|
|||||||
className="form-control"
|
className="form-control"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
{form.errors.name ? (
|
||||||
|
<div className="invalid-feedback">
|
||||||
|
{form.errors.name && form.touched.name
|
||||||
|
? form.errors.name
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -210,10 +253,10 @@ export function AccessListModal({ id, onClose }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tab-pane" id="tab-auth" role="tabpanel">
|
<div className="tab-pane" id="tab-auth" role="tabpanel">
|
||||||
<BasicAuthField />
|
<BasicAuthFields initialValues={data?.items || []} />
|
||||||
</div>
|
</div>
|
||||||
<div className="tab-pane" id="tab-rules" role="tabpanel">
|
<div className="tab-pane" id="tab-rules" role="tabpanel">
|
||||||
todo
|
<AccessClientFields initialValues={data?.clients || []} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -66,6 +66,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
|
|||||||
<input
|
<input
|
||||||
id="current"
|
id="current"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className={`form-control ${form.errors.current && form.touched.current ? "is-invalid" : ""}`}
|
className={`form-control ${form.errors.current && form.touched.current ? "is-invalid" : ""}`}
|
||||||
placeholder={intl.formatMessage({
|
placeholder={intl.formatMessage({
|
||||||
@@ -94,6 +95,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
|
|||||||
<input
|
<input
|
||||||
id="new"
|
id="new"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
className={`form-control ${form.errors.new && form.touched.new ? "is-invalid" : ""}`}
|
className={`form-control ${form.errors.new && form.touched.new ? "is-invalid" : ""}`}
|
||||||
placeholder={intl.formatMessage({ id: "user.new-password" })}
|
placeholder={intl.formatMessage({ id: "user.new-password" })}
|
||||||
@@ -118,6 +120,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
|
|||||||
<input
|
<input
|
||||||
id="confirm"
|
id="confirm"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
className={`form-control ${form.errors.confirm && form.touched.confirm ? "is-invalid" : ""}`}
|
className={`form-control ${form.errors.confirm && form.touched.confirm ? "is-invalid" : ""}`}
|
||||||
placeholder={intl.formatMessage({ id: "user.confirm-password" })}
|
placeholder={intl.formatMessage({ id: "user.confirm-password" })}
|
||||||
|
@@ -99,11 +99,11 @@ export default function Login() {
|
|||||||
<input
|
<input
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
maxLength={255}
|
maxLength={255}
|
||||||
className={`form-control ${form.errors.password && form.touched.password ? " is-invalid" : ""}`}
|
className={`form-control ${form.errors.password && form.touched.password ? " is-invalid" : ""}`}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
autoComplete="off"
|
|
||||||
/>
|
/>
|
||||||
<div className="invalid-feedback">{form.errors.password}</div>
|
<div className="invalid-feedback">{form.errors.password}</div>
|
||||||
</label>
|
</label>
|
||||||
|
@@ -154,6 +154,7 @@ export default function Setup() {
|
|||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
className={`form-control ${form.errors.password && form.touched.password ? "is-invalid" : ""}`}
|
className={`form-control ${form.errors.password && form.touched.password ? "is-invalid" : ""}`}
|
||||||
placeholder={intl.formatMessage({ id: "user.new-password" })}
|
placeholder={intl.formatMessage({ id: "user.new-password" })}
|
||||||
{...field}
|
{...field}
|
||||||
|
Reference in New Issue
Block a user