mirror of
https://github.com/xiaoxinpro/nginx-proxy-manager-zh.git
synced 2025-01-23 05:18:12 -05:00
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:
parent
35550082bf
commit
215083f6cf
@ -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"
|
||||
|
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
backend/embed/api_docs/paths/tokens/sse/post.json
Normal file
33
backend/embed/api_docs/paths/tokens/sse/post.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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",
|
||||
|
28
backend/internal/api/handler/sse_notification.go
Normal file
28
backend/internal/api/handler/sse_notification.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
34
backend/internal/api/middleware/sse_auth.go
Normal file
34
backend/internal/api/middleware/sse_auth.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
74
backend/internal/serverevents/sse.go
Normal file
74
backend/internal/serverevents/sse.go
Normal 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.
|
@ -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 {
|
||||
|
@ -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/;
|
||||
}
|
||||
}
|
||||
|
14
frontend/src/api/npm/getSSEToken.ts
Normal file
14
frontend/src/api/npm/getSSEToken.ts
Normal 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;
|
||||
}
|
@ -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";
|
||||
|
@ -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[];
|
||||
}
|
||||
|
15
frontend/src/api/npm/renewCertificate.ts
Normal file
15
frontend/src/api/npm/renewCertificate.ts
Normal 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;
|
||||
}
|
@ -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}>
|
||||
|
@ -71,6 +71,9 @@
|
||||
"certificate.create": {
|
||||
"defaultMessage": "Zertifikat erstellen"
|
||||
},
|
||||
"certificate.renewal-requested": {
|
||||
"defaultMessage": "Renewal has been queued"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Zertifikate"
|
||||
},
|
||||
|
@ -347,6 +347,9 @@
|
||||
"certificate.create": {
|
||||
"defaultMessage": "Create Certificate"
|
||||
},
|
||||
"certificate.renewal-requested": {
|
||||
"defaultMessage": "Renewal has been queued"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Certificates"
|
||||
},
|
||||
|
@ -71,6 +71,9 @@
|
||||
"certificate.create": {
|
||||
"defaultMessage": "ایجاد گواهی"
|
||||
},
|
||||
"certificate.renewal-requested": {
|
||||
"defaultMessage": "Renewal has been queued"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "گواهینامه ها"
|
||||
},
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -25,4 +25,4 @@
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user