mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 03:30:05 +00:00
Certificate Authority work
This commit is contained in:
@@ -7,7 +7,11 @@
|
|||||||
"created_on",
|
"created_on",
|
||||||
"modified_on",
|
"modified_on",
|
||||||
"name",
|
"name",
|
||||||
"acme2_url"
|
"acmesh_server",
|
||||||
|
"ca_bundle",
|
||||||
|
"max_domains",
|
||||||
|
"is_wildcard_supported",
|
||||||
|
"is_setup"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -27,10 +31,25 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"maxLength": 100
|
"maxLength": 100
|
||||||
},
|
},
|
||||||
"acme2_url": {
|
"acmesh_server": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 8,
|
"minLength": 2,
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"ca_bundle": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 0,
|
||||||
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"max_domains": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"is_wildcard_supported": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"is_setup": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -49,6 +49,9 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0
|
"minimum": 0
|
||||||
},
|
},
|
||||||
|
"certificate_authority": {
|
||||||
|
"$ref": "#/components/schemas/CertificateAuthorityObject"
|
||||||
|
},
|
||||||
"dns_provider_id": {
|
"dns_provider_id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0
|
"minimum": 0
|
||||||
|
@@ -37,10 +37,14 @@
|
|||||||
"value": {
|
"value": {
|
||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created_on": 1602588511,
|
"created_on": 1627531400,
|
||||||
"modified_on": 1602588511,
|
"modified_on": 1627531400,
|
||||||
"name": "Let's Encrypt",
|
"name": "ZeroSSL",
|
||||||
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
|
"acmesh_server": "zerossl",
|
||||||
|
"ca_bundle": "",
|
||||||
|
"max_domains": 10,
|
||||||
|
"is_wildcard_supported": true,
|
||||||
|
"is_setup": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,10 +46,14 @@
|
|||||||
"value": {
|
"value": {
|
||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created_on": 1602588511,
|
"created_on": 1627531400,
|
||||||
"modified_on": 1602588511,
|
"modified_on": 1627531400,
|
||||||
"name": "Let's Encrypt",
|
"name": "ZeroSSL",
|
||||||
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
|
"acmesh_server": "zerossl",
|
||||||
|
"ca_bundle": "",
|
||||||
|
"max_domains": 10,
|
||||||
|
"is_wildcard_supported": true,
|
||||||
|
"is_setup": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -64,17 +64,25 @@
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created_on": 1602588511,
|
"created_on": 1627531400,
|
||||||
"modified_on": 1602588511,
|
"modified_on": 1627531400,
|
||||||
"name": "Let's Encrypt",
|
"name": "ZeroSSL",
|
||||||
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
|
"acmesh_server": "zerossl",
|
||||||
|
"ca_bundle": "",
|
||||||
|
"max_domains": 10,
|
||||||
|
"is_wildcard_supported": true,
|
||||||
|
"is_setup": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"created_on": 1602588511,
|
"created_on": 1627531400,
|
||||||
"modified_on": 1602588511,
|
"modified_on": 1627531400,
|
||||||
"name": "Let's Encrypt (Staging)",
|
"name": "Let's Encrypt",
|
||||||
"acme2_url": "https://acme-staging-v02.api.letsencrypt.org/directory"
|
"acmesh_server": "https://acme-v02.api.letsencrypt.org/directory",
|
||||||
|
"ca_bundle": "",
|
||||||
|
"max_domains": 10,
|
||||||
|
"is_wildcard_supported": true,
|
||||||
|
"is_setup": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -32,11 +32,15 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"value": {
|
"value": {
|
||||||
"result": {
|
"result": {
|
||||||
"id": 3,
|
"id": 1,
|
||||||
"created_on": 1602588900,
|
"created_on": 1627531400,
|
||||||
"modified_on": 1602588900,
|
"modified_on": 1627531400,
|
||||||
"name": "Boulder",
|
"name": "ZeroSSL",
|
||||||
"acme2_url": "https://boulder.local/directory"
|
"acmesh_server": "zerossl",
|
||||||
|
"ca_bundle": "",
|
||||||
|
"max_domains": 10,
|
||||||
|
"is_wildcard_supported": true,
|
||||||
|
"is_setup": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -56,7 +56,11 @@ CREATE TABLE IF NOT EXISTS `certificate_authority`
|
|||||||
created_on INTEGER NOT NULL DEFAULT 0,
|
created_on INTEGER NOT NULL DEFAULT 0,
|
||||||
modified_on INTEGER NOT NULL DEFAULT 0,
|
modified_on INTEGER NOT NULL DEFAULT 0,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
acme2_url TEXT NOT NULL,
|
acmesh_server TEXT NOT NULL DEFAULT "",
|
||||||
|
is_setup INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ca_bundle TEXT NOT NULL DEFAULT "",
|
||||||
|
is_wildcard_supported INTEGER NOT NULL DEFAULT 0, -- specific to each CA, acme v1 doesn't usually have wildcards
|
||||||
|
max_domains INTEGER NOT NULL DEFAULT 5, -- per request
|
||||||
is_deleted INTEGER NOT NULL DEFAULT 0
|
is_deleted INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -36,20 +36,51 @@ INSERT INTO `certificate_authority` (
|
|||||||
created_on,
|
created_on,
|
||||||
modified_on,
|
modified_on,
|
||||||
name,
|
name,
|
||||||
acme2_url
|
acmesh_server,
|
||||||
|
is_wildcard_supported,
|
||||||
|
max_domains
|
||||||
) VALUES (
|
) VALUES (
|
||||||
strftime('%s', 'now'),
|
strftime('%s', 'now'),
|
||||||
strftime('%s', 'now'),
|
strftime('%s', 'now'),
|
||||||
"Let's Encrypt",
|
"ZeroSSL",
|
||||||
"https://acme-v02.api.letsencrypt.org/directory"
|
"zerossl",
|
||||||
|
1,
|
||||||
|
10
|
||||||
), (
|
), (
|
||||||
strftime('%s', 'now'),
|
strftime('%s', 'now'),
|
||||||
strftime('%s', 'now'),
|
strftime('%s', 'now'),
|
||||||
"Let's Encrypt (Staging)",
|
"Let's Encrypt",
|
||||||
"https://acme-staging-v02.api.letsencrypt.org/directory"
|
"https://acme-v02.api.letsencrypt.org/directory",
|
||||||
|
1,
|
||||||
|
10
|
||||||
|
), (
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
"Buypass Go SSL",
|
||||||
|
"https://api.buypass.com/acme/directory",
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
), (
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
"Let's Encrypt (Testing)",
|
||||||
|
"https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||||
|
1,
|
||||||
|
10
|
||||||
|
), (
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
"Buypass Go SSL (Testing)",
|
||||||
|
"https://api.test4.buypass.no/acme/directory",
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
), (
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
strftime('%s', 'now'),
|
||||||
|
"SSL.com",
|
||||||
|
"ssl.com",
|
||||||
|
0,
|
||||||
|
10
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- migrate:down
|
-- migrate:down
|
||||||
|
|
||||||
-- Not allowed to go down from initial
|
|
||||||
|
@@ -17,7 +17,7 @@ var acmeShFile string
|
|||||||
|
|
||||||
// GetAcmeShVersion will return the acme.sh script version
|
// GetAcmeShVersion will return the acme.sh script version
|
||||||
func GetAcmeShVersion() string {
|
func GetAcmeShVersion() string {
|
||||||
if r, err := acmeShExec("--version"); err == nil {
|
if r, err := shExec("--version"); err == nil {
|
||||||
// modify the output
|
// modify the output
|
||||||
r = strings.Trim(r, "\n")
|
r = strings.Trim(r, "\n")
|
||||||
v := strings.Split(r, "\n")
|
v := strings.Split(r, "\n")
|
||||||
@@ -26,13 +26,15 @@ func GetAcmeShVersion() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func acmeShExec(args ...string) (string, error) {
|
// shExec executes the acme.sh with arguments
|
||||||
|
func shExec(args ...string) (string, error) {
|
||||||
if _, err := os.Stat(acmeShFile); os.IsNotExist(err) {
|
if _, err := os.Stat(acmeShFile); os.IsNotExist(err) {
|
||||||
e := fmt.Errorf("%s does not exist", acmeShFile)
|
e := fmt.Errorf("%s does not exist", acmeShFile)
|
||||||
logger.Error("AcmeShError", e)
|
logger.Error("AcmeShError", e)
|
||||||
return "", e
|
return "", e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug("CMD: %s %v", acmeShFile, args)
|
||||||
// nolint: gosec
|
// nolint: gosec
|
||||||
c := exec.Command(acmeShFile, args...)
|
c := exec.Command(acmeShFile, args...)
|
||||||
b, e := c.Output()
|
b, e := c.Output()
|
||||||
@@ -61,3 +63,33 @@ func WriteAcmeSh() {
|
|||||||
logger.Info("Wrote %s", acmeShFile)
|
logger.Info("Wrote %s", acmeShFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestCert does all the heavy lifting
|
||||||
|
func RequestCert(domains []string, method string) error {
|
||||||
|
args := []string{"--issue"}
|
||||||
|
|
||||||
|
webroot := "/home/wwwroot/example.com"
|
||||||
|
|
||||||
|
// Add domains to args
|
||||||
|
for _, domain := range domains {
|
||||||
|
args = append(args, "-d", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
// case "dns":
|
||||||
|
case "http":
|
||||||
|
args = append(args, "-w", webroot)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("RequestCert method not supported: %s", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := shExec(args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("ret: %+v", ret)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -10,12 +10,14 @@ func CreateCertificateAuthority() string {
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"acme2_url"
|
"acmesh_server",
|
||||||
|
"max_domains"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": %s,
|
"name": %s,
|
||||||
"acme2_url": %s
|
"acmesh_server": %s,
|
||||||
|
"max_domains": %s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, stringMinMax(1, 100), stringMinMax(8, 255))
|
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne)
|
||||||
}
|
}
|
||||||
|
@@ -10,8 +10,9 @@ func UpdateCertificateAuthority() string {
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": %s,
|
"name": %s,
|
||||||
"acme2_url": %s
|
"acmesh_server": %s,
|
||||||
|
"max_domains": %s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, stringMinMax(1, 100), stringMinMax(8, 255))
|
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne)
|
||||||
}
|
}
|
||||||
|
@@ -152,13 +152,11 @@ func GetByStatus(status string) ([]Model, error) {
|
|||||||
SELECT
|
SELECT
|
||||||
t.*
|
t.*
|
||||||
FROM "%s" t
|
FROM "%s" t
|
||||||
INNER JOIN "dns_provider" d ON d."id" = t."dns_provider_id"
|
|
||||||
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
|
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
|
||||||
WHERE
|
WHERE
|
||||||
t."type" IN ("http", "dns") AND
|
t."type" IN ("http", "dns") AND
|
||||||
t."status" = ? AND
|
t."status" = ? AND
|
||||||
t."certificate_authority_id" > 0 AND
|
t."certificate_authority_id" > 0 AND
|
||||||
t."dns_provider_id" > 0 AND
|
|
||||||
t."is_deleted" = 0
|
t."is_deleted" = 0
|
||||||
`, tableName)
|
`, tableName)
|
||||||
|
|
||||||
|
@@ -2,11 +2,14 @@ package certificate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"npm/internal/acme"
|
||||||
"npm/internal/database"
|
"npm/internal/database"
|
||||||
"npm/internal/entity/certificateauthority"
|
"npm/internal/entity/certificateauthority"
|
||||||
"npm/internal/entity/dnsprovider"
|
"npm/internal/entity/dnsprovider"
|
||||||
|
"npm/internal/logger"
|
||||||
"npm/internal/types"
|
"npm/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,6 +89,10 @@ func (m *Model) Save() error {
|
|||||||
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
|
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !m.ValidateWildcardSupport() {
|
||||||
|
return fmt.Errorf("Cannot use Wildcard domains with this CA")
|
||||||
|
}
|
||||||
|
|
||||||
m.setDefaultStatus()
|
m.setDefaultStatus()
|
||||||
|
|
||||||
if m.ID == 0 {
|
if m.ID == 0 {
|
||||||
@@ -129,6 +136,32 @@ func (m *Model) Validate() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateWildcardSupport will ensure that the CA given supports wildcards,
|
||||||
|
// only if the domains on this object have at least 1 wildcard
|
||||||
|
func (m *Model) ValidateWildcardSupport() bool {
|
||||||
|
domains, err := m.DomainNames.AsStringArray()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("ValidateWildcardSupportError", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWildcard := false
|
||||||
|
for _, domain := range domains {
|
||||||
|
if strings.Contains(domain, "*") {
|
||||||
|
hasWildcard = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasWildcard {
|
||||||
|
m.Expand()
|
||||||
|
if !m.CertificateAuthority.IsWildcardSupported {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) setDefaultStatus() {
|
func (m *Model) setDefaultStatus() {
|
||||||
if m.ID == 0 {
|
if m.ID == 0 {
|
||||||
// It's a new certificate
|
// It's a new certificate
|
||||||
@@ -154,23 +187,33 @@ func (m *Model) Expand() {
|
|||||||
|
|
||||||
// Request makes a certificate request
|
// Request makes a certificate request
|
||||||
func (m *Model) Request() error {
|
func (m *Model) Request() error {
|
||||||
|
logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name)
|
||||||
|
|
||||||
m.Expand()
|
m.Expand()
|
||||||
m.Status = StatusRequesting
|
m.Status = StatusRequesting
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If error
|
// do request
|
||||||
|
domains, err := m.DomainNames.AsStringArray()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = acme.RequestCert(domains, m.Type)
|
||||||
|
if err != nil {
|
||||||
m.Status = StatusFailed
|
m.Status = StatusFailed
|
||||||
m.ErrorMessage = "something"
|
m.ErrorMessage = err.Error()
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If done
|
// If done
|
||||||
m.Status = StatusProvided
|
m.Status = StatusProvided
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
m.ExpiresOn.Time = &t
|
m.ExpiresOn.Time = &t // todo
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -33,13 +33,21 @@ func Create(ca *Model) (int, error) {
|
|||||||
created_on,
|
created_on,
|
||||||
modified_on,
|
modified_on,
|
||||||
name,
|
name,
|
||||||
acme2_url,
|
acmesh_server,
|
||||||
|
ca_bundle,
|
||||||
|
max_domains,
|
||||||
|
is_wildcard_supported,
|
||||||
|
is_setup,
|
||||||
is_deleted
|
is_deleted
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:created_on,
|
:created_on,
|
||||||
:modified_on,
|
:modified_on,
|
||||||
:name,
|
:name,
|
||||||
:acme2_url,
|
:acmesh_server,
|
||||||
|
:ca_bundle,
|
||||||
|
:max_domains,
|
||||||
|
:is_wildcard_supported,
|
||||||
|
:is_setup,
|
||||||
:is_deleted
|
:is_deleted
|
||||||
)`, ca)
|
)`, ca)
|
||||||
|
|
||||||
@@ -69,7 +77,11 @@ func Update(ca *Model) error {
|
|||||||
created_on = :created_on,
|
created_on = :created_on,
|
||||||
modified_on = :modified_on,
|
modified_on = :modified_on,
|
||||||
name = :name,
|
name = :name,
|
||||||
acme2_url = :acme2_url,
|
acmesh_server = :acmesh_server,
|
||||||
|
ca_bundle = :ca_bundle,
|
||||||
|
max_domains = :max_domains,
|
||||||
|
is_wildcard_supported = :is_wildcard_supported,
|
||||||
|
is_setup = :is_setup,
|
||||||
is_deleted = :is_deleted
|
is_deleted = :is_deleted
|
||||||
WHERE id = :id`, ca)
|
WHERE id = :id`, ca)
|
||||||
|
|
||||||
|
@@ -18,7 +18,11 @@ type Model struct {
|
|||||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||||
Name string `json:"name" db:"name" filter:"name,string"`
|
Name string `json:"name" db:"name" filter:"name,string"`
|
||||||
Acme2URL string `json:"acme2_url" db:"acme2_url" filter:"acme2_url,string"`
|
AcmeshServer string `json:"acmesh_server" db:"acmesh_server" filter:"acmesh_server,string"`
|
||||||
|
CABundle string `json:"ca_bundle" db:"ca_bundle" filter:"ca_bundle,string"`
|
||||||
|
MaxDomains int `json:"max_domains" db:"max_domains" filter:"max_domains,integer"`
|
||||||
|
IsWildcardSupported bool `json:"is_wildcard_supported" db:"is_wildcard_supported" filter:"is_wildcard_supported,boolean"`
|
||||||
|
IsSetup bool `json:"is_setup" db:"is_setup" filter:"is_setup,boolean"`
|
||||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,3 +56,16 @@ func (j *JSONB) UnmarshalJSON(data []byte) error {
|
|||||||
func (j JSONB) MarshalJSON() ([]byte, error) {
|
func (j JSONB) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(j.Decoded)
|
return json.Marshal(j.Decoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsStringArray will attempt to return as []string
|
||||||
|
func (j JSONB) AsStringArray() ([]string, error) {
|
||||||
|
var strs []string
|
||||||
|
|
||||||
|
// Encode then Decode onto this type
|
||||||
|
b, _ := j.MarshalJSON()
|
||||||
|
if err := json.Unmarshal(b, &strs); err != nil {
|
||||||
|
return strs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strs, nil
|
||||||
|
}
|
||||||
|
@@ -41,12 +41,14 @@ mainLoop:
|
|||||||
break mainLoop
|
break mainLoop
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
// Can confirm that this will wait for completion before the next loop
|
||||||
requestCertificates()
|
requestCertificates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestCertificates() {
|
func requestCertificates() {
|
||||||
|
// logger.Debug("requestCertificates fired")
|
||||||
rows, err := certificate.GetByStatus(certificate.StatusReady)
|
rows, err := certificate.GetByStatus(certificate.StatusReady)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("requestCertificatesError", err)
|
logger.Error("requestCertificatesError", err)
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
FROM nginxproxymanager/testca as testca
|
||||||
FROM jc21/nginx-full:github-no-acme-golang
|
FROM jc21/nginx-full:github-no-acme-golang
|
||||||
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
|
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
|
||||||
|
|
||||||
@@ -38,6 +39,8 @@ RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/
|
|||||||
# Fix for golang dev:
|
# Fix for golang dev:
|
||||||
RUN chown -R 1000:1000 /opt/go
|
RUN chown -R 1000:1000 /opt/go
|
||||||
|
|
||||||
|
COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD [ "/init" ]
|
CMD [ "/init" ]
|
||||||
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://127.0.0.1:81/api || exit 1
|
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://127.0.0.1:81/api || exit 1
|
||||||
|
@@ -24,8 +24,15 @@ services:
|
|||||||
- ../data:/data
|
- ../data:/data
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
|
||||||
|
stepca:
|
||||||
|
image: nginxproxymanager/testca
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- ca.internal
|
||||||
|
|
||||||
swagger:
|
swagger:
|
||||||
image: 'swaggerapi/swagger-ui:latest'
|
image: swaggerapi/swagger-ui:latest
|
||||||
ports:
|
ports:
|
||||||
- 3001:80
|
- 3001:80
|
||||||
environment:
|
environment:
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as api from "./base";
|
import * as api from "./base";
|
||||||
import { UserResponse } from "./responseTypes";
|
import { User } from "./models";
|
||||||
|
|
||||||
interface AuthOptions {
|
interface AuthOptions {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -20,7 +20,7 @@ interface Options {
|
|||||||
export async function createUser(
|
export async function createUser(
|
||||||
{ payload }: Options,
|
{ payload }: Options,
|
||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
): Promise<UserResponse> {
|
): Promise<User> {
|
||||||
const { result } = await api.post(
|
const { result } = await api.post(
|
||||||
{
|
{
|
||||||
url: "/users",
|
url: "/users",
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
import * as api from "./base";
|
import * as api from "./base";
|
||||||
import { UserResponse } from "./responseTypes";
|
import { User } from "./models";
|
||||||
|
|
||||||
export async function getUser(
|
export async function getUser(id: number | string = "me"): Promise<User> {
|
||||||
id: number | string = "me",
|
|
||||||
): Promise<UserResponse> {
|
|
||||||
const userId = id ? id : "me";
|
const userId = id ? id : "me";
|
||||||
const { result } = await api.get({
|
const { result } = await api.get({
|
||||||
url: `/users/${userId}`,
|
url: `/users/${userId}`,
|
||||||
|
@@ -3,6 +3,9 @@ export * from "./getToken";
|
|||||||
export * from "./getUser";
|
export * from "./getUser";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./refreshToken";
|
export * from "./refreshToken";
|
||||||
|
export * from "./requestCertificateAuthorities";
|
||||||
|
export * from "./requestCertificates";
|
||||||
export * from "./requestHealth";
|
export * from "./requestHealth";
|
||||||
export * from "./requestSettings";
|
export * from "./requestSettings";
|
||||||
|
export * from "./requestUsers";
|
||||||
export * from "./responseTypes";
|
export * from "./responseTypes";
|
||||||
|
@@ -3,6 +3,27 @@ export interface Sort {
|
|||||||
direction: "ASC" | "DESC";
|
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 {
|
export interface Setting {
|
||||||
id: number;
|
id: number;
|
||||||
createdOn: number;
|
createdOn: number;
|
||||||
@@ -10,3 +31,27 @@ export interface Setting {
|
|||||||
name: string;
|
name: string;
|
||||||
value: any;
|
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;
|
||||||
|
}
|
||||||
|
16
frontend/src/api/npm/requestCertificateAuthorities.ts
Normal file
16
frontend/src/api/npm/requestCertificateAuthorities.ts
Normal 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;
|
||||||
|
}
|
16
frontend/src/api/npm/requestCertificates.ts
Normal file
16
frontend/src/api/npm/requestCertificates.ts
Normal 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;
|
||||||
|
}
|
16
frontend/src/api/npm/requestUsers.ts
Normal file
16
frontend/src/api/npm/requestUsers.ts
Normal 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;
|
||||||
|
}
|
@@ -1,4 +1,10 @@
|
|||||||
import { Sort, Setting } from "./models";
|
import {
|
||||||
|
Certificate,
|
||||||
|
CertificateAuthority,
|
||||||
|
Setting,
|
||||||
|
Sort,
|
||||||
|
User,
|
||||||
|
} from "./models";
|
||||||
|
|
||||||
export interface HealthResponse {
|
export interface HealthResponse {
|
||||||
commit: string;
|
commit: string;
|
||||||
@@ -8,32 +14,11 @@ export interface HealthResponse {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAuthResponse {
|
|
||||||
id: number;
|
|
||||||
userId: number;
|
|
||||||
type: string;
|
|
||||||
createdOn: number;
|
|
||||||
updatedOn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TokenResponse {
|
export interface TokenResponse {
|
||||||
expires: number;
|
expires: number;
|
||||||
token: string;
|
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 {
|
export interface SettingsResponse {
|
||||||
total: number;
|
total: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
@@ -41,3 +26,27 @@ export interface SettingsResponse {
|
|||||||
sort: Sort[];
|
sort: Sort[];
|
||||||
items: Setting[];
|
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[];
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Navigation } from "components";
|
import { Dropdown, Navigation } from "components";
|
||||||
import { intl } from "locale";
|
import { intl } from "locale";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Book,
|
Book,
|
||||||
DeviceDesktop,
|
DeviceDesktop,
|
||||||
@@ -43,20 +44,30 @@ function NavMenu() {
|
|||||||
to: "/access-lists",
|
to: "/access-lists",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({
|
title: "SSL",
|
||||||
|
icon: <Shield />,
|
||||||
|
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",
|
id: "certificates.title",
|
||||||
defaultMessage: "Certificates",
|
defaultMessage: "Certificates",
|
||||||
}),
|
})}
|
||||||
icon: <Shield />,
|
</span>
|
||||||
to: "/certificates",
|
</Link>
|
||||||
},
|
</Dropdown.Item>,
|
||||||
{
|
<Dropdown.Item key="ssl-authorities">
|
||||||
title: intl.formatMessage({
|
<Link to="/ssl/authorities" role="button" aria-expanded="false">
|
||||||
id: "users.title",
|
<span className="nav-link-title">
|
||||||
defaultMessage: "Users",
|
{intl.formatMessage({
|
||||||
}),
|
id: "cert_authorities.title",
|
||||||
icon: <Users />,
|
defaultMessage: "Certificate Authorities",
|
||||||
to: "/users",
|
})}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Dropdown.Item>,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({
|
title: intl.formatMessage({
|
||||||
@@ -66,6 +77,14 @@ function NavMenu() {
|
|||||||
icon: <Book />,
|
icon: <Book />,
|
||||||
to: "/audit-log",
|
to: "/audit-log",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: "users.title",
|
||||||
|
defaultMessage: "Users",
|
||||||
|
}),
|
||||||
|
icon: <Users />,
|
||||||
|
to: "/users",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({
|
title: intl.formatMessage({
|
||||||
id: "settings.title",
|
id: "settings.title",
|
||||||
|
@@ -12,6 +12,9 @@ import { BrowserRouter, Switch, Route } from "react-router-dom";
|
|||||||
const AccessLists = lazy(() => import("pages/AccessLists"));
|
const AccessLists = lazy(() => import("pages/AccessLists"));
|
||||||
const AuditLog = lazy(() => import("pages/AuditLog"));
|
const AuditLog = lazy(() => import("pages/AuditLog"));
|
||||||
const Certificates = lazy(() => import("pages/Certificates"));
|
const Certificates = lazy(() => import("pages/Certificates"));
|
||||||
|
const CertificateAuthorities = lazy(
|
||||||
|
() => import("pages/CertificateAuthorities"),
|
||||||
|
);
|
||||||
const Dashboard = lazy(() => import("pages/Dashboard"));
|
const Dashboard = lazy(() => import("pages/Dashboard"));
|
||||||
const Hosts = lazy(() => import("pages/Hosts"));
|
const Hosts = lazy(() => import("pages/Hosts"));
|
||||||
const Login = lazy(() => import("pages/Login"));
|
const Login = lazy(() => import("pages/Login"));
|
||||||
@@ -54,9 +57,12 @@ function Router() {
|
|||||||
<Route path="/hosts">
|
<Route path="/hosts">
|
||||||
<Hosts />
|
<Hosts />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/certificates">
|
<Route path="/ssl/certificates">
|
||||||
<Certificates />
|
<Certificates />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/ssl/authorities">
|
||||||
|
<CertificateAuthorities />
|
||||||
|
</Route>
|
||||||
<Route path="/audit-log">
|
<Route path="/audit-log">
|
||||||
<AuditLog />
|
<AuditLog />
|
||||||
</Route>
|
</Route>
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
|
import { Badge } from "components";
|
||||||
|
import { intl } from "locale";
|
||||||
|
|
||||||
export interface TableColumn {
|
export interface TableColumn {
|
||||||
/**
|
/**
|
||||||
@@ -52,9 +54,33 @@ export const Table = ({ columns, data, pagination, sortBy }: TableProps) => {
|
|||||||
switch (given) {
|
switch (given) {
|
||||||
// Simple ID column has text-muted
|
// Simple ID column has text-muted
|
||||||
case "id":
|
case "id":
|
||||||
return (val: any) => {
|
return (val: number) => {
|
||||||
return <span className="text-muted">{val}</span>;
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { getUser, UserResponse } from "api/npm";
|
import { getUser, User } from "api/npm";
|
||||||
import { useAuthState } from "context";
|
import { useAuthState } from "context";
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
const initalValue = null;
|
const initalValue = null;
|
||||||
const UserContext = React.createContext<UserResponse | null>(initalValue);
|
const UserContext = React.createContext<User | null>(initalValue);
|
||||||
|
|
||||||
// Provider
|
// Provider
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
}
|
}
|
||||||
function UserProvider({ children }: Props) {
|
function UserProvider({ children }: Props) {
|
||||||
const [userData, setUserData] = useState<UserResponse>({
|
const [userData, setUserData] = useState<User>({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "",
|
name: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
|
@@ -5,6 +5,9 @@
|
|||||||
"auditlog.title": {
|
"auditlog.title": {
|
||||||
"defaultMessage": "Audit Log"
|
"defaultMessage": "Audit Log"
|
||||||
},
|
},
|
||||||
|
"cert_authorities.title": {
|
||||||
|
"defaultMessage": "Certificate Authorities"
|
||||||
|
},
|
||||||
"certificates.title": {
|
"certificates.title": {
|
||||||
"defaultMessage": "Certificates"
|
"defaultMessage": "Certificates"
|
||||||
},
|
},
|
||||||
@@ -14,12 +17,21 @@
|
|||||||
"column.id": {
|
"column.id": {
|
||||||
"defaultMessage": "ID"
|
"defaultMessage": "ID"
|
||||||
},
|
},
|
||||||
|
"column.max_domains": {
|
||||||
|
"defaultMessage": "Domains per Cert"
|
||||||
|
},
|
||||||
"column.name": {
|
"column.name": {
|
||||||
"defaultMessage": "Name"
|
"defaultMessage": "Name"
|
||||||
},
|
},
|
||||||
|
"column.status": {
|
||||||
|
"defaultMessage": "Status"
|
||||||
|
},
|
||||||
"dashboard.title": {
|
"dashboard.title": {
|
||||||
"defaultMessage": "Dashboard"
|
"defaultMessage": "Dashboard"
|
||||||
},
|
},
|
||||||
|
"column.wildcard_support": {
|
||||||
|
"defaultMessage": "Wildcard Support"
|
||||||
|
},
|
||||||
"footer.changelog": {
|
"footer.changelog": {
|
||||||
"defaultMessage": "Change Log"
|
"defaultMessage": "Change Log"
|
||||||
},
|
},
|
||||||
@@ -47,6 +59,12 @@
|
|||||||
"profile.title": {
|
"profile.title": {
|
||||||
"defaultMessage": "Profile settings"
|
"defaultMessage": "Profile settings"
|
||||||
},
|
},
|
||||||
|
"ready": {
|
||||||
|
"defaultMessage": "Ready"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"defaultMessage": "Required"
|
||||||
|
},
|
||||||
"settings.title": {
|
"settings.title": {
|
||||||
"defaultMessage": "Settings"
|
"defaultMessage": "Settings"
|
||||||
},
|
},
|
||||||
|
115
frontend/src/pages/CertificateAuthorities/index.tsx
Normal file
115
frontend/src/pages/CertificateAuthorities/index.tsx
Normal 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;
|
@@ -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 { intl } from "locale";
|
||||||
|
import { useInterval } from "rooks";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
@@ -8,11 +12,43 @@ const Root = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Certificates() {
|
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 (
|
return (
|
||||||
<Root>
|
<Root>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-status-top bg-cyan" />
|
<div className="card-status-top bg-yellow" />
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="card-title">
|
<h3 className="card-title">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
@@ -21,9 +57,31 @@ function Certificates() {
|
|||||||
})}
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Certificates;
|
if (typeof data.total !== "undefined") {
|
||||||
|
return <p>No items!</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SuspenseLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CertificateAuthorities;
|
||||||
|
@@ -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 { intl } from "locale";
|
||||||
|
import { useInterval } from "rooks";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
@@ -9,10 +13,46 @@ const Root = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function Users() {
|
function Users() {
|
||||||
|
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 (
|
return (
|
||||||
<Root>
|
<Root>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-status-top bg-cyan" />
|
<div className="card-status-top bg-indigo" />
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="card-title">
|
<h3 className="card-title">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
@@ -21,9 +61,31 @@ function Users() {
|
|||||||
})}
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</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;
|
export default Users;
|
||||||
|
Reference in New Issue
Block a user