Certificate Authority work

This commit is contained in:
Jamie Curnow
2021-07-29 17:45:14 +10:00
parent ae00ab09e4
commit 339ee13346
35 changed files with 737 additions and 136 deletions

View File

@@ -1,5 +1,5 @@
import * as api from "./base";
import { UserResponse } from "./responseTypes";
import { User } from "./models";
interface AuthOptions {
type: string;
@@ -20,7 +20,7 @@ interface Options {
export async function createUser(
{ payload }: Options,
abortController?: AbortController,
): Promise<UserResponse> {
): Promise<User> {
const { result } = await api.post(
{
url: "/users",

View File

@@ -1,9 +1,7 @@
import * as api from "./base";
import { UserResponse } from "./responseTypes";
import { User } from "./models";
export async function getUser(
id: number | string = "me",
): Promise<UserResponse> {
export async function getUser(id: number | string = "me"): Promise<User> {
const userId = id ? id : "me";
const { result } = await api.get({
url: `/users/${userId}`,

View File

@@ -3,6 +3,9 @@ export * from "./getToken";
export * from "./getUser";
export * from "./models";
export * from "./refreshToken";
export * from "./requestCertificateAuthorities";
export * from "./requestCertificates";
export * from "./requestHealth";
export * from "./requestSettings";
export * from "./requestUsers";
export * from "./responseTypes";

View File

@@ -3,6 +3,27 @@ export interface Sort {
direction: "ASC" | "DESC";
}
export interface UserAuth {
id: number;
userId: number;
type: string;
createdOn: number;
updatedOn: number;
}
export interface User {
id: number;
name: string;
nickname: string;
email: string;
createdOn: number;
updatedOn: number;
roles: string[];
gravatarUrl: string;
isDisabled: boolean;
auth?: UserAuth;
}
export interface Setting {
id: number;
createdOn: number;
@@ -10,3 +31,27 @@ export interface Setting {
name: string;
value: any;
}
export interface Certificate {
id: number;
createdOn: number;
modifiedOn: number;
name: string;
acmeshServer: string;
caBundle: string;
maxDomains: number;
isWildcardSupported: boolean;
isSetup: boolean;
}
export interface CertificateAuthority {
id: number;
createdOn: number;
modifiedOn: number;
name: string;
acmeshServer: string;
caBundle: string;
maxDomains: number;
isWildcardSupported: boolean;
isSetup: boolean;
}

View File

@@ -0,0 +1,16 @@
import * as api from "./base";
import { CertificateAuthoritiesResponse } from "./responseTypes";
export async function requestCertificateAuthorities(
offset?: number,
abortController?: AbortController,
): Promise<CertificateAuthoritiesResponse> {
const { result } = await api.get(
{
url: "certificate-authorities",
params: { limit: 20, offset: offset || 0 },
},
abortController,
);
return result;
}

View File

@@ -0,0 +1,16 @@
import * as api from "./base";
import { CertificatesResponse } from "./responseTypes";
export async function requestCertificates(
offset?: number,
abortController?: AbortController,
): Promise<CertificatesResponse> {
const { result } = await api.get(
{
url: "certificates",
params: { limit: 20, offset: offset || 0 },
},
abortController,
);
return result;
}

View File

@@ -0,0 +1,16 @@
import * as api from "./base";
import { UsersResponse } from "./responseTypes";
export async function requestUsers(
offset?: number,
abortController?: AbortController,
): Promise<UsersResponse> {
const { result } = await api.get(
{
url: "users",
params: { limit: 20, offset: offset || 0 },
},
abortController,
);
return result;
}

View File

@@ -1,4 +1,10 @@
import { Sort, Setting } from "./models";
import {
Certificate,
CertificateAuthority,
Setting,
Sort,
User,
} from "./models";
export interface HealthResponse {
commit: string;
@@ -8,32 +14,11 @@ export interface HealthResponse {
version: string;
}
export interface UserAuthResponse {
id: number;
userId: number;
type: string;
createdOn: number;
updatedOn: number;
}
export interface TokenResponse {
expires: number;
token: string;
}
export interface UserResponse {
id: number;
name: string;
nickname: string;
email: string;
createdOn: number;
updatedOn: number;
roles: string[];
gravatarUrl: string;
isDisabled: boolean;
auth?: UserAuthResponse;
}
export interface SettingsResponse {
total: number;
offset: number;
@@ -41,3 +26,27 @@ export interface SettingsResponse {
sort: Sort[];
items: Setting[];
}
export interface CertificatesResponse {
total: number;
offset: number;
limit: number;
sort: Sort[];
items: Certificate[];
}
export interface CertificateAuthoritiesResponse {
total: number;
offset: number;
limit: number;
sort: Sort[];
items: CertificateAuthority[];
}
export interface UsersResponse {
total: number;
offset: number;
limit: number;
sort: Sort[];
items: User[];
}

View File

@@ -1,7 +1,8 @@
import React from "react";
import { Navigation } from "components";
import { Dropdown, Navigation } from "components";
import { intl } from "locale";
import { Link } from "react-router-dom";
import {
Book,
DeviceDesktop,
@@ -43,20 +44,30 @@ function NavMenu() {
to: "/access-lists",
},
{
title: intl.formatMessage({
id: "certificates.title",
defaultMessage: "Certificates",
}),
title: "SSL",
icon: <Shield />,
to: "/certificates",
},
{
title: intl.formatMessage({
id: "users.title",
defaultMessage: "Users",
}),
icon: <Users />,
to: "/users",
dropdownItems: [
<Dropdown.Item key="ssl-certificates">
<Link to="/ssl/certificates" role="button" aria-expanded="false">
<span className="nav-link-title">
{intl.formatMessage({
id: "certificates.title",
defaultMessage: "Certificates",
})}
</span>
</Link>
</Dropdown.Item>,
<Dropdown.Item key="ssl-authorities">
<Link to="/ssl/authorities" role="button" aria-expanded="false">
<span className="nav-link-title">
{intl.formatMessage({
id: "cert_authorities.title",
defaultMessage: "Certificate Authorities",
})}
</span>
</Link>
</Dropdown.Item>,
],
},
{
title: intl.formatMessage({
@@ -66,6 +77,14 @@ function NavMenu() {
icon: <Book />,
to: "/audit-log",
},
{
title: intl.formatMessage({
id: "users.title",
defaultMessage: "Users",
}),
icon: <Users />,
to: "/users",
},
{
title: intl.formatMessage({
id: "settings.title",

View File

@@ -12,6 +12,9 @@ import { BrowserRouter, Switch, Route } from "react-router-dom";
const AccessLists = lazy(() => import("pages/AccessLists"));
const AuditLog = lazy(() => import("pages/AuditLog"));
const Certificates = lazy(() => import("pages/Certificates"));
const CertificateAuthorities = lazy(
() => import("pages/CertificateAuthorities"),
);
const Dashboard = lazy(() => import("pages/Dashboard"));
const Hosts = lazy(() => import("pages/Hosts"));
const Login = lazy(() => import("pages/Login"));
@@ -54,9 +57,12 @@ function Router() {
<Route path="/hosts">
<Hosts />
</Route>
<Route path="/certificates">
<Route path="/ssl/certificates">
<Certificates />
</Route>
<Route path="/ssl/authorities">
<CertificateAuthorities />
</Route>
<Route path="/audit-log">
<AuditLog />
</Route>

View File

@@ -1,6 +1,8 @@
import React from "react";
import cn from "classnames";
import { Badge } from "components";
import { intl } from "locale";
export interface TableColumn {
/**
@@ -52,9 +54,33 @@ export const Table = ({ columns, data, pagination, sortBy }: TableProps) => {
switch (given) {
// Simple ID column has text-muted
case "id":
return (val: any) => {
return (val: number) => {
return <span className="text-muted">{val}</span>;
};
case "setup":
return (val: boolean) => {
return (
<Badge color={val ? "lime" : "red"}>
{val
? intl.formatMessage({
id: "ready",
defaultMessage: "Ready",
})
: intl.formatMessage({
id: "required",
defaultMessage: "Required",
})}
</Badge>
);
};
case "bool":
return (val: boolean) => {
return (
<Badge color={val ? "lime" : "red"}>
{val ? "true" : "false"}
</Badge>
);
};
}
}

View File

@@ -1,18 +1,18 @@
import React, { useState, useEffect } from "react";
import { getUser, UserResponse } from "api/npm";
import { getUser, User } from "api/npm";
import { useAuthState } from "context";
// Context
const initalValue = null;
const UserContext = React.createContext<UserResponse | null>(initalValue);
const UserContext = React.createContext<User | null>(initalValue);
// Provider
interface Props {
children?: JSX.Element;
}
function UserProvider({ children }: Props) {
const [userData, setUserData] = useState<UserResponse>({
const [userData, setUserData] = useState<User>({
id: 0,
name: "",
nickname: "",

View File

@@ -5,6 +5,9 @@
"auditlog.title": {
"defaultMessage": "Audit Log"
},
"cert_authorities.title": {
"defaultMessage": "Certificate Authorities"
},
"certificates.title": {
"defaultMessage": "Certificates"
},
@@ -14,12 +17,21 @@
"column.id": {
"defaultMessage": "ID"
},
"column.max_domains": {
"defaultMessage": "Domains per Cert"
},
"column.name": {
"defaultMessage": "Name"
},
"column.status": {
"defaultMessage": "Status"
},
"dashboard.title": {
"defaultMessage": "Dashboard"
},
"column.wildcard_support": {
"defaultMessage": "Wildcard Support"
},
"footer.changelog": {
"defaultMessage": "Change Log"
},
@@ -47,6 +59,12 @@
"profile.title": {
"defaultMessage": "Profile settings"
},
"ready": {
"defaultMessage": "Ready"
},
"required": {
"defaultMessage": "Required"
},
"settings.title": {
"defaultMessage": "Settings"
},

View File

@@ -0,0 +1,115 @@
import React, { useState, useEffect, useCallback } from "react";
import {
CertificateAuthoritiesResponse,
requestCertificateAuthorities,
} from "api/npm";
import { Table } from "components";
import { SuspenseLoader } from "components";
import { intl } from "locale";
import { useInterval } from "rooks";
import styled from "styled-components";
const Root = styled.div`
display: flex;
flex-direction: column;
`;
function CertificateAuthorities() {
const [data, setData] = useState({} as CertificateAuthoritiesResponse);
const [offset, setOffset] = useState(0);
const asyncFetch = useCallback(() => {
requestCertificateAuthorities(offset)
.then(setData)
.catch((error: any) => {
console.error("fetch data failed", error);
});
}, [offset]);
useEffect(() => {
asyncFetch();
}, [asyncFetch]);
// 1 Minute
useInterval(asyncFetch, 1 * 60 * 1000, true);
const cols = [
/*
{
name: "id",
title: intl.formatMessage({ id: "column.id", defaultMessage: "ID" }),
formatter: "id",
className: "w-1",
},
*/
{
name: "name",
title: intl.formatMessage({ id: "column.name", defaultMessage: "Name" }),
},
{
name: "maxDomains",
title: intl.formatMessage({
id: "column.max_domains",
defaultMessage: "Max Domains",
}),
},
{
name: "isWildcardSupported",
title: intl.formatMessage({
id: "column.wildcard_support",
defaultMessage: "Wildcard Support",
}),
formatter: "bool",
},
{
name: "isSetup",
title: intl.formatMessage({
id: "column.status",
defaultMessage: "Status",
}),
formatter: "setup",
},
];
if (typeof data.total !== "undefined" && data.total) {
return (
<Root>
<div className="card">
<div className="card-status-top bg-orange" />
<div className="card-header">
<h3 className="card-title">
{intl.formatMessage({
id: "cert_authorities.title",
defaultMessage: "Certificate Authorities",
})}
</h3>
</div>
<Table
columns={cols}
data={data.items}
sortBy={data.sort[0].field}
pagination={{
limit: data.limit,
offset: data.offset,
total: data.total,
onSetOffset: (num: number) => {
if (offset !== num) {
setOffset(num);
}
},
}}
/>
</div>
</Root>
);
}
if (typeof data.total !== "undefined") {
return <p>No items!</p>;
}
return <SuspenseLoader />;
}
export default CertificateAuthorities;

View File

@@ -1,6 +1,10 @@
import React from "react";
import React, { useState, useEffect, useCallback } from "react";
import { CertificatesResponse, requestCertificates } from "api/npm";
import { Table } from "components";
import { SuspenseLoader } from "components";
import { intl } from "locale";
import { useInterval } from "rooks";
import styled from "styled-components";
const Root = styled.div`
@@ -8,22 +12,76 @@ const Root = styled.div`
flex-direction: column;
`;
function Certificates() {
return (
<Root>
<div className="card">
<div className="card-status-top bg-cyan" />
<div className="card-header">
<h3 className="card-title">
{intl.formatMessage({
id: "certificates.title",
defaultMessage: "Certificates",
})}
</h3>
function CertificateAuthorities() {
const [data, setData] = useState({} as CertificatesResponse);
const [offset, setOffset] = useState(0);
const asyncFetch = useCallback(() => {
requestCertificates(offset)
.then(setData)
.catch((error: any) => {
console.error("fetch data failed", error);
});
}, [offset]);
useEffect(() => {
asyncFetch();
}, [asyncFetch]);
// 1 Minute
useInterval(asyncFetch, 1 * 60 * 1000, true);
const cols = [
{
name: "id",
title: intl.formatMessage({ id: "column.id", defaultMessage: "ID" }),
formatter: "id",
className: "w-1",
},
{
name: "name",
title: intl.formatMessage({ id: "column.name", defaultMessage: "Name" }),
},
];
if (typeof data.total !== "undefined" && data.total) {
return (
<Root>
<div className="card">
<div className="card-status-top bg-yellow" />
<div className="card-header">
<h3 className="card-title">
{intl.formatMessage({
id: "certificates.title",
defaultMessage: "Certificates",
})}
</h3>
</div>
<Table
columns={cols}
data={data.items}
sortBy={data.sort[0].field}
pagination={{
limit: data.limit,
offset: data.offset,
total: data.total,
onSetOffset: (num: number) => {
if (offset !== num) {
setOffset(num);
}
},
}}
/>
</div>
</div>
</Root>
);
</Root>
);
}
if (typeof data.total !== "undefined") {
return <p>No items!</p>;
}
return <SuspenseLoader />;
}
export default Certificates;
export default CertificateAuthorities;

View File

@@ -1,6 +1,10 @@
import React from "react";
import React, { useState, useEffect, useCallback } from "react";
import { UsersResponse, requestUsers } from "api/npm";
import { Table } from "components";
import { SuspenseLoader } from "components";
import { intl } from "locale";
import { useInterval } from "rooks";
import styled from "styled-components";
const Root = styled.div`
@@ -9,21 +13,79 @@ const Root = styled.div`
`;
function Users() {
return (
<Root>
<div className="card">
<div className="card-status-top bg-cyan" />
<div className="card-header">
<h3 className="card-title">
{intl.formatMessage({
id: "users.title",
defaultMessage: "Users",
})}
</h3>
const [data, setData] = useState({} as UsersResponse);
const [offset, setOffset] = useState(0);
const asyncFetch = useCallback(() => {
requestUsers(offset)
.then(setData)
.catch((error: any) => {
console.error("fetch data failed", error);
});
}, [offset]);
useEffect(() => {
asyncFetch();
}, [asyncFetch]);
// 1 Minute
useInterval(asyncFetch, 1 * 60 * 1000, true);
const cols = [
{
name: "id",
title: intl.formatMessage({ id: "column.id", defaultMessage: "ID" }),
formatter: "id",
className: "w-1",
},
{
name: "name",
title: intl.formatMessage({ id: "users.name", defaultMessage: "Name" }),
},
{
name: "email",
title: intl.formatMessage({ id: "users.email", defaultMessage: "Email" }),
},
];
if (typeof data.total !== "undefined" && data.total) {
return (
<Root>
<div className="card">
<div className="card-status-top bg-indigo" />
<div className="card-header">
<h3 className="card-title">
{intl.formatMessage({
id: "users.title",
defaultMessage: "Users",
})}
</h3>
</div>
<Table
columns={cols}
data={data.items}
sortBy={data.sort[0].field}
pagination={{
limit: data.limit,
offset: data.offset,
total: data.total,
onSetOffset: (num: number) => {
if (offset !== num) {
setOffset(num);
}
},
}}
/>
</div>
</div>
</Root>
);
</Root>
);
}
if (typeof data.total !== "undefined") {
return <p>No items!</p>;
}
return <SuspenseLoader />;
}
export default Users;