Add API tokens support #9
@@ -53,6 +53,7 @@ struct ServerConstraints {
 | 
				
			|||||||
    nwfilter_selectors_count: LenConstraints,
 | 
					    nwfilter_selectors_count: LenConstraints,
 | 
				
			||||||
    api_token_name_size: LenConstraints,
 | 
					    api_token_name_size: LenConstraints,
 | 
				
			||||||
    api_token_description_size: LenConstraints,
 | 
					    api_token_description_size: LenConstraints,
 | 
				
			||||||
 | 
					    api_token_right_path_size: LenConstraints,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
					pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
				
			||||||
@@ -110,6 +111,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
				
			|||||||
                min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
 | 
					                min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
 | 
				
			||||||
                max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
 | 
					                max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            api_token_right_path_size: LenConstraints { min: 0, max: 255 },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -85,7 +85,7 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let mut cors = Cors::default()
 | 
					        let mut cors = Cors::default()
 | 
				
			||||||
            .allowed_origin(&AppConfig::get().website_origin)
 | 
					            .allowed_origin(&AppConfig::get().website_origin)
 | 
				
			||||||
            .allowed_methods(vec!["GET", "POST", "DELETE", "PUT"])
 | 
					            .allowed_methods(vec!["GET", "POST", "DELETE", "PUT", "PATCH"])
 | 
				
			||||||
            .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
 | 
					            .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
 | 
				
			||||||
            .allowed_header(header::CONTENT_TYPE)
 | 
					            .allowed_header(header::CONTENT_TYPE)
 | 
				
			||||||
            .supports_credentials()
 | 
					            .supports_credentials()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,7 @@ export interface ServerConstraints {
 | 
				
			|||||||
  nwfilter_selectors_count: LenConstraint;
 | 
					  nwfilter_selectors_count: LenConstraint;
 | 
				
			||||||
  api_token_name_size: LenConstraint;
 | 
					  api_token_name_size: LenConstraint;
 | 
				
			||||||
  api_token_description_size: LenConstraint;
 | 
					  api_token_description_size: LenConstraint;
 | 
				
			||||||
 | 
					  api_token_right_path_size: LenConstraint;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LenConstraint {
 | 
					export interface LenConstraint {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ export interface SelectOption {
 | 
				
			|||||||
export function SelectInput(p: {
 | 
					export function SelectInput(p: {
 | 
				
			||||||
  value?: string;
 | 
					  value?: string;
 | 
				
			||||||
  editable: boolean;
 | 
					  editable: boolean;
 | 
				
			||||||
  label: string;
 | 
					  label?: string;
 | 
				
			||||||
  options: SelectOption[];
 | 
					  options: SelectOption[];
 | 
				
			||||||
  onValueChange: (o?: string) => void;
 | 
					  onValueChange: (o?: string) => void;
 | 
				
			||||||
}): React.ReactElement {
 | 
					}): React.ReactElement {
 | 
				
			||||||
@@ -29,7 +29,7 @@ export function SelectInput(p: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
 | 
					    <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
 | 
				
			||||||
      <InputLabel>{p.label}</InputLabel>
 | 
					      {p.label && <InputLabel>{p.label}</InputLabel>}
 | 
				
			||||||
      <Select
 | 
					      <Select
 | 
				
			||||||
        value={p.value ?? ""}
 | 
					        value={p.value ?? ""}
 | 
				
			||||||
        label={p.label}
 | 
					        label={p.label}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ import { EditSection } from "../forms/EditSection";
 | 
				
			|||||||
import { IPInputWithMask } from "../forms/IPInput";
 | 
					import { IPInputWithMask } from "../forms/IPInput";
 | 
				
			||||||
import { RadioGroupInput } from "../forms/RadioGroupInput";
 | 
					import { RadioGroupInput } from "../forms/RadioGroupInput";
 | 
				
			||||||
import { TextInput } from "../forms/TextInput";
 | 
					import { TextInput } from "../forms/TextInput";
 | 
				
			||||||
 | 
					import { RawRightsEditor } from "./RawRightsEditor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SECS_PER_DAY = 3600 * 24;
 | 
					const SECS_PER_DAY = 3600 * 24;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -104,14 +105,15 @@ function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      {currTab === TokenTab.General && <NetworkDetailsTabGeneral {...p} />}
 | 
					      {currTab === TokenTab.General && <APITokenTabGeneral {...p} />}
 | 
				
			||||||
      {/* todo: rights */}
 | 
					      {/* todo: rights */}
 | 
				
			||||||
 | 
					      {currTab === TokenTab.RawRights && <APITokenRawRights {...p} />}
 | 
				
			||||||
      {currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
 | 
					      {currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
 | 
					function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
 | 
				
			||||||
  const [ipVersion, setIpVersion] = React.useState<4 | 6>(
 | 
					  const [ipVersion, setIpVersion] = React.useState<4 | 6>(
 | 
				
			||||||
    (p.token.ip_restriction ?? "").includes(":") ? 6 : 4
 | 
					    (p.token.ip_restriction ?? "").includes(":") ? 6 : 4
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@@ -196,6 +198,14 @@ function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function APITokenRawRights(p: DetailsInnerProps): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={{ padding: "30px" }}>
 | 
				
			||||||
 | 
					      <RawRightsEditor {...p} editable={p.status !== TokenWidgetStatus.Read} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
 | 
					function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
 | 
				
			||||||
  const confirm = useConfirm();
 | 
					  const confirm = useConfirm();
 | 
				
			||||||
  const snackbar = useSnackbar();
 | 
					  const snackbar = useSnackbar();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										111
									
								
								virtweb_frontend/src/widgets/tokens/RawRightsEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								virtweb_frontend/src/widgets/tokens/RawRightsEditor.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					import AddIcon from "@mui/icons-material/Add";
 | 
				
			||||||
 | 
					import DeleteIcon from "@mui/icons-material/Delete";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  Paper,
 | 
				
			||||||
 | 
					  Table,
 | 
				
			||||||
 | 
					  TableBody,
 | 
				
			||||||
 | 
					  TableCell,
 | 
				
			||||||
 | 
					  TableContainer,
 | 
				
			||||||
 | 
					  TableHead,
 | 
				
			||||||
 | 
					  TableRow,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					  Typography,
 | 
				
			||||||
 | 
					} from "@mui/material";
 | 
				
			||||||
 | 
					import { ServerApi } from "../../api/ServerApi";
 | 
				
			||||||
 | 
					import { APIToken } from "../../api/TokensApi";
 | 
				
			||||||
 | 
					import { SelectInput } from "../forms/SelectInput";
 | 
				
			||||||
 | 
					import { TextInput } from "../forms/TextInput";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RawRightsEditor(p: {
 | 
				
			||||||
 | 
					  token: APIToken;
 | 
				
			||||||
 | 
					  editable: boolean;
 | 
				
			||||||
 | 
					  onChange?: () => void;
 | 
				
			||||||
 | 
					}): React.ReactElement {
 | 
				
			||||||
 | 
					  const addRule = () => {
 | 
				
			||||||
 | 
					    p.token.rights.push({ path: "/api/", verb: "GET" });
 | 
				
			||||||
 | 
					    p.onChange?.();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const deleteRule = (id: number) => {
 | 
				
			||||||
 | 
					    p.token.rights.splice(id, 1);
 | 
				
			||||||
 | 
					    p.onChange?.();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <TableContainer component={Paper}>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        style={{
 | 
				
			||||||
 | 
					          padding: "10px 10px 0px 10px",
 | 
				
			||||||
 | 
					          display: "flex",
 | 
				
			||||||
 | 
					          justifyContent: "space-between",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Typography variant="h5">Raw rights</Typography>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          {p.editable && (
 | 
				
			||||||
 | 
					            <Tooltip title="Add a new right rule">
 | 
				
			||||||
 | 
					              <IconButton onClick={addRule}>
 | 
				
			||||||
 | 
					                <AddIcon />
 | 
				
			||||||
 | 
					              </IconButton>
 | 
				
			||||||
 | 
					            </Tooltip>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Table>
 | 
				
			||||||
 | 
					        <TableHead>
 | 
				
			||||||
 | 
					          <TableRow>
 | 
				
			||||||
 | 
					            <TableCell>Verb</TableCell>
 | 
				
			||||||
 | 
					            <TableCell>URI</TableCell>
 | 
				
			||||||
 | 
					            {p.editable && <TableCell>Actions</TableCell>}
 | 
				
			||||||
 | 
					          </TableRow>
 | 
				
			||||||
 | 
					        </TableHead>
 | 
				
			||||||
 | 
					        <TableBody>
 | 
				
			||||||
 | 
					          {p.token.rights.map((r, num) => (
 | 
				
			||||||
 | 
					            <TableRow key={num} hover>
 | 
				
			||||||
 | 
					              <TableCell style={{ width: "100px" }}>
 | 
				
			||||||
 | 
					                <SelectInput
 | 
				
			||||||
 | 
					                  {...p}
 | 
				
			||||||
 | 
					                  value={r.verb}
 | 
				
			||||||
 | 
					                  onValueChange={(v) => {
 | 
				
			||||||
 | 
					                    r.verb = v as any;
 | 
				
			||||||
 | 
					                    p.onChange?.();
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  options={[
 | 
				
			||||||
 | 
					                    { value: "GET" },
 | 
				
			||||||
 | 
					                    { value: "POST" },
 | 
				
			||||||
 | 
					                    { value: "PATCH" },
 | 
				
			||||||
 | 
					                    { value: "PUT" },
 | 
				
			||||||
 | 
					                    { value: "DELETE" },
 | 
				
			||||||
 | 
					                  ]}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </TableCell>
 | 
				
			||||||
 | 
					              <TableCell>
 | 
				
			||||||
 | 
					                <TextInput
 | 
				
			||||||
 | 
					                  {...p}
 | 
				
			||||||
 | 
					                  value={r.path}
 | 
				
			||||||
 | 
					                  onValueChange={(v) => {
 | 
				
			||||||
 | 
					                    r.path = v ?? "";
 | 
				
			||||||
 | 
					                    p.onChange?.();
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  checkValue={(v) => v.startsWith("/api/")}
 | 
				
			||||||
 | 
					                  size={ServerApi.Config.constraints.api_token_right_path_size}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </TableCell>
 | 
				
			||||||
 | 
					              {p.editable && (
 | 
				
			||||||
 | 
					                <TableCell style={{ width: "100px" }}>
 | 
				
			||||||
 | 
					                  <IconButton onClick={() => deleteRule(num)}>
 | 
				
			||||||
 | 
					                    <Tooltip title="Remove the rule">
 | 
				
			||||||
 | 
					                      <DeleteIcon />
 | 
				
			||||||
 | 
					                    </Tooltip>
 | 
				
			||||||
 | 
					                  </IconButton>
 | 
				
			||||||
 | 
					                </TableCell>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </TableRow>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </TableBody>
 | 
				
			||||||
 | 
					      </Table>
 | 
				
			||||||
 | 
					    </TableContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user