mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-22 19:43:32 +00:00
Access list modal polish
This commit is contained in:
@@ -74,3 +74,24 @@
|
||||
label.row {
|
||||
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;
|
||||
username: string;
|
||||
password: string;
|
||||
meta: Record<string, any>;
|
||||
hint: string;
|
||||
meta?: Record<string, any>;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export type AccessListClient = {
|
||||
@@ -78,7 +78,7 @@ export type AccessListClient = {
|
||||
accessListId?: number;
|
||||
address: string;
|
||||
directive: "allow" | "deny";
|
||||
meta: Record<string, any>;
|
||||
meta?: Record<string, any>;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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 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 "./BasicAuthField";
|
||||
export * from "./BasicAuthFields";
|
||||
export * from "./DNSProviderFields";
|
||||
export * from "./DomainNamesField";
|
||||
export * from "./NginxConfigField";
|
||||
|
@@ -1,7 +1,13 @@
|
||||
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") {
|
||||
return Promise.resolve({
|
||||
id: 0,
|
||||
@@ -14,13 +20,13 @@ const fetchAccessList = (id: number | "new") => {
|
||||
meta: {},
|
||||
} 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>({
|
||||
queryKey: ["access-list", id],
|
||||
queryFn: () => fetchAccessList(id),
|
||||
queryKey: ["access-list", id, expand],
|
||||
queryFn: () => fetchAccessList(id, expand),
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
@@ -44,7 +50,7 @@ const useSetAccessList = () => {
|
||||
onError: (_, __, rollback: any) => rollback(),
|
||||
onSuccess: async ({ id }: AccessList) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["access-list", id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["access-list"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["access-lists"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||
},
|
||||
});
|
||||
|
@@ -51,6 +51,7 @@ const useSetDeadHost = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
|
||||
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-hosts"] });
|
||||
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-hosts"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["host-report"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@@ -47,6 +47,7 @@ const useSetStream = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["stream", id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["streams"] });
|
||||
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.add": "Add Access List",
|
||||
"access.auth-count": "{count} Users",
|
||||
"access.auth-count": "{count} {count, plural, one {User} other {Users}}",
|
||||
"access.edit": "Edit Access",
|
||||
"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.pass-auth": "Pass Auth to Upstream",
|
||||
"access.public": "Publicly Accessible",
|
||||
"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",
|
||||
"action.add": "Add",
|
||||
"action.delete": "Delete",
|
||||
"action.disable": "Disable",
|
||||
"action.edit": "Edit",
|
||||
@@ -56,7 +59,7 @@
|
||||
"dead-host.new": "New 404 Host",
|
||||
"dead-hosts.actions-title": "404 Host #{id}",
|
||||
"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.title": "404 Hosts",
|
||||
"disabled": "Disabled",
|
||||
@@ -74,6 +77,8 @@
|
||||
"empty-search": "No results found",
|
||||
"empty-subtitle": "Why don't you create one?",
|
||||
"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-domain": "Invalid domain: {domain}",
|
||||
"error.invalid-email": "Invalid email address",
|
||||
@@ -115,6 +120,7 @@
|
||||
"notfound.action": "Take me home",
|
||||
"notfound.text": "We are sorry but the page you are looking for was not found",
|
||||
"notfound.title": "Oops… You just found an error page",
|
||||
"notification.access-deleted": "Access has been deleted",
|
||||
"notification.access-saved": "Access has been saved",
|
||||
"notification.dead-host-saved": "404 Host has been saved",
|
||||
"notification.error": "Error",
|
||||
@@ -146,7 +152,7 @@
|
||||
"proxy-host.new": "New Proxy Host",
|
||||
"proxy-hosts.actions-title": "Proxy Host #{id}",
|
||||
"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.title": "Proxy Hosts",
|
||||
"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-hosts.actions-title": "Redirection Host #{id}",
|
||||
"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.title": "Redirection Hosts",
|
||||
"role.admin": "Administrator",
|
||||
@@ -174,7 +180,7 @@
|
||||
"stream.new": "New Stream",
|
||||
"streams.actions-title": "Stream #{id}",
|
||||
"streams.add": "Add Stream",
|
||||
"streams.count": "{count} Streams",
|
||||
"streams.count": "{count} {count, plural, one {Stream} other {Streams}}",
|
||||
"streams.empty": "There are no Streams",
|
||||
"streams.tcp": "TCP",
|
||||
"streams.title": "Streams",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"access.access-count": {
|
||||
"defaultMessage": "{count} Rules"
|
||||
"defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}"
|
||||
},
|
||||
"access.actions-title": {
|
||||
"defaultMessage": "Access List #{id}"
|
||||
@@ -9,7 +9,7 @@
|
||||
"defaultMessage": "Add Access List"
|
||||
},
|
||||
"access.auth-count": {
|
||||
"defaultMessage": "{count} Users"
|
||||
"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
|
||||
},
|
||||
"access.edit": {
|
||||
"defaultMessage": "Edit Access"
|
||||
@@ -17,6 +17,12 @@
|
||||
"access.empty": {
|
||||
"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": {
|
||||
"defaultMessage": "New Access"
|
||||
},
|
||||
@@ -30,11 +36,14 @@
|
||||
"defaultMessage": "Satisfy Any"
|
||||
},
|
||||
"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": {
|
||||
"defaultMessage": "Access"
|
||||
},
|
||||
"action.add": {
|
||||
"defaultMessage": "Add"
|
||||
},
|
||||
"action.delete": {
|
||||
"defaultMessage": "Delete"
|
||||
},
|
||||
@@ -171,7 +180,7 @@
|
||||
"defaultMessage": "Add 404 Host"
|
||||
},
|
||||
"dead-hosts.count": {
|
||||
"defaultMessage": "{count} 404 Hosts"
|
||||
"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
|
||||
},
|
||||
"dead-hosts.empty": {
|
||||
"defaultMessage": "There are no 404 Hosts"
|
||||
@@ -224,6 +233,12 @@
|
||||
"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": {
|
||||
"defaultMessage": "Invalid email or password"
|
||||
},
|
||||
@@ -347,6 +362,9 @@
|
||||
"notfound.title": {
|
||||
"defaultMessage": "Oops… You just found an error page"
|
||||
},
|
||||
"notification.access-deleted": {
|
||||
"defaultMessage": "Access has been deleted"
|
||||
},
|
||||
"notification.access-saved": {
|
||||
"defaultMessage": "Access has been saved"
|
||||
},
|
||||
@@ -441,7 +459,7 @@
|
||||
"defaultMessage": "Add Proxy Host"
|
||||
},
|
||||
"proxy-hosts.count": {
|
||||
"defaultMessage": "{count} Proxy Hosts"
|
||||
"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
|
||||
},
|
||||
"proxy-hosts.empty": {
|
||||
"defaultMessage": "There are no Proxy Hosts"
|
||||
@@ -468,7 +486,7 @@
|
||||
"defaultMessage": "Add Redirection Host"
|
||||
},
|
||||
"redirection-hosts.count": {
|
||||
"defaultMessage": "{count} Redirection Hosts"
|
||||
"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
|
||||
},
|
||||
"redirection-hosts.empty": {
|
||||
"defaultMessage": "There are no Redirection Hosts"
|
||||
@@ -525,7 +543,7 @@
|
||||
"defaultMessage": "Add Stream"
|
||||
},
|
||||
"streams.count": {
|
||||
"defaultMessage": "{count} Streams"
|
||||
"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
|
||||
},
|
||||
"streams.empty": {
|
||||
"defaultMessage": "There are no Streams"
|
||||
|
@@ -3,7 +3,8 @@ import { Field, Form, Formik } from "formik";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { Alert } from "react-bootstrap";
|
||||
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 { intl, T } from "src/locale";
|
||||
import { validateString } from "src/modules/Validations";
|
||||
@@ -14,13 +15,36 @@ interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
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 [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
|
||||
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) => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
const vErr = validate(values);
|
||||
if (vErr) {
|
||||
setErrorMsg(vErr);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrorMsg(null);
|
||||
|
||||
@@ -29,6 +53,18 @@ export function AccessListModal({ id, onClose }: Props) {
|
||||
...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, {
|
||||
onError: (err: any) => setErrorMsg(<T id={err.message} />),
|
||||
onSuccess: () => {
|
||||
@@ -60,9 +96,9 @@ export function AccessListModal({ id, onClose }: Props) {
|
||||
name: data?.name,
|
||||
satisfyAny: data?.satisfyAny,
|
||||
passAuth: data?.passAuth,
|
||||
// todo: more? there's stuff missing here?
|
||||
meta: data?.meta || {},
|
||||
} as any
|
||||
items: data?.items || [],
|
||||
clients: data?.clients || [],
|
||||
} as AccessList
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
@@ -105,7 +141,7 @@ export function AccessListModal({ id, onClose }: Props) {
|
||||
</li>
|
||||
<li className="nav-item" role="presentation">
|
||||
<a
|
||||
href="#tab-access"
|
||||
href="#tab-rules"
|
||||
className="nav-link"
|
||||
data-bs-toggle="tab"
|
||||
aria-selected="false"
|
||||
@@ -120,8 +156,8 @@ export function AccessListModal({ id, onClose }: Props) {
|
||||
<div className="card-body">
|
||||
<div className="tab-content">
|
||||
<div className="tab-pane active show" id="tab-details" role="tabpanel">
|
||||
<Field name="name" validate={validateString(8, 255)}>
|
||||
{({ field }: any) => (
|
||||
<Field name="name" validate={validateString(1, 255)}>
|
||||
{({ field, form }: any) => (
|
||||
<div>
|
||||
<label htmlFor="name" className="form-label">
|
||||
<T id="column.name" />
|
||||
@@ -134,6 +170,13 @@ export function AccessListModal({ id, onClose }: Props) {
|
||||
className="form-control"
|
||||
{...field}
|
||||
/>
|
||||
{form.errors.name ? (
|
||||
<div className="invalid-feedback">
|
||||
{form.errors.name && form.touched.name
|
||||
? form.errors.name
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
@@ -210,10 +253,10 @@ export function AccessListModal({ id, onClose }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="tab-pane" id="tab-auth" role="tabpanel">
|
||||
<BasicAuthField />
|
||||
<BasicAuthFields initialValues={data?.items || []} />
|
||||
</div>
|
||||
<div className="tab-pane" id="tab-rules" role="tabpanel">
|
||||
todo
|
||||
<AccessClientFields initialValues={data?.clients || []} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -66,6 +66,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
|
||||
<input
|
||||
id="current"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className={`form-control ${form.errors.current && form.touched.current ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({
|
||||
@@ -94,6 +95,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
|
||||
<input
|
||||
id="new"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`form-control ${form.errors.new && form.touched.new ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.new-password" })}
|
||||
@@ -118,6 +120,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
|
||||
<input
|
||||
id="confirm"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`form-control ${form.errors.confirm && form.touched.confirm ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.confirm-password" })}
|
||||
|
@@ -99,11 +99,11 @@ export default function Login() {
|
||||
<input
|
||||
{...field}
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
maxLength={255}
|
||||
className={`form-control ${form.errors.password && form.touched.password ? " is-invalid" : ""}`}
|
||||
placeholder="Password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<div className="invalid-feedback">{form.errors.password}</div>
|
||||
</label>
|
||||
|
@@ -154,6 +154,7 @@ export default function Setup() {
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={`form-control ${form.errors.password && form.touched.password ? "is-invalid" : ""}`}
|
||||
placeholder={intl.formatMessage({ id: "user.new-password" })}
|
||||
{...field}
|
||||
|
Reference in New Issue
Block a user