Certificates Renewal + SSE

- Certificate renewal is just a re-request as it's forced already
- Rejig the routes for readability
- Added Server Side Events so that the UI would invalidate the
cache when changes happen on the backend, such as certs being
provided or failing
- Added a SSE Token, which has the same shelf life as normal token
but can't be used interchangeably. The reason for this is, the
SSE endpoint needs a token for auth as a Query param, so it would
be stored in log files. If someone where to get a hold of that,
it's pretty useless as it can't be used to change anything, only
to listen for events until it expires
- Added test endpoint for SSE testing only availabe in debug mode
This commit is contained in:
Jamie Curnow 2023-03-07 16:42:26 +10:00
parent 35550082bf
commit 215083f6cf
No known key found for this signature in database
GPG Key ID: FFBB624C43388E9E
29 changed files with 665 additions and 197 deletions

View File

@ -163,6 +163,11 @@
"$ref": "file://./paths/tokens/post.json"
}
},
"/tokens/sse": {
"post": {
"$ref": "file://./paths/tokens/sse/post.json"
}
},
"/upstreams": {
"get": {
"$ref": "file://./paths/upstreams/get.json"

View File

@ -1,21 +1,17 @@
{
"operationId": "refreshToken",
"summary": "Refresh your access token",
"tags": [
"Tokens"
],
"tags": ["Tokens"],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"required": ["result"],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
"$ref": "#/components/schemas/TokenObject"
}
}
},
@ -34,4 +30,4 @@
}
}
}
}
}

View File

@ -1,9 +1,7 @@
{
"operationId": "requestToken",
"summary": "Request a new access token from credentials",
"tags": [
"Tokens"
],
"tags": ["Tokens"],
"requestBody": {
"description": "Credentials Payload",
"required": true,
@ -19,12 +17,10 @@
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"required": ["result"],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
"$ref": "#/components/schemas/TokenObject"
}
}
},
@ -49,9 +45,7 @@
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"required": ["error"],
"properties": {
"result": {
"nullable": true
@ -76,4 +70,4 @@
}
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"operationId": "requestSSEToken",
"summary": "Request a new SSE token",
"tags": ["Tokens"],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": ["result"],
"properties": {
"result": {
"$ref": "#/components/schemas/TokenObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"expires": 1566540510,
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"scope": "user"
}
}
}
}
}
}
}
}
}

View File

@ -12,6 +12,7 @@ require (
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.2.1
github.com/go-chi/jwtauth v4.0.4+incompatible
github.com/jc21/go-sse v0.0.0-20230307041911-8ea9bdc44a58
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760
github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.16

View File

@ -27,6 +27,12 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/jc21/go-sse v0.0.0-20230307004720-e0a2266806a8 h1:3zrxixsRpzrMXd/c6vUHaoIi9OqirEPxPe4Ydxn3jNU=
github.com/jc21/go-sse v0.0.0-20230307004720-e0a2266806a8/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
github.com/jc21/go-sse v0.0.0-20230307015818-b2783ddda573 h1:aaRu9mFSjxNfbXWVe7MlarmuB0vcdTShXFbxjzHAseA=
github.com/jc21/go-sse v0.0.0-20230307015818-b2783ddda573/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
github.com/jc21/go-sse v0.0.0-20230307041911-8ea9bdc44a58 h1:WSD0YdEuFPZHIe8hkAjxoAEWZnzieAiLg3zw28EVf80=
github.com/jc21/go-sse v0.0.0-20230307041911-8ea9bdc44a58/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k=
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=

View File

@ -39,23 +39,11 @@ func GetCertificates() func(http.ResponseWriter, *http.Request) {
// Route: GET /certificates/{certificateID}
func GetCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := certificate.GetByID(certificateID)
switch err {
case sql.ErrNoRows:
h.NotFound(w, r)
case nil:
logger.Debug("here")
if item := getCertificateFromRequest(w, r); item != nil {
// nolint: errcheck,gosec
item.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, item)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
@ -64,27 +52,20 @@ func GetCertificate() func(http.ResponseWriter, *http.Request) {
// Route: POST /certificates
func CreateCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var item certificate.Model
if fillObjectFromBody(w, r, "", &item) {
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
item.UserID = userID
var newCertificate certificate.Model
err := json.Unmarshal(bodyBytes, &newCertificate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
if err := item.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
return
}
configureCertificate(item)
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newCertificate.UserID = userID
if err = newCertificate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
return
}
configureCertificate(newCertificate)
h.ResultResponseJSON(w, r, http.StatusOK, newCertificate)
}
}
@ -92,49 +73,19 @@ func CreateCertificate() func(http.ResponseWriter, *http.Request) {
// Route: PUT /certificates/{certificateID}
func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificateObject, err := certificate.GetByID(certificateID)
switch err {
case sql.ErrNoRows:
h.NotFound(w, r)
case nil:
if item := getCertificateFromRequest(w, r); item != nil {
// This is a special endpoint, as it needs to verify the schema payload
// based on the certificate type, without being given a type in the payload.
// The middleware would normally handle this.
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes)
if jsonErr != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
return
if fillObjectFromBody(w, r, schema.UpdateCertificate(item.Type), item) {
if err := item.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
// configureCertificate(*item, item.Request)
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
if len(schemaErrors) > 0 {
h.ResultSchemaErrorJSON(w, r, schemaErrors)
return
}
err := json.Unmarshal(bodyBytes, &certificateObject)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = certificateObject.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
configureCertificate(certificateObject)
h.ResultResponseJSON(w, r, http.StatusOK, certificateObject)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
@ -143,31 +94,87 @@ func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
// Route: DELETE /certificates/{certificateID}
func DeleteCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := certificate.GetByID(certificateID)
switch err {
case sql.ErrNoRows:
h.NotFound(w, r)
case nil:
// Ensure that this upstream isn't in use by a host
cnt := host.GetCertificateUseCount(certificateID)
if item := getCertificateFromRequest(w, r); item != nil {
cnt := host.GetCertificateUseCount(item.ID)
if cnt > 0 {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot delete certificate that is in use by at least 1 host", nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
// RenewCertificate is self explanatory
// Route: PUT /certificates/{certificateID}/renew
func RenewCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if item := getCertificateFromRequest(w, r); item != nil {
configureCertificate(*item)
h.ResultResponseJSON(w, r, http.StatusOK, true)
}
}
}
// DownloadCertificate is self explanatory
// Route: PUT /certificates/{certificateID}/download
func DownloadCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if item := getCertificateFromRequest(w, r); item != nil {
// todo
h.ResultResponseJSON(w, r, http.StatusOK, "ok")
}
}
}
// getCertificateFromRequest has some reusable code for all endpoints that
// have a certificate id in the url. it will write errors to the output.
func getCertificateFromRequest(w http.ResponseWriter, r *http.Request) *certificate.Model {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return nil
}
certificateObject, err := certificate.GetByID(certificateID)
switch err {
case sql.ErrNoRows:
h.NotFound(w, r)
case nil:
return &certificateObject
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
return nil
}
// getCertificateFromRequest has some reusable code for all endpoints that
// have a certificate id in the url. it will write errors to the output.
func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema string, o interface{}) bool {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
if validationSchema != "" {
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), validationSchema, bodyBytes)
if jsonErr != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
return false
}
if len(schemaErrors) > 0 {
h.ResultSchemaErrorJSON(w, r, schemaErrors)
return false
}
}
err := json.Unmarshal(bodyBytes, o)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return false
}
return true
}
func configureCertificate(c certificate.Model) {
err := jobqueue.AddJob(jobqueue.Job{
Name: "RequestCertificate",

View File

@ -0,0 +1,28 @@
package handler
import (
"encoding/json"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/serverevents"
)
// TestSSENotification specifically fires of a SSE message for testing purposes
// Route: POST /sse-notification
func TestSSENotification() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var msg serverevents.Message
err := json.Unmarshal(bodyBytes, &msg)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
serverevents.Send(msg, "")
h.ResultResponseJSON(w, r, http.StatusOK, true)
}
}

View File

@ -65,7 +65,7 @@ func NewToken() func(http.ResponseWriter, *http.Request) {
return
}
if response, err := njwt.Generate(&userObj); err != nil {
if response, err := njwt.Generate(&userObj, false); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
@ -80,7 +80,34 @@ func RefreshToken() func(http.ResponseWriter, *http.Request) {
// TODO: Use your own methods to verify an existing user is
// able to refresh their token and then give them a new one
userObj, _ := user.GetByEmail("jc@jc21.com")
if response, err := njwt.Generate(&userObj); err != nil {
if response, err := njwt.Generate(&userObj, false); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
}
// NewSSEToken will generate and return a very short lived token for
// use by the /sse/* endpoint. It requires an app token to generate this
// Route: POST /tokens/sse
func NewSSEToken() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(c.UserIDCtxKey).(int)
// Find user
userObj, userErr := user.GetByID(userID)
if userErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
if userObj.IsDisabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
return
}
if response, err := njwt.Generate(&userObj, true); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)

View File

@ -0,0 +1,34 @@
package middleware
import (
"net/http"
h "npm/internal/api/http"
"npm/internal/entity/user"
"github.com/go-chi/jwtauth"
)
// SSEAuth will validate that the jwt token provided to get this far is a SSE token
// and that the user is enabled
func SSEAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, claims, err := jwtauth.FromContext(ctx)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil)
return
}
userID := int(claims["uid"].(float64))
_, enabled := user.IsEnabled(userID)
if token == nil || !token.Valid || !enabled || !claims.VerifyIssuer("sse", true) {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
return
}
// Should be all good now
next.ServeHTTP(w, r)
})
}

View File

@ -19,6 +19,7 @@ import (
"npm/internal/entity/upstream"
"npm/internal/entity/user"
"npm/internal/logger"
"npm/internal/serverevents"
"github.com/go-chi/chi"
chiMiddleware "github.com/go-chi/chi/middleware"
@ -45,7 +46,6 @@ func NewRouter() http.Handler {
chiMiddleware.RealIP,
chiMiddleware.Recoverer,
chiMiddleware.Throttle(5),
chiMiddleware.Timeout(30*time.Second),
middleware.PrettyPrint,
middleware.Expansion,
middleware.DecodeAuth(),
@ -61,8 +61,14 @@ func applyRoutes(r chi.Router) chi.Router {
r.NotFound(handler.NotFound())
r.MethodNotAllowed(handler.NotAllowed())
// SSE - requires a sse token as the `jwt` get parameter
// Exists inside /api but it's here so that we can skip the Timeout middleware
// that applies to other endpoints.
r.With(middleware.EnforceSetup(true), middleware.SSEAuth).
Mount("/api/sse", serverevents.Get())
// API
r.Route("/api", func(r chi.Router) {
r.With(chiMiddleware.Timeout(30*time.Second)).Route("/api", func(r chi.Router) {
r.Get("/", handler.Health())
r.Get("/schema", handler.Schema())
r.With(middleware.EnforceSetup(true), middleware.Enforce("")).
@ -74,34 +80,54 @@ func applyRoutes(r chi.Router) chi.Router {
Post("/", handler.NewToken())
r.With(middleware.Enforce("")).
Get("/", handler.RefreshToken())
r.With(middleware.Enforce("")).
Post("/sse", handler.NewSSEToken())
})
// Users
r.Route("/users", func(r chi.Router) {
r.With(middleware.EnforceSetup(true), middleware.Enforce("")).Get("/{userID:(?:me)}", handler.GetUser())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).Get("/{userID:(?:[0-9]+)}", handler.GetUser())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).Delete("/{userID:(?:[0-9]+|me)}", handler.DeleteUser())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).With(middleware.Filters(user.GetFilterSchema())).
Get("/", handler.GetUsers())
r.With(middleware.EnforceRequestSchema(schema.CreateUser()), middleware.Enforce(user.CapabilityUsersManage)).
// Create - can be done in Setup stage as well
r.With(middleware.Enforce(user.CapabilityUsersManage), middleware.EnforceRequestSchema(schema.CreateUser())).
Post("/", handler.CreateUser())
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser()), middleware.Enforce("")).
Put("/{userID:(?:me)}", handler.UpdateUser())
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser()), middleware.Enforce(user.CapabilityUsersManage)).
Put("/{userID:(?:[0-9]+)}", handler.UpdateUser())
// Requires Setup stage to be completed
r.With(middleware.EnforceSetup(true)).Route("/", func(r chi.Router) {
// Get yourself, requires a login but no other permissions
r.With(middleware.Enforce("")).
Get("/{userID:(?:me)}", handler.GetUser())
// Auth
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth()), middleware.Enforce("")).
Post("/{userID:(?:me)}/auth", handler.SetAuth())
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth()), middleware.Enforce(user.CapabilityUsersManage)).
Post("/{userID:(?:[0-9]+)}/auth", handler.SetAuth())
// Update yourself, requires a login but no other permissions
r.With(middleware.Enforce(""), middleware.EnforceRequestSchema(schema.UpdateUser())).
Put("/{userID:(?:me)}", handler.UpdateUser())
r.With(middleware.Enforce(user.CapabilityUsersManage)).Route("/", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityUsersManage), middleware.Filters(user.GetFilterSchema())).
Get("/", handler.GetUsers())
// Specific Item
r.Get("/{userID:(?:[0-9]+)}", handler.GetUser())
r.Delete("/{userID:(?:[0-9]+|me)}", handler.DeleteUser())
// Update another user
r.With(middleware.EnforceRequestSchema(schema.UpdateUser())).
Put("/{userID:(?:[0-9]+)}", handler.UpdateUser())
})
// Auth - sets passwords
r.With(middleware.Enforce(""), middleware.EnforceRequestSchema(schema.SetAuth())).
Post("/{userID:(?:me)}/auth", handler.SetAuth())
r.With(middleware.Enforce(user.CapabilityUsersManage), middleware.EnforceRequestSchema(schema.SetAuth())).
Post("/{userID:(?:[0-9]+)}/auth", handler.SetAuth())
})
})
// Only available in debug mode: delete users without auth
// Only available in debug mode
if config.GetLogLevel() == logger.DebugLevel {
// delete users without auth
r.Delete("/users", handler.DeleteUsers())
// SSE test endpoints
r.Post("/sse-notification", handler.TestSSENotification())
}
// Settings
@ -117,27 +143,48 @@ func applyRoutes(r chi.Router) chi.Router {
// Access Lists
r.With(middleware.EnforceSetup(true)).Route("/access-lists", func(r chi.Router) {
// List
r.With(middleware.Filters(accesslist.GetFilterSchema()), middleware.Enforce(user.CapabilityAccessListsView)).
Get("/", handler.GetAccessLists())
r.With(middleware.Enforce(user.CapabilityAccessListsView)).Get("/{accessListID:[0-9]+}", handler.GetAccessList())
r.With(middleware.Enforce(user.CapabilityAccessListsManage)).Delete("/{accessListID:[0-9]+}", handler.DeleteAccessList())
r.With(middleware.Enforce(user.CapabilityAccessListsManage)).With(middleware.EnforceRequestSchema(schema.CreateAccessList())).
// Create
r.With(middleware.Enforce(user.CapabilityAccessListsManage), middleware.EnforceRequestSchema(schema.CreateAccessList())).
Post("/", handler.CreateAccessList())
r.With(middleware.Enforce(user.CapabilityAccessListsManage)).With(middleware.EnforceRequestSchema(schema.UpdateAccessList())).
Put("/{accessListID:[0-9]+}", handler.UpdateAccessList())
// Specific Item
r.Route("/{accessListID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityAccessListsView)).
Get("/", handler.GetAccessList())
r.With(middleware.Enforce(user.CapabilityAccessListsManage)).Route("/", func(r chi.Router) {
r.Delete("/{accessListID:[0-9]+}", handler.DeleteAccessList())
r.With(middleware.EnforceRequestSchema(schema.UpdateAccessList())).
Put("/{accessListID:[0-9]+}", handler.UpdateAccessList())
})
})
})
// DNS Providers
r.With(middleware.EnforceSetup(true)).Route("/dns-providers", func(r chi.Router) {
r.With(middleware.Filters(dnsprovider.GetFilterSchema()), middleware.Enforce(user.CapabilityDNSProvidersView)).
// List
r.With(middleware.Enforce(user.CapabilityDNSProvidersView), middleware.Filters(dnsprovider.GetFilterSchema())).
Get("/", handler.GetDNSProviders())
r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).Get("/{providerID:[0-9]+}", handler.GetDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).Delete("/{providerID:[0-9]+}", handler.DeleteDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).With(middleware.EnforceRequestSchema(schema.CreateDNSProvider())).
Post("/", handler.CreateDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())).
Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider())
// Create
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage), middleware.EnforceRequestSchema(schema.CreateDNSProvider())).
Post("/", handler.CreateDNSProvider())
// Specific Item
r.Route("/{providerID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).
Get("/{providerID:[0-9]+}", handler.GetDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).Route("/", func(r chi.Router) {
r.Delete("/", handler.DeleteDNSProvider())
r.With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())).
Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider())
})
})
// List Acme DNS Providers
r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).Route("/acmesh", func(r chi.Router) {
r.Get("/{acmeshID:[a-z0-9_]+}", handler.GetAcmeshProvider())
r.Get("/", handler.GetAcmeshProviders())
@ -146,81 +193,141 @@ func applyRoutes(r chi.Router) chi.Router {
// Certificate Authorities
r.With(middleware.EnforceSetup(true)).Route("/certificate-authorities", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView), middleware.Filters(certificateauthority.GetFilterSchema())).
Get("/", handler.GetCertificateAuthorities())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView)).Get("/{caID:[0-9]+}", handler.GetCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())).
// Create
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage), middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())).
Post("/", handler.CreateCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())).
Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority())
// Specific Item
r.Route("/{caID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView)).
Get("/", handler.GetCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).Route("/", func(r chi.Router) {
r.Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority())
r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())).
Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority())
})
})
})
// Certificates
r.With(middleware.EnforceSetup(true)).Route("/certificates", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityCertificatesView), middleware.Filters(certificate.GetFilterSchema())).
Get("/", handler.GetCertificates())
r.With(middleware.Enforce(user.CapabilityCertificatesView)).Get("/{certificateID:[0-9]+}", handler.GetCertificate())
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Delete("/{certificateID:[0-9]+}", handler.DeleteCertificate())
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).With(middleware.EnforceRequestSchema(schema.CreateCertificate())).
// Create
r.With(middleware.Enforce(user.CapabilityCertificatesManage), middleware.EnforceRequestSchema(schema.CreateCertificate())).
Post("/", handler.CreateCertificate())
/*
r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())).
Put("/{certificateID:[0-9]+}", handler.UpdateCertificate())
*/
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Put("/{certificateID:[0-9]+}", handler.UpdateCertificate())
// Specific Item
r.Route("/{certificateID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityCertificatesView)).
Get("/", handler.GetCertificate())
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Route("/", func(r chi.Router) {
r.Delete("/", handler.DeleteCertificate())
r.Put("/", handler.UpdateCertificate())
// r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())).
// Put("/", handler.UpdateCertificate())
r.Post("/renew", handler.RenewCertificate())
r.Get("/download", handler.DownloadCertificate())
})
})
})
// Hosts
r.With(middleware.EnforceSetup(true)).Route("/hosts", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityHostsView), middleware.Filters(host.GetFilterSchema())).
Get("/", handler.GetHosts())
r.With(middleware.Enforce(user.CapabilityHostsView)).Get("/{hostID:[0-9]+}", handler.GetHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Delete("/{hostID:[0-9]+}", handler.DeleteHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.CreateHost())).
// Create
r.With(middleware.Enforce(user.CapabilityHostsManage), middleware.EnforceRequestSchema(schema.CreateHost())).
Post("/", handler.CreateHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.UpdateHost())).
Put("/{hostID:[0-9]+}", handler.UpdateHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Get("/{hostID:[0-9]+}/nginx-config", handler.GetHostNginxConfig("json"))
r.With(middleware.Enforce(user.CapabilityHostsManage)).Get("/{hostID:[0-9]+}/nginx-config.txt", handler.GetHostNginxConfig("text"))
// Specific Item
r.Route("/{hostID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityHostsView)).
Get("/", handler.GetHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) {
r.Delete("/", handler.DeleteHost())
r.With(middleware.EnforceRequestSchema(schema.UpdateHost())).
Put("/", handler.UpdateHost())
r.Get("/nginx-config", handler.GetHostNginxConfig("json"))
r.Get("/nginx-config.txt", handler.GetHostNginxConfig("text"))
})
})
})
// Nginx Templates
r.With(middleware.EnforceSetup(true)).Route("/nginx-templates", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityNginxTemplatesView), middleware.Filters(nginxtemplate.GetFilterSchema())).
Get("/", handler.GetNginxTemplates())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetNginxTemplates())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteNginxTemplate())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateNginxTemplate())).
// Create
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage), middleware.EnforceRequestSchema(schema.CreateNginxTemplate())).
Post("/", handler.CreateNginxTemplate())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateNginxTemplate())).
Put("/{templateID:[0-9]+}", handler.UpdateNginxTemplate())
// Specific Item
r.Route("/{templateID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityNginxTemplatesView)).
Get("/", handler.GetNginxTemplates())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) {
r.Delete("/", handler.DeleteNginxTemplate())
r.With(middleware.EnforceRequestSchema(schema.UpdateNginxTemplate())).
Put("/", handler.UpdateNginxTemplate())
})
})
})
// Streams
r.With(middleware.EnforceSetup(true)).Route("/streams", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityStreamsView), middleware.Filters(stream.GetFilterSchema())).
Get("/", handler.GetStreams())
r.With(middleware.Enforce(user.CapabilityStreamsView)).Get("/{hostID:[0-9]+}", handler.GetStream())
r.With(middleware.Enforce(user.CapabilityStreamsManage)).Delete("/{hostID:[0-9]+}", handler.DeleteStream())
r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.CreateStream())).
// Create
r.With(middleware.Enforce(user.CapabilityStreamsManage), middleware.EnforceRequestSchema(schema.CreateStream())).
Post("/", handler.CreateStream())
r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.UpdateStream())).
Put("/{hostID:[0-9]+}", handler.UpdateStream())
// Specific Item
r.Route("/{hostID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityStreamsView)).
Get("/", handler.GetStream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) {
r.Delete("/", handler.DeleteStream())
r.With(middleware.EnforceRequestSchema(schema.UpdateStream())).
Put("/", handler.UpdateStream())
})
})
})
// Upstreams
r.With(middleware.EnforceSetup(true)).Route("/upstreams", func(r chi.Router) {
// List
r.With(middleware.Enforce(user.CapabilityHostsView), middleware.Filters(upstream.GetFilterSchema())).
Get("/", handler.GetUpstreams())
r.With(middleware.Enforce(user.CapabilityHostsView)).Get("/{upstreamID:[0-9]+}", handler.GetUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Delete("/{upstreamID:[0-9]+}", handler.DeleteUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.CreateUpstream())).
// Create
r.With(middleware.Enforce(user.CapabilityHostsManage), middleware.EnforceRequestSchema(schema.CreateUpstream())).
Post("/", handler.CreateUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.UpdateUpstream())).
Put("/{upstreamID:[0-9]+}", handler.UpdateUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Get("/{upstreamID:[0-9]+}/nginx-config", handler.GetUpstreamNginxConfig("json"))
r.With(middleware.Enforce(user.CapabilityHostsManage)).Get("/{upstreamID:[0-9]+}/nginx-config.txt", handler.GetUpstreamNginxConfig("text"))
// Specific Item
r.Route("/{upstreamID:[0-9]+}", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityHostsView)).
Get("/", handler.GetUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) {
r.Delete("/", handler.DeleteUpstream())
r.With(middleware.EnforceRequestSchema(schema.UpdateUpstream())).
Put("/", handler.UpdateUpstream())
r.Get("/nginx-config", handler.GetUpstreamNginxConfig("json"))
r.Get("/nginx-config.txt", handler.GetUpstreamNginxConfig("text"))
})
})
})
})

View File

@ -6,6 +6,7 @@ import (
"time"
"npm/internal/logger"
"npm/internal/serverevents"
)
const httpPort = 3000
@ -20,6 +21,8 @@ func StartServer() {
ReadHeaderTimeout: 3 * time.Second,
}
defer serverevents.Shutdown()
err := server.ListenAndServe()
if err != nil {
logger.Error("HttpListenError", err)

View File

@ -14,6 +14,7 @@ import (
"npm/internal/entity/dnsprovider"
"npm/internal/entity/user"
"npm/internal/logger"
"npm/internal/serverevents"
"npm/internal/types"
"npm/internal/util"
@ -123,6 +124,9 @@ func (m *Model) Delete() bool {
if err := m.Save(); err != nil {
return false
}
// todo: delete from acme.sh as well
return true
}
@ -239,6 +243,7 @@ func (m *Model) GetCertificateLocations() (string, string, string) {
// Request makes a certificate request
func (m *Model) Request() error {
logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name)
serverevents.SendChange("certificates")
// nolint: errcheck, gosec
m.Expand([]string{"certificate-authority", "dns-provider"})
@ -283,6 +288,7 @@ func (m *Model) Request() error {
return err
}
serverevents.SendChange("certificates")
logger.Info("Request for certificate for: #%d %v was completed", m.ID, m.Name)
return nil
}

View File

@ -24,11 +24,16 @@ type GeneratedResponse struct {
}
// Generate will create a JWT
func Generate(userObj *user.Model) (GeneratedResponse, error) {
func Generate(userObj *user.Model, forSSE bool) (GeneratedResponse, error) {
var response GeneratedResponse
key, _ := GetPrivateKey()
expires := time.Now().AddDate(0, 0, 1) // 1 day
issuer := "api"
if forSSE {
issuer = "sse"
}
// Create the Claims
claims := UserJWTClaims{
@ -37,7 +42,7 @@ func Generate(userObj *user.Model) (GeneratedResponse, error) {
jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
ExpiresAt: expires.Unix(),
Issuer: "api",
Issuer: issuer,
},
}

View File

@ -92,6 +92,11 @@ func Error(errorClass string, err error) {
logger.Error(errorClass, err)
}
// Get returns the logger
func Get() *Logger {
return logger
}
// Configure logger and will return error if missing required fields.
func (l *Logger) Configure(c *Config) error {
// ensure updates to the config are atomic

View File

@ -0,0 +1,74 @@
package serverevents
import (
"encoding/json"
"net/http"
"npm/internal/logger"
"github.com/jc21/go-sse"
)
var instance *sse.Server
const defaultChannel = "changes"
// Message is how we're going to send the data
type Message struct {
Lang string `json:"lang,omitempty"`
LangParams map[string]string `json:"lang_params,omitempty"`
Type string `json:"type,omitempty"`
Affects string `json:"affects,omitempty"`
}
// Get will return a sse server
func Get() *sse.Server {
if instance == nil {
instance = sse.NewServer(&sse.Options{
Logger: logger.Get(),
ChannelNameFunc: func(request *http.Request) string {
return defaultChannel // This is the channel for all updates regardless of visibility
},
})
}
return instance
}
// Shutdown will shutdown the server
func Shutdown() {
if instance != nil {
instance.Shutdown()
}
}
// SendChange will send a specific change
func SendChange(affects string) {
Send(Message{Affects: affects}, "")
}
// SendMessage will construct a message for the UI
func SendMessage(typ, lang string, langParams map[string]string) {
Send(Message{
Type: typ,
Lang: lang,
LangParams: langParams,
}, "")
}
// Send will send a message
func Send(msg Message, channel string) {
if channel == "" {
channel = defaultChannel
}
logger.Debug("SSE Sending: %+v", msg)
if data, err := json.Marshal(msg); err != nil {
logger.Error("SSEError", err)
} else {
Get().SendMessage(channel, sse.SimpleMessage(string(data)))
}
}
// TODO: if we end up implementing user visibility,
// then we'll have to subscribe people to their own
// channels and publish to all or some depending on visibility.
// This means using a specific ChannelNameFunc that revolves
// around the user and their visibility.

View File

@ -7,12 +7,18 @@ server {
}
location /api/ {
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:3000/api/;
add_header X-Served-By $host;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection '';
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Accel-Buffering no;
proxy_pass http://127.0.0.1:3000/api/;
}
location ~ .html {

View File

@ -4,11 +4,17 @@ server {
server_name nginxproxymanager;
location / {
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://localhost:3000/;
add_header X-Served-By $host;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection '';
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Accel-Buffering no;
proxy_pass http://localhost:3000/;
}
}

View File

@ -0,0 +1,14 @@
import * as api from "./base";
import { TokenResponse } from "./responseTypes";
export async function getSSEToken(
abortController?: AbortController,
): Promise<TokenResponse> {
const { result } = await api.post(
{
url: "/tokens/sse",
},
abortController,
);
return result;
}

View File

@ -14,6 +14,7 @@ export * from "./getHealth";
export * from "./getHosts";
export * from "./getNginxTemplates";
export * from "./getSettings";
export * from "./getSSEToken";
export * from "./getToken";
export * from "./getUpstreamNginxConfig";
export * from "./getUpstreams";
@ -22,6 +23,7 @@ export * from "./getUsers";
export * from "./helpers";
export * from "./models";
export * from "./refreshToken";
export * from "./renewCertificate";
export * from "./responseTypes";
export * from "./setAuth";
export * from "./setCertificate";

View File

@ -165,3 +165,10 @@ export interface Upstream {
advancedConfig: string;
isDisabled: boolean;
}
export interface SSEMessage {
lang?: string;
langParams?: string;
type?: "info" | "warning" | "success" | "error" | "loading";
affects?: string | string[];
}

View File

@ -0,0 +1,15 @@
import * as api from "./base";
import { Certificate } from "./models";
export async function renewCertificate(
id: number,
abortController?: AbortController,
): Promise<Certificate> {
const { result } = await api.post(
{
url: `/certificates/${id}/renew`,
},
abortController,
);
return result;
}

View File

@ -1,12 +1,56 @@
import { ReactNode } from "react";
import { useEffect, ReactNode } from "react";
import { Box, Container } from "@chakra-ui/react";
import { Box, Container, useToast } from "@chakra-ui/react";
import { getSSEToken, SSEMessage } from "api/npm";
import { Footer, Navigation } from "components";
import { intl } from "locale";
import AuthStore from "modules/AuthStore";
import { useQueryClient } from "react-query";
interface Props {
children?: ReactNode;
}
function SiteWrapper({ children }: Props) {
const queryClient = useQueryClient();
const toast = useToast();
// TODO: fix bug where this will fail if the browser is kept open longer
// than the expiry of the sse token
useEffect(() => {
async function fetchData() {
const response = await getSSEToken();
const eventSource = new EventSource(
`/api/sse/changes?jwt=${response.token}`,
);
eventSource.onmessage = (e: any) => {
const j: SSEMessage = JSON.parse(e.data);
if (j) {
if (j.affects) {
queryClient.invalidateQueries(j.affects);
}
if (j.type) {
toast({
description: intl.formatMessage({ id: j.lang }),
status: j.type || "info",
position: "top",
duration: 3000,
isClosable: true,
});
}
}
};
eventSource.onerror = (e) => {
console.error("SSE EventSource failed:", e);
};
return () => {
eventSource.close();
};
}
if (AuthStore.token) {
fetchData();
}
}, [queryClient, toast]);
return (
<Box display="flex" flexDir="column" height="100vh">
<Box flexShrink={0}>

View File

@ -71,6 +71,9 @@
"certificate.create": {
"defaultMessage": "Zertifikat erstellen"
},
"certificate.renewal-requested": {
"defaultMessage": "Renewal has been queued"
},
"certificates.title": {
"defaultMessage": "Zertifikate"
},

View File

@ -347,6 +347,9 @@
"certificate.create": {
"defaultMessage": "Create Certificate"
},
"certificate.renewal-requested": {
"defaultMessage": "Renewal has been queued"
},
"certificates.title": {
"defaultMessage": "Certificates"
},

View File

@ -71,6 +71,9 @@
"certificate.create": {
"defaultMessage": "ایجاد گواهی"
},
"certificate.renewal-requested": {
"defaultMessage": "Renewal has been queued"
},
"certificates.title": {
"defaultMessage": "گواهینامه ها"
},

View File

@ -25,6 +25,7 @@ export interface TableProps {
sortBy: TableSortBy[];
filters: TableFilter[];
onTableEvent: any;
onRenewal: (id: number) => void;
}
function Table({
data,
@ -32,6 +33,7 @@ function Table({
onTableEvent,
sortBy,
filters,
onRenewal,
}: TableProps) {
const [editId, setEditId] = useState(0);
const [columns, tableData] = useMemo(() => {
@ -85,7 +87,7 @@ function Table({
title: intl.formatMessage({
id: "action.renew",
}),
onClick: (e: any, { id }: any) => alert(id),
onClick: (e: any, { id }: any) => onRenewal(id),
icon: <FiRefreshCw />,
disabled: (data: any) =>
data.type !== "dns" && data.type !== "http",
@ -110,7 +112,7 @@ function Table({
},
];
return [columns, data];
}, [data]);
}, [data, onRenewal]);
const tableInstance = useTable(
{

View File

@ -1,9 +1,11 @@
import { useEffect, useReducer, useState } from "react";
import { Alert, AlertIcon } from "@chakra-ui/react";
import { Alert, AlertIcon, useToast } from "@chakra-ui/react";
import { renewCertificate } from "api/npm";
import { EmptyList, SpinnerPage, tableEventReducer } from "components";
import { useCertificates } from "hooks";
import { intl } from "locale";
import { useQueryClient } from "react-query";
import Table from "./Table";
@ -19,6 +21,9 @@ const initialState = {
filters: [],
};
function TableWrapper() {
const toast = useToast();
const queryClient = useQueryClient();
const [{ offset, limit, sortBy, filters }, dispatch] = useReducer(
tableEventReducer,
initialState,
@ -36,6 +41,32 @@ function TableWrapper() {
setTableData(data as any);
}, [data]);
const renewCert = async (id: number) => {
try {
await renewCertificate(id);
toast({
description: intl.formatMessage({
id: `certificate.renewal-requested`,
}),
status: "info",
position: "top",
duration: 3000,
isClosable: true,
});
setTimeout(() => {
queryClient.invalidateQueries("certificates");
}, 500);
} catch (err: any) {
toast({
description: err.message,
status: "error",
position: "top",
duration: 3000,
isClosable: true,
});
}
};
if (isFetching || isLoading || !tableData) {
return <SpinnerPage />;
}
@ -76,6 +107,7 @@ function TableWrapper() {
sortBy={sortBy}
filters={filters}
onTableEvent={dispatch}
onRenewal={renewCert}
/>
);
}

View File

@ -25,4 +25,4 @@
"include": [
"src"
]
}
}