Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
25
backend/internal/api/context/context.go
Normal file
25
backend/internal/api/context/context.go
Normal file
@ -0,0 +1,25 @@
|
||||
package context
|
||||
|
||||
var (
|
||||
// BodyCtxKey is the name of the Body value on the context
|
||||
BodyCtxKey = &contextKey{"Body"}
|
||||
// UserIDCtxKey is the name of the UserID value on the context
|
||||
UserIDCtxKey = &contextKey{"UserID"}
|
||||
// FiltersCtxKey is the name of the Filters value on the context
|
||||
FiltersCtxKey = &contextKey{"Filters"}
|
||||
// PrettyPrintCtxKey is the name of the pretty print context
|
||||
PrettyPrintCtxKey = &contextKey{"Pretty"}
|
||||
// ExpansionCtxKey is the name of the expansion context
|
||||
ExpansionCtxKey = &contextKey{"Expansion"}
|
||||
)
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *contextKey) String() string {
|
||||
return "context value: " + k.name
|
||||
}
|
208
backend/internal/api/filters/helpers.go
Normal file
208
backend/internal/api/filters/helpers.go
Normal file
@ -0,0 +1,208 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware
|
||||
func NewFilterSchema(fieldSchemas []string) string {
|
||||
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
|
||||
}
|
||||
|
||||
// BoolFieldSchema returns the Field Schema for a Boolean accepted value field
|
||||
func BoolFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
%s,
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, boolModifiers, filterBool, filterBool)
|
||||
}
|
||||
|
||||
// IntFieldSchema returns the Field Schema for a Integer accepted value field
|
||||
func IntFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
// StringFieldSchema returns the Field Schema for a String accepted value field
|
||||
func StringFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
%s,
|
||||
{
|
||||
"type": "array",
|
||||
"items": %s
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, stringModifiers, filterString, filterString)
|
||||
}
|
||||
|
||||
// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
|
||||
func RegexFieldSchema(fieldName string, regex string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "%s"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, stringModifiers, regex, regex)
|
||||
}
|
||||
|
||||
// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
||||
func DateFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format
|
||||
// 2020-03-01T10:30:00+10:00
|
||||
func DateTimeFieldSchema(fieldName string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"modifier": %s,
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`, fieldName, allModifiers)
|
||||
}
|
||||
|
||||
const allModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
|
||||
}`
|
||||
|
||||
const boolModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not)$"
|
||||
}`
|
||||
|
||||
const stringModifiers = `{
|
||||
"type": "string",
|
||||
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
|
||||
}`
|
||||
|
||||
const filterBool = `{
|
||||
"type": "string",
|
||||
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
|
||||
}`
|
||||
|
||||
const filterString = `{
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}`
|
||||
|
||||
const baseFilterSchema = `{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
%s
|
||||
]
|
||||
}
|
||||
}`
|
93
backend/internal/api/handler/auth.go
Normal file
93
backend/internal/api/handler/auth.go
Normal file
@ -0,0 +1,93 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/entity/user"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
type setAuthModel struct {
|
||||
Type string `json:"type" db:"type"`
|
||||
Secret string `json:"secret,omitempty" db:"secret"`
|
||||
CurrentSecret string `json:"current_secret,omitempty"`
|
||||
}
|
||||
|
||||
// SetAuth sets a auth method. This can be used for "me" and `2` for example
|
||||
// Route: POST /users/:userID/auth
|
||||
func SetAuth() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newAuth setAuthModel
|
||||
err := json.Unmarshal(bodyBytes, &newAuth)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userID, isSelf, userIDErr := getUserIDFromRequest(r)
|
||||
if userIDErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Load user
|
||||
thisUser, thisUserErr := user.GetByID(userID)
|
||||
if thisUserErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if thisUser.IsSystem {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Load existing auth for user
|
||||
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
|
||||
if userAuthErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if isSelf {
|
||||
// confirm that the current_secret given is valid for the one stored in the database
|
||||
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
|
||||
if validateErr != nil {
|
||||
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
|
||||
// Sleep for 1 second to prevent brute force password guessing
|
||||
time.Sleep(time.Second)
|
||||
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if newAuth.Type == auth.TypePassword {
|
||||
err := userAuth.SetPassword(newAuth.Secret)
|
||||
if err != nil {
|
||||
logger.Error("SetPasswordError", err)
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
if err = userAuth.Save(); err != nil {
|
||||
logger.Error("AuthSaveError", err)
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
||||
return
|
||||
}
|
||||
|
||||
userAuth.Secret = ""
|
||||
|
||||
// todo: add to audit-log
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
|
||||
}
|
||||
}
|
141
backend/internal/api/handler/certificate_authorities.go
Normal file
141
backend/internal/api/handler/certificate_authorities.go
Normal file
@ -0,0 +1,141 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"npm/internal/acme"
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
// GetCertificateAuthorities will return a list of Certificate Authorities
|
||||
// Route: GET /certificate-authorities
|
||||
func GetCertificateAuthorities() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
certificates, err := certificateauthority.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificateAuthority will return a single Certificate Authority
|
||||
// Route: GET /certificate-authorities/{caID}
|
||||
func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var caID int
|
||||
if caID, err = getURLParamInt(r, "caID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := certificateauthority.GetByID(caID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCertificateAuthority will create a Certificate Authority
|
||||
// Route: POST /certificate-authorities
|
||||
func CreateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newCA certificateauthority.Model
|
||||
err := json.Unmarshal(bodyBytes, &newCA)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newCA.Check(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newCA.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate Authority: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = acme.CreateAccountKey(&newCA); err != nil {
|
||||
logger.Error("CreateAccountKeyError", err)
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newCA)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCertificateAuthority updates a ca
|
||||
// Route: PUT /certificate-authorities/{caID}
|
||||
func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var caID int
|
||||
if caID, err = getURLParamInt(r, "caID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := certificateauthority.GetByID(caID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &ca)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ca.Check(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ca.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, ca)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCertificateAuthority deletes a ca
|
||||
// Route: DELETE /certificate-authorities/{caID}
|
||||
func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var caID int
|
||||
if caID, err = getURLParamInt(r, "caID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := certificateauthority.GetByID(caID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
|
||||
}
|
||||
}
|
||||
}
|
145
backend/internal/api/handler/certificates.go
Normal file
145
backend/internal/api/handler/certificates.go
Normal file
@ -0,0 +1,145 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/api/schema"
|
||||
"npm/internal/entity/certificate"
|
||||
)
|
||||
|
||||
// GetCertificates will return a list of Certificates
|
||||
// Route: GET /certificates
|
||||
func GetCertificates() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCertificate will return a single Certificate
|
||||
// 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
|
||||
}
|
||||
|
||||
cert, err := certificate.GetByID(certificateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCertificate will create a Certificate
|
||||
// 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 newCertificate certificate.Model
|
||||
err := json.Unmarshal(bodyBytes, &newCertificate)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCertificate updates a cert
|
||||
// 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)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, certificateObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCertificate deletes a cert
|
||||
// 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
|
||||
}
|
||||
|
||||
cert, err := certificate.GetByID(certificateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
|
||||
}
|
||||
}
|
||||
}
|
15
backend/internal/api/handler/config.go
Normal file
15
backend/internal/api/handler/config.go
Normal file
@ -0,0 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
// Config returns the entire configuration, for debug purposes
|
||||
// Route: GET /config
|
||||
func Config() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, config.Configuration)
|
||||
}
|
||||
}
|
159
backend/internal/api/handler/dns_providers.go
Normal file
159
backend/internal/api/handler/dns_providers.go
Normal file
@ -0,0 +1,159 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/dnsproviders"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
)
|
||||
|
||||
// GetDNSProviders will return a list of DNS Providers
|
||||
// Route: GET /dns-providers
|
||||
func GetDNSProviders() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDNSProvider will return a single DNS Provider
|
||||
// Route: GET /dns-providers/{providerID}
|
||||
func GetDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var providerID int
|
||||
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := dnsprovider.GetByID(providerID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDNSProvider will create a DNS Provider
|
||||
// Route: POST /dns-providers
|
||||
func CreateDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newItem dnsprovider.Model
|
||||
err := json.Unmarshal(bodyBytes, &newItem)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newItem.UserID = userID
|
||||
|
||||
if err = newItem.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDNSProvider updates a provider
|
||||
// Route: PUT /dns-providers/{providerID}
|
||||
func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var providerID int
|
||||
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := dnsprovider.GetByID(providerID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &item)
|
||||
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, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteDNSProvider removes a provider
|
||||
// Route: DELETE /dns-providers/{providerID}
|
||||
func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var providerID int
|
||||
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := dnsprovider.GetByID(providerID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAcmeshProviders will return a list of acme.sh providers
|
||||
// Route: GET /dns-providers/acmesh
|
||||
func GetAcmeshProviders() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
items := dnsproviders.List()
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, items)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAcmeshProvider will return a single acme.sh provider
|
||||
// Route: GET /dns-providers/acmesh/{acmeshID}
|
||||
func GetAcmeshProvider() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var acmeshID string
|
||||
var err error
|
||||
if acmeshID, err = getURLParamString(r, "acmeshID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
item, getErr := dnsproviders.Get(acmeshID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, getErr.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||
}
|
||||
}
|
||||
}
|
34
backend/internal/api/handler/health.go
Normal file
34
backend/internal/api/handler/health.go
Normal file
@ -0,0 +1,34 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"npm/internal/acme"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
type healthCheckResponse struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
AcmeShVersion string `json:"acme.sh"`
|
||||
Healthy bool `json:"healthy"`
|
||||
IsSetup bool `json:"setup"`
|
||||
ErrorReporting bool `json:"error_reporting"`
|
||||
}
|
||||
|
||||
// Health returns the health of the api
|
||||
// Route: GET /health
|
||||
func Health() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
health := healthCheckResponse{
|
||||
Version: config.Version,
|
||||
Commit: config.Commit,
|
||||
Healthy: true,
|
||||
IsSetup: config.IsSetup,
|
||||
AcmeShVersion: acme.GetAcmeShVersion(),
|
||||
ErrorReporting: config.ErrorReporting,
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, health)
|
||||
}
|
||||
}
|
175
backend/internal/api/handler/helpers.go
Normal file
175
backend/internal/api/handler/helpers.go
Normal file
@ -0,0 +1,175 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/api/context"
|
||||
"npm/internal/model"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
const defaultLimit = 10
|
||||
|
||||
func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
|
||||
var pageInfo model.PageInfo
|
||||
var err error
|
||||
|
||||
pageInfo.FromDate, pageInfo.ToDate, err = getDateRanges(r)
|
||||
if err != nil {
|
||||
return pageInfo, err
|
||||
}
|
||||
|
||||
pageInfo.Offset, pageInfo.Limit, err = getPagination(r)
|
||||
if err != nil {
|
||||
return pageInfo, err
|
||||
}
|
||||
|
||||
pageInfo.Sort = getSortParameter(r)
|
||||
|
||||
return pageInfo, nil
|
||||
}
|
||||
|
||||
func getDateRanges(r *http.Request) (time.Time, time.Time, error) {
|
||||
queryValues := r.URL.Query()
|
||||
from := queryValues.Get("from")
|
||||
fromDate := time.Now().AddDate(0, -1, 0) // 1 month ago by default
|
||||
to := queryValues.Get("to")
|
||||
toDate := time.Now()
|
||||
|
||||
if from != "" {
|
||||
var fromErr error
|
||||
fromDate, fromErr = time.Parse(time.RFC3339, from)
|
||||
if fromErr != nil {
|
||||
return fromDate, toDate, fmt.Errorf("From date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
|
||||
}
|
||||
}
|
||||
|
||||
if to != "" {
|
||||
var toErr error
|
||||
toDate, toErr = time.Parse(time.RFC3339, to)
|
||||
if toErr != nil {
|
||||
return fromDate, toDate, fmt.Errorf("To date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
|
||||
}
|
||||
}
|
||||
|
||||
return fromDate, toDate, nil
|
||||
}
|
||||
|
||||
func getSortParameter(r *http.Request) []model.Sort {
|
||||
var sortFields []model.Sort
|
||||
|
||||
queryValues := r.URL.Query()
|
||||
sortString := queryValues.Get("sort")
|
||||
if sortString == "" {
|
||||
return sortFields
|
||||
}
|
||||
|
||||
// Split sort fields up in to slice
|
||||
sorts := strings.Split(sortString, ",")
|
||||
for _, sortItem := range sorts {
|
||||
if strings.Contains(sortItem, ".") {
|
||||
theseItems := strings.Split(sortItem, ".")
|
||||
|
||||
switch strings.ToLower(theseItems[1]) {
|
||||
case "desc":
|
||||
fallthrough
|
||||
case "descending":
|
||||
theseItems[1] = "DESC"
|
||||
default:
|
||||
theseItems[1] = "ASC"
|
||||
}
|
||||
|
||||
sortFields = append(sortFields, model.Sort{
|
||||
Field: theseItems[0],
|
||||
Direction: theseItems[1],
|
||||
})
|
||||
} else {
|
||||
sortFields = append(sortFields, model.Sort{
|
||||
Field: sortItem,
|
||||
Direction: "ASC",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return sortFields
|
||||
}
|
||||
|
||||
func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) {
|
||||
queryValues := r.URL.Query()
|
||||
varValue := queryValues.Get(varName)
|
||||
|
||||
if varValue == "" && required {
|
||||
return 0, fmt.Errorf("%v was not supplied in the request", varName)
|
||||
} else if varValue == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
varInt, intErr := strconv.Atoi(varValue)
|
||||
if intErr != nil {
|
||||
return 0, fmt.Errorf("%v is not a valid number", varName)
|
||||
}
|
||||
|
||||
return varInt, nil
|
||||
}
|
||||
|
||||
func getURLParamInt(r *http.Request, varName string) (int, error) {
|
||||
required := true
|
||||
defaultValue := 0
|
||||
paramStr := chi.URLParam(r, varName)
|
||||
var err error
|
||||
var paramInt int
|
||||
|
||||
if paramStr == "" && required {
|
||||
return 0, fmt.Errorf("%v was not supplied in the request", varName)
|
||||
} else if paramStr == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
if paramInt, err = strconv.Atoi(paramStr); err != nil {
|
||||
return 0, fmt.Errorf("%v is not a valid number", varName)
|
||||
}
|
||||
|
||||
return paramInt, nil
|
||||
}
|
||||
|
||||
func getURLParamString(r *http.Request, varName string) (string, error) {
|
||||
required := true
|
||||
defaultValue := ""
|
||||
paramStr := chi.URLParam(r, varName)
|
||||
|
||||
if paramStr == "" && required {
|
||||
return "", fmt.Errorf("%v was not supplied in the request", varName)
|
||||
} else if paramStr == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
return paramStr, nil
|
||||
}
|
||||
|
||||
func getPagination(r *http.Request) (int, int, error) {
|
||||
var err error
|
||||
offset, err := getQueryVarInt(r, "offset", false, 0)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
limit, err := getQueryVarInt(r, "limit", false, defaultLimit)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return offset, limit, nil
|
||||
}
|
||||
|
||||
// getExpandFromContext returns the Expansion setting
|
||||
func getExpandFromContext(r *http.Request) []string {
|
||||
expand, ok := r.Context().Value(context.ExpansionCtxKey).([]string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return expand
|
||||
}
|
130
backend/internal/api/handler/host_templates.go
Normal file
130
backend/internal/api/handler/host_templates.go
Normal file
@ -0,0 +1,130 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/host"
|
||||
"npm/internal/entity/hosttemplate"
|
||||
)
|
||||
|
||||
// GetHostTemplates will return a list of Host Templates
|
||||
// Route: GET /host-templates
|
||||
func GetHostTemplates() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := hosttemplate.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetHostTemplate will return a single Host Template
|
||||
// Route: GET /host-templates/{templateID}
|
||||
func GetHostTemplate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var templateID int
|
||||
if templateID, err = getURLParamInt(r, "templateID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := hosttemplate.GetByID(templateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHostTemplate will create a Host Template
|
||||
// Route: POST /host-templates
|
||||
func CreateHostTemplate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newHostTemplate hosttemplate.Model
|
||||
err := json.Unmarshal(bodyBytes, &newHostTemplate)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newHostTemplate.UserID = userID
|
||||
|
||||
if err = newHostTemplate.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host Template: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHostTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateHostTemplate updates a host template
|
||||
// Route: PUT /host-templates/{templateID}
|
||||
func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var templateID int
|
||||
if templateID, err = getURLParamInt(r, "templateID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hostTemplate, err := hosttemplate.GetByID(templateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &hostTemplate)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = hostTemplate.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteHostTemplate removes a host template
|
||||
// Route: DELETE /host-templates/{templateID}
|
||||
func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var templateID int
|
||||
if templateID, err = getURLParamInt(r, "templateID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hostTemplate, err := host.GetByID(templateID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate.Delete())
|
||||
}
|
||||
}
|
||||
}
|
140
backend/internal/api/handler/hosts.go
Normal file
140
backend/internal/api/handler/hosts.go
Normal file
@ -0,0 +1,140 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/host"
|
||||
"npm/internal/validator"
|
||||
)
|
||||
|
||||
// GetHosts will return a list of Hosts
|
||||
// Route: GET /hosts
|
||||
func GetHosts() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetHost will return a single Host
|
||||
// Route: GET /hosts/{hostID}
|
||||
func GetHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hostObject, err := host.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
// nolint: errcheck,gosec
|
||||
hostObject.Expand(getExpandFromContext(r))
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hostObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateHost will create a Host
|
||||
// Route: POST /hosts
|
||||
func CreateHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newHost host.Model
|
||||
err := json.Unmarshal(bodyBytes, &newHost)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newHost.UserID = userID
|
||||
|
||||
if err = validator.ValidateHost(newHost); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newHost.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateHost updates a host
|
||||
// Route: PUT /hosts/{hostID}
|
||||
func UpdateHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hostObject, err := host.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &hostObject)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = hostObject.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// nolint: errcheck,gosec
|
||||
hostObject.Expand(getExpandFromContext(r))
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hostObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteHost removes a host
|
||||
// Route: DELETE /hosts/{hostID}
|
||||
func DeleteHost() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := host.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
|
||||
}
|
||||
}
|
||||
}
|
14
backend/internal/api/handler/not_allowed.go
Normal file
14
backend/internal/api/handler/not_allowed.go
Normal file
@ -0,0 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
h "npm/internal/api/http"
|
||||
)
|
||||
|
||||
// NotAllowed is a json error handler for when method is not allowed
|
||||
func NotAllowed() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not allowed", nil)
|
||||
}
|
||||
}
|
64
backend/internal/api/handler/not_found.go
Normal file
64
backend/internal/api/handler/not_found.go
Normal file
@ -0,0 +1,64 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"npm/embed"
|
||||
h "npm/internal/api/http"
|
||||
)
|
||||
|
||||
var (
|
||||
assetsSub fs.FS
|
||||
errIsDir = errors.New("path is dir")
|
||||
)
|
||||
|
||||
// NotFound is a json error handler for 404's and method not allowed.
|
||||
// It also serves the react frontend as embedded files in the golang binary.
|
||||
func NotFound() func(http.ResponseWriter, *http.Request) {
|
||||
assetsSub, _ = fs.Sub(embed.Assets, "assets")
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimLeft(r.URL.Path, "/")
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
err := tryRead(assetsSub, path, w)
|
||||
if err == errIsDir {
|
||||
err = tryRead(assetsSub, "index.html", w)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
} else if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func tryRead(folder fs.FS, requestedPath string, w http.ResponseWriter) error {
|
||||
f, err := folder.Open(requestedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
defer f.Close()
|
||||
|
||||
stat, _ := f.Stat()
|
||||
if stat.IsDir() {
|
||||
return errIsDir
|
||||
}
|
||||
|
||||
contentType := mime.TypeByExtension(filepath.Ext(requestedPath))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
_, err = io.Copy(w, f)
|
||||
return err
|
||||
}
|
108
backend/internal/api/handler/schema.go
Normal file
108
backend/internal/api/handler/schema.go
Normal file
@ -0,0 +1,108 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"npm/embed"
|
||||
"npm/internal/api/schema"
|
||||
"npm/internal/config"
|
||||
"npm/internal/logger"
|
||||
|
||||
jsref "github.com/jc21/jsref"
|
||||
"github.com/jc21/jsref/provider"
|
||||
)
|
||||
|
||||
var (
|
||||
swaggerSchema []byte
|
||||
apiDocsSub fs.FS
|
||||
)
|
||||
|
||||
// Schema simply reads the swagger schema from disk and returns is raw
|
||||
// Route: GET /schema
|
||||
func Schema() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, string(getSchema()))
|
||||
}
|
||||
}
|
||||
|
||||
func getSchema() []byte {
|
||||
if swaggerSchema == nil {
|
||||
apiDocsSub, _ = fs.Sub(embed.APIDocFiles, "api_docs")
|
||||
|
||||
// nolint:gosec
|
||||
swaggerSchema, _ = fs.ReadFile(apiDocsSub, "api.swagger.json")
|
||||
|
||||
// Replace {{VERSION}} with Config Version
|
||||
swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version))
|
||||
|
||||
// Dereference the JSON Schema:
|
||||
var schema interface{}
|
||||
if err := json.Unmarshal(swaggerSchema, &schema); err != nil {
|
||||
logger.Error("SwaggerUnmarshalError", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
provider := provider.NewIoFS(apiDocsSub, "")
|
||||
resolver := jsref.New()
|
||||
err := resolver.AddProvider(provider)
|
||||
if err != nil {
|
||||
logger.Error("SchemaProviderError", err)
|
||||
}
|
||||
|
||||
result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...)
|
||||
if err != nil {
|
||||
logger.Error("SwaggerResolveError", err)
|
||||
} else {
|
||||
var marshalErr error
|
||||
swaggerSchema, marshalErr = json.MarshalIndent(result, "", " ")
|
||||
if marshalErr != nil {
|
||||
logger.Error("SwaggerMarshalError", err)
|
||||
}
|
||||
}
|
||||
// End dereference
|
||||
|
||||
// Replace incoming schemas with those we actually use in code
|
||||
swaggerSchema = replaceIncomingSchemas(swaggerSchema)
|
||||
}
|
||||
return swaggerSchema
|
||||
}
|
||||
|
||||
func replaceIncomingSchemas(swaggerSchema []byte) []byte {
|
||||
str := string(swaggerSchema)
|
||||
|
||||
// Remember to include the double quotes in the replacement!
|
||||
str = strings.ReplaceAll(str, `"{{schema.SetAuth}}"`, schema.SetAuth())
|
||||
str = strings.ReplaceAll(str, `"{{schema.GetToken}}"`, schema.GetToken())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateCertificateAuthority}}"`, schema.CreateCertificateAuthority())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificateAuthority}}"`, schema.UpdateCertificateAuthority())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateCertificate}}"`, schema.CreateCertificate())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificate}}"`, schema.UpdateCertificate(""))
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateSetting}}"`, schema.CreateSetting())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateSetting}}"`, schema.UpdateSetting())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateUser}}"`, schema.CreateUser())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateUser}}"`, schema.UpdateUser())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateHostTemplate}}"`, schema.CreateHostTemplate())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateHostTemplate}}"`, schema.UpdateHostTemplate())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream())
|
||||
|
||||
str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider())
|
||||
str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider())
|
||||
|
||||
return []byte(str)
|
||||
}
|
98
backend/internal/api/handler/settings.go
Normal file
98
backend/internal/api/handler/settings.go
Normal file
@ -0,0 +1,98 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/setting"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// GetSettings will return a list of Settings
|
||||
// Route: GET /settings
|
||||
func GetSettings() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := setting.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSetting will return a single Setting
|
||||
// Route: GET /settings/{name}
|
||||
func GetSetting() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
|
||||
sett, err := setting.GetByName(name)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, sett)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSetting will create a Setting
|
||||
// Route: POST /settings
|
||||
func CreateSetting() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newSetting setting.Model
|
||||
err := json.Unmarshal(bodyBytes, &newSetting)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newSetting.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newSetting)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSetting updates a setting
|
||||
// Route: PUT /settings/{name}
|
||||
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
settingName := chi.URLParam(r, "name")
|
||||
|
||||
setting, err := setting.GetByName(settingName)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &setting)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = setting.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, setting)
|
||||
}
|
||||
}
|
||||
}
|
129
backend/internal/api/handler/streams.go
Normal file
129
backend/internal/api/handler/streams.go
Normal file
@ -0,0 +1,129 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/entity/stream"
|
||||
)
|
||||
|
||||
// GetStreams will return a list of Streams
|
||||
// Route: GET /hosts/streams
|
||||
func GetStreams() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStream will return a single Streams
|
||||
// Route: GET /hosts/streams/{hostID}
|
||||
func GetStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := stream.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateStream will create a Stream
|
||||
// Route: POST /hosts/steams
|
||||
func CreateStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newHost stream.Model
|
||||
err := json.Unmarshal(bodyBytes, &newHost)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get userID from token
|
||||
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
newHost.UserID = userID
|
||||
|
||||
if err = newHost.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateStream updates a stream
|
||||
// Route: PUT /hosts/streams/{hostID}
|
||||
func UpdateStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := stream.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &host)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = host.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteStream removes a stream
|
||||
// Route: DELETE /hosts/streams/{hostID}
|
||||
func DeleteStream() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var hostID int
|
||||
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := stream.GetByID(hostID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
|
||||
}
|
||||
}
|
||||
}
|
89
backend/internal/api/handler/tokens.go
Normal file
89
backend/internal/api/handler/tokens.go
Normal file
@ -0,0 +1,89 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
"time"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/entity/user"
|
||||
njwt "npm/internal/jwt"
|
||||
)
|
||||
|
||||
// tokenPayload is the structure we expect from a incoming login request
|
||||
type tokenPayload struct {
|
||||
Type string `json:"type"`
|
||||
Identity string `json:"identity"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// NewToken Also known as a Login, requesting a new token with credentials
|
||||
// Route: POST /tokens
|
||||
func NewToken() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Read the bytes from the body
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var payload tokenPayload
|
||||
err := json.Unmarshal(bodyBytes, &payload)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Find user
|
||||
userObj, userErr := user.GetByEmail(payload.Identity)
|
||||
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
|
||||
}
|
||||
|
||||
// Get Auth
|
||||
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
|
||||
if authErr != nil {
|
||||
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify Auth
|
||||
validateErr := authObj.ValidateSecret(payload.Secret)
|
||||
if validateErr != nil {
|
||||
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
|
||||
// Sleep for 1 second to prevent brute force password guessing
|
||||
time.Sleep(time.Second)
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if response, err := njwt.Generate(&userObj); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshToken an existing token by given them a new one with the same claims
|
||||
// Route: GET /tokens
|
||||
func RefreshToken() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *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 {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
}
|
235
backend/internal/api/handler/users.go
Normal file
235
backend/internal/api/handler/users.go
Normal file
@ -0,0 +1,235 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/auth"
|
||||
"npm/internal/entity/user"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// GetUsers returns all users
|
||||
// Route: GET /users
|
||||
func GetUsers() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageInfo, err := getPageInfoFromRequest(r)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, users)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUser returns a specific user
|
||||
// Route: GET /users/{userID}
|
||||
func GetUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _, userIDErr := getUserIDFromRequest(r)
|
||||
if userIDErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userObject, err := user.GetByID(userID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
// nolint: errcheck,gosec
|
||||
userObject.Expand(getExpandFromContext(r))
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUser updates a user
|
||||
// Route: PUT /users/{userID}
|
||||
func UpdateUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, self, userIDErr := getUserIDFromRequest(r)
|
||||
if userIDErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
userObject, err := user.GetByID(userID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
// nolint: errcheck,gosec
|
||||
userObject.Expand([]string{"capabilities"})
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
err := json.Unmarshal(bodyBytes, &userObject)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if userObject.IsDisabled && self {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = userObject.Save(); err != nil {
|
||||
if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
logger.Error("UpdateUserError", err)
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !self {
|
||||
err = userObject.SaveCapabilities()
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: errcheck,gosec
|
||||
userObject.Expand(getExpandFromContext(r))
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser removes a user
|
||||
// Route: DELETE /users/{userID}
|
||||
func DeleteUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var userID int
|
||||
var err error
|
||||
if userID, err = getURLParamInt(r, "userID"); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
myUserID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
if myUserID == userID {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := user.GetByID(userID)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, user.Delete())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser creates a user
|
||||
// Route: POST /users
|
||||
func CreateUser() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
var newUser user.Model
|
||||
err := json.Unmarshal(bodyBytes, &newUser)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err = newUser.Save(); err != nil {
|
||||
if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
logger.Error("UpdateUserError", err)
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set the permissions to full-admin for this user
|
||||
if !config.IsSetup {
|
||||
newUser.Capabilities = []string{user.CapabilityFullAdmin}
|
||||
}
|
||||
|
||||
// nolint: errcheck,gosec
|
||||
err = newUser.SaveCapabilities()
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// newUser has been saved, now save their auth
|
||||
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
|
||||
newUser.Auth.UserID = newUser.ID
|
||||
if newUser.Auth.Type == auth.TypePassword {
|
||||
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
|
||||
if err != nil {
|
||||
logger.Error("SetPasswordError", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = newUser.Auth.Save(); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
||||
return
|
||||
}
|
||||
|
||||
newUser.Auth.Secret = ""
|
||||
}
|
||||
|
||||
if !config.IsSetup {
|
||||
config.IsSetup = true
|
||||
logger.Info("A new user was created, leaving Setup Mode")
|
||||
}
|
||||
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, newUser)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUsers is only available in debug mode for cypress tests
|
||||
// Route: DELETE /users
|
||||
func DeleteUsers() func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := user.DeleteAll()
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||
} else {
|
||||
// also change setup to true
|
||||
config.IsSetup = false
|
||||
logger.Info("Users have been wiped, entering Setup Mode")
|
||||
h.ResultResponseJSON(w, r, http.StatusOK, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUserIDFromRequest(r *http.Request) (int, bool, error) {
|
||||
userIDstr := chi.URLParam(r, "userID")
|
||||
selfUserID, _ := r.Context().Value(c.UserIDCtxKey).(int)
|
||||
|
||||
var userID int
|
||||
self := false
|
||||
if userIDstr == "me" {
|
||||
// Get user id from Token
|
||||
userID = selfUserID
|
||||
self = true
|
||||
} else {
|
||||
var userIDerr error
|
||||
if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil {
|
||||
return 0, false, userIDerr
|
||||
}
|
||||
self = selfUserID == userID
|
||||
}
|
||||
return userID, self, nil
|
||||
}
|
46
backend/internal/api/http/requests.go
Normal file
46
backend/internal/api/http/requests.go
Normal file
@ -0,0 +1,46 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidJSON is an error for invalid json
|
||||
ErrInvalidJSON = errors.New("JSON is invalid")
|
||||
// ErrInvalidPayload is an error for invalid incoming data
|
||||
ErrInvalidPayload = errors.New("Payload is invalid")
|
||||
)
|
||||
|
||||
// ValidateRequestSchema takes a Schema and the Content to validate against it
|
||||
func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) {
|
||||
var jsonErrors []jsonschema.KeyError
|
||||
var schemaBytes = []byte(schema)
|
||||
|
||||
// Make sure the body is valid JSON
|
||||
if !isJSON(requestBody) {
|
||||
return jsonErrors, ErrInvalidJSON
|
||||
}
|
||||
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal(schemaBytes, rs); err != nil {
|
||||
return jsonErrors, err
|
||||
}
|
||||
|
||||
var validationErr error
|
||||
ctx := context.TODO()
|
||||
if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 {
|
||||
return jsonErrors, validationErr
|
||||
}
|
||||
|
||||
// Valid
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func isJSON(bytes []byte) bool {
|
||||
var js map[string]interface{}
|
||||
return json.Unmarshal(bytes, &js) == nil
|
||||
}
|
91
backend/internal/api/http/responses.go
Normal file
91
backend/internal/api/http/responses.go
Normal file
@ -0,0 +1,91 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
"npm/internal/errors"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// Response interface for standard API results
|
||||
type Response struct {
|
||||
Result interface{} `json:"result"`
|
||||
Error interface{} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse interface for errors returned via the API
|
||||
type ErrorResponse struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message interface{} `json:"message"`
|
||||
Invalid interface{} `json:"invalid,omitempty"`
|
||||
}
|
||||
|
||||
// ResultResponseJSON will write the result as json to the http output
|
||||
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
|
||||
var response Response
|
||||
resultClass := fmt.Sprintf("%v", reflect.TypeOf(result))
|
||||
|
||||
if resultClass == "http.ErrorResponse" {
|
||||
response = Response{
|
||||
Error: result,
|
||||
}
|
||||
} else {
|
||||
response = Response{
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
var payload []byte
|
||||
var err error
|
||||
if getPrettyPrintFromContext(r) {
|
||||
payload, err = json.MarshalIndent(response, "", " ")
|
||||
} else {
|
||||
payload, err = json.Marshal(response)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("ResponseMarshalError", err)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, string(payload))
|
||||
}
|
||||
|
||||
// ResultSchemaErrorJSON will format the result as a standard error object and send it for output
|
||||
func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errs []jsonschema.KeyError) {
|
||||
errorResponse := ErrorResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: errors.ErrValidationFailed,
|
||||
Invalid: errs,
|
||||
}
|
||||
|
||||
ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse)
|
||||
}
|
||||
|
||||
// ResultErrorJSON will format the result as a standard error object and send it for output
|
||||
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) {
|
||||
errorResponse := ErrorResponse{
|
||||
Code: status,
|
||||
Message: message,
|
||||
Invalid: extended,
|
||||
}
|
||||
|
||||
ResultResponseJSON(w, r, status, errorResponse)
|
||||
}
|
||||
|
||||
// getPrettyPrintFromContext returns the PrettyPrint setting
|
||||
func getPrettyPrintFromContext(r *http.Request) bool {
|
||||
pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return pretty
|
||||
}
|
13
backend/internal/api/middleware/access_control.go
Normal file
13
backend/internal/api/middleware/access_control.go
Normal file
@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AccessControl sets http headers for responses
|
||||
func AccessControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
94
backend/internal/api/middleware/auth.go
Normal file
94
backend/internal/api/middleware/auth.go
Normal file
@ -0,0 +1,94 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/user"
|
||||
njwt "npm/internal/jwt"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/util"
|
||||
|
||||
"github.com/go-chi/jwtauth"
|
||||
)
|
||||
|
||||
// DecodeAuth decodes an auth header
|
||||
func DecodeAuth() func(http.Handler) http.Handler {
|
||||
privateKey, privateKeyParseErr := njwt.GetPrivateKey()
|
||||
if privateKeyParseErr != nil && privateKey == nil {
|
||||
logger.Error("PrivateKeyParseError", privateKeyParseErr)
|
||||
}
|
||||
|
||||
publicKey, publicKeyParseErr := njwt.GetPublicKey()
|
||||
if publicKeyParseErr != nil && publicKey == nil {
|
||||
logger.Error("PublicKeyParseError", publicKeyParseErr)
|
||||
}
|
||||
|
||||
tokenAuth := jwtauth.New("RS256", privateKey, publicKey)
|
||||
return jwtauth.Verifier(tokenAuth)
|
||||
}
|
||||
|
||||
// Enforce is a authentication middleware to enforce access from the
|
||||
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
|
||||
// response for any unverified tokens and passes the good ones through.
|
||||
func Enforce(permission string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if config.IsSetup {
|
||||
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 {
|
||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if permissions exist for this user
|
||||
if permission != "" {
|
||||
// Since the permission that we require is not on the token, we have to get it from the DB
|
||||
// So we don't go crazy with hits, we will use a memory cache
|
||||
cacheKey := fmt.Sprintf("userCapabilties.%v", userID)
|
||||
cacheItem, found := AuthCache.Get(cacheKey)
|
||||
|
||||
var userCapabilities []string
|
||||
if found {
|
||||
userCapabilities = cacheItem.([]string)
|
||||
} else {
|
||||
// Get from db and store it
|
||||
userCapabilities, err = user.GetCapabilities(userID)
|
||||
if err != nil {
|
||||
AuthCacheSet(cacheKey, userCapabilities)
|
||||
}
|
||||
}
|
||||
|
||||
// Now check that they have the permission in their admin capabilities
|
||||
// full-admin can do anything
|
||||
if !util.SliceContainsItem(userCapabilities, user.CapabilityFullAdmin) && !util.SliceContainsItem(userCapabilities, permission) {
|
||||
// Access denied
|
||||
logger.Debug("User has: %+v but needs %s", userCapabilities, permission)
|
||||
h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add claims to context
|
||||
ctx = context.WithValue(ctx, c.UserIDCtxKey, userID)
|
||||
}
|
||||
|
||||
// Token is authenticated, continue as normal
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
23
backend/internal/api/middleware/auth_cache.go
Normal file
23
backend/internal/api/middleware/auth_cache.go
Normal file
@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"npm/internal/logger"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints
|
||||
var AuthCache *cache.Cache
|
||||
|
||||
// AuthCacheInit will create a new Memory Cache
|
||||
func AuthCacheInit() {
|
||||
logger.Debug("Creating a new AuthCache")
|
||||
AuthCache = cache.New(1*time.Minute, 5*time.Minute)
|
||||
}
|
||||
|
||||
// AuthCacheSet will store the item in memory for the expiration time
|
||||
func AuthCacheSet(k string, x interface{}) {
|
||||
AuthCache.Set(k, x, cache.DefaultExpiration)
|
||||
}
|
26
backend/internal/api/middleware/body_context.go
Normal file
26
backend/internal/api/middleware/body_context.go
Normal file
@ -0,0 +1,26 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// BodyContext simply adds the body data to a context item
|
||||
func BodyContext() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Grab the Body Data
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
body, _ = ioutil.ReadAll(r.Body)
|
||||
}
|
||||
// Add it to the context
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.BodyCtxKey, body)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
88
backend/internal/api/middleware/cors.go
Normal file
88
backend/internal/api/middleware/cors.go
Normal file
@ -0,0 +1,88 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
var methodMap = []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodTrace,
|
||||
}
|
||||
|
||||
func getRouteMethods(routes chi.Router, path string) []string {
|
||||
var methods []string
|
||||
tctx := chi.NewRouteContext()
|
||||
for _, method := range methodMap {
|
||||
if routes.Match(tctx, method, path) {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
var headersAllowedByCORS = []string{
|
||||
"Authorization",
|
||||
"Host",
|
||||
"Content-Type",
|
||||
"Connection",
|
||||
"User-Agent",
|
||||
"Cache-Control",
|
||||
"Accept-Encoding",
|
||||
"X-Jumbo-AppKey",
|
||||
"X-Jumbo-SKey",
|
||||
"X-Jumbo-SV",
|
||||
"X-Jumbo-Timestamp",
|
||||
"X-Jumbo-Version",
|
||||
"X-Jumbo-Customer-Id",
|
||||
}
|
||||
|
||||
// Cors handles cors headers
|
||||
func Cors(routes chi.Router) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods := getRouteMethods(routes, r.URL.Path)
|
||||
if len(methods) == 0 {
|
||||
// no route no cors
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
methods = append(methods, http.MethodOptions)
|
||||
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
|
||||
w.Header().Set("Access-Control-Allow-Headers",
|
||||
strings.Join(headersAllowedByCORS, ","),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Options handles options requests
|
||||
func Options(routes chi.Router) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods := getRouteMethods(routes, r.URL.Path)
|
||||
if len(methods) == 0 {
|
||||
// no route shouldn't have options
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, "{}")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
28
backend/internal/api/middleware/enforce_setup.go
Normal file
28
backend/internal/api/middleware/enforce_setup.go
Normal file
@ -0,0 +1,28 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/config"
|
||||
)
|
||||
|
||||
// EnforceSetup will error if the config setup doesn't match what is required
|
||||
func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.IsSetup != shouldBeSetup {
|
||||
state := "during"
|
||||
if config.IsSetup {
|
||||
state = "after"
|
||||
}
|
||||
h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// All good
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
24
backend/internal/api/middleware/expansion.go
Normal file
24
backend/internal/api/middleware/expansion.go
Normal file
@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// Expansion will determine whether the request should have objects expanded
|
||||
// with ?expand=1 or ?expand=true
|
||||
func Expansion(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
expandStr := r.URL.Query().Get("expand")
|
||||
if expandStr != "" {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.ExpansionCtxKey, strings.Split(expandStr, ","))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
115
backend/internal/api/middleware/filters.go
Normal file
115
backend/internal/api/middleware/filters.go
Normal file
@ -0,0 +1,115 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
"npm/internal/model"
|
||||
"npm/internal/util"
|
||||
"strings"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// Filters will accept a pre-defined schemaData to validate against the GET query params
|
||||
// passed in to this endpoint. This will ensure that the filters are not injecting SQL.
|
||||
// After we have determined what the Filters are to be, they are saved on the Context
|
||||
// to be used later in other endpoints.
|
||||
func Filters(schemaData string) func(http.Handler) http.Handler {
|
||||
reservedFilterKeys := []string{
|
||||
"limit",
|
||||
"offset",
|
||||
"sort",
|
||||
"order",
|
||||
"expand",
|
||||
"t", // This is used as a timestamp paramater in some clients and can be ignored
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var filters []model.Filter
|
||||
for key, val := range r.URL.Query() {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
// Split out the modifier from the field name and set a default modifier
|
||||
var keyParts []string
|
||||
keyParts = strings.Split(key, ":")
|
||||
if len(keyParts) == 1 {
|
||||
// Default modifier
|
||||
keyParts = append(keyParts, "equals")
|
||||
}
|
||||
|
||||
// Only use this filter if it's not a reserved get param
|
||||
if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) {
|
||||
for _, valItem := range val {
|
||||
// Check that the val isn't empty
|
||||
if len(strings.TrimSpace(valItem)) > 0 {
|
||||
valSlice := []string{valItem}
|
||||
if keyParts[1] == "in" || keyParts[1] == "notin" {
|
||||
valSlice = strings.Split(valItem, ",")
|
||||
}
|
||||
|
||||
filters = append(filters, model.Filter{
|
||||
Field: keyParts[0],
|
||||
Modifier: keyParts[1],
|
||||
Value: valSlice,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only validate schema if there are filters to validate
|
||||
if len(filters) > 0 {
|
||||
ctx := r.Context()
|
||||
|
||||
// Marshal the Filters in to a JSON string so that the Schema Validation works against it
|
||||
filterData, marshalErr := json.MarshalIndent(filters, "", " ")
|
||||
if marshalErr != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create root schema
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate it
|
||||
errors, jsonError := rs.ValidateBytes(ctx, filterData)
|
||||
if jsonError != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, c.FiltersCtxKey, filters)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetFiltersFromContext returns the Filters
|
||||
func GetFiltersFromContext(r *http.Request) []model.Filter {
|
||||
filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter)
|
||||
if !ok {
|
||||
// the assertion failed
|
||||
var emptyFilters []model.Filter
|
||||
return emptyFilters
|
||||
}
|
||||
return filters
|
||||
}
|
23
backend/internal/api/middleware/pretty_print.go
Normal file
23
backend/internal/api/middleware/pretty_print.go
Normal file
@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
)
|
||||
|
||||
// PrettyPrint will determine whether the request should be pretty printed in output
|
||||
// with ?pretty=1 or ?pretty=true
|
||||
func PrettyPrint(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
prettyStr := r.URL.Query().Get("pretty")
|
||||
if prettyStr == "1" || prettyStr == "true" {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
55
backend/internal/api/middleware/schema.go
Normal file
55
backend/internal/api/middleware/schema.go
Normal file
@ -0,0 +1,55 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
c "npm/internal/api/context"
|
||||
h "npm/internal/api/http"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
)
|
||||
|
||||
// CheckRequestSchema checks the payload against schema
|
||||
func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) {
|
||||
// Create root schema
|
||||
rs := &jsonschema.Schema{}
|
||||
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
|
||||
return nil, fmt.Errorf("Schema Fatal: %v", err)
|
||||
}
|
||||
|
||||
// Validate it
|
||||
schemaErrors, jsonError := rs.ValidateBytes(ctx, payload)
|
||||
if jsonError != nil {
|
||||
return nil, jsonError
|
||||
}
|
||||
|
||||
return schemaErrors, nil
|
||||
}
|
||||
|
||||
// EnforceRequestSchema accepts a schema and validates the request body against it
|
||||
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get content from context
|
||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||
|
||||
schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes)
|
||||
if err != nil {
|
||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(schemaErrors) > 0 {
|
||||
h.ResultSchemaErrorJSON(w, r, schemaErrors)
|
||||
return
|
||||
}
|
||||
|
||||
// All good
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
198
backend/internal/api/router.go
Normal file
198
backend/internal/api/router.go
Normal file
@ -0,0 +1,198 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"npm/internal/api/handler"
|
||||
"npm/internal/api/middleware"
|
||||
"npm/internal/api/schema"
|
||||
"npm/internal/config"
|
||||
"npm/internal/entity/certificate"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
"npm/internal/entity/host"
|
||||
"npm/internal/entity/hosttemplate"
|
||||
"npm/internal/entity/setting"
|
||||
"npm/internal/entity/stream"
|
||||
"npm/internal/entity/user"
|
||||
"npm/internal/logger"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
chiMiddleware "github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
// NewRouter returns a new router object
|
||||
func NewRouter() http.Handler {
|
||||
// Cors
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(
|
||||
middleware.AccessControl,
|
||||
middleware.Cors(r),
|
||||
middleware.Options(r),
|
||||
cors.Handler,
|
||||
chiMiddleware.RealIP,
|
||||
chiMiddleware.Recoverer,
|
||||
chiMiddleware.Throttle(5),
|
||||
chiMiddleware.Timeout(30*time.Second),
|
||||
middleware.PrettyPrint,
|
||||
middleware.Expansion,
|
||||
middleware.DecodeAuth(),
|
||||
middleware.BodyContext(),
|
||||
)
|
||||
|
||||
return applyRoutes(r)
|
||||
}
|
||||
|
||||
// applyRoutes is where the magic happens
|
||||
func applyRoutes(r chi.Router) chi.Router {
|
||||
middleware.AuthCacheInit()
|
||||
r.NotFound(handler.NotFound())
|
||||
r.MethodNotAllowed(handler.NotAllowed())
|
||||
|
||||
// API
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Get("/", handler.Health())
|
||||
r.Get("/schema", handler.Schema())
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce("")).
|
||||
Get("/config", handler.Config())
|
||||
|
||||
// Tokens
|
||||
r.With(middleware.EnforceSetup(true)).Route("/tokens", func(r chi.Router) {
|
||||
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
|
||||
Post("/", handler.NewToken())
|
||||
r.With(middleware.Enforce("")).
|
||||
Get("/", handler.RefreshToken())
|
||||
})
|
||||
|
||||
// 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)).
|
||||
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())
|
||||
|
||||
// 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())
|
||||
})
|
||||
|
||||
// Only available in debug mode: delete users without auth
|
||||
if config.GetLogLevel() == logger.DebugLevel {
|
||||
r.Delete("/users", handler.DeleteUsers())
|
||||
}
|
||||
|
||||
// Settings
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilitySettingsManage)).Route("/settings", func(r chi.Router) {
|
||||
r.With(middleware.Filters(setting.GetFilterSchema())).
|
||||
Get("/", handler.GetSettings())
|
||||
r.Get("/{name}", handler.GetSetting())
|
||||
r.With(middleware.EnforceRequestSchema(schema.CreateSetting())).
|
||||
Post("/", handler.CreateSetting())
|
||||
r.With(middleware.EnforceRequestSchema(schema.UpdateSetting())).
|
||||
Put("/{name}", handler.UpdateSetting())
|
||||
})
|
||||
|
||||
// DNS Providers
|
||||
r.With(middleware.EnforceSetup(true)).Route("/dns-providers", func(r chi.Router) {
|
||||
r.With(middleware.Filters(dnsprovider.GetFilterSchema()), middleware.Enforce(user.CapabilityDNSProvidersView)).
|
||||
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())
|
||||
|
||||
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityDNSProvidersView)).Route("/acmesh", func(r chi.Router) {
|
||||
r.Get("/{acmeshID:[a-z0-9_]+}", handler.GetAcmeshProvider())
|
||||
r.Get("/", handler.GetAcmeshProviders())
|
||||
})
|
||||
})
|
||||
|
||||
// Certificate Authorities
|
||||
r.With(middleware.EnforceSetup(true)).Route("/certificate-authorities", func(r chi.Router) {
|
||||
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())).
|
||||
Post("/", handler.CreateCertificateAuthority())
|
||||
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())).
|
||||
Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority())
|
||||
})
|
||||
|
||||
// Certificates
|
||||
r.With(middleware.EnforceSetup(true)).Route("/certificates", func(r chi.Router) {
|
||||
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())).
|
||||
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())
|
||||
})
|
||||
|
||||
// Hosts
|
||||
r.With(middleware.EnforceSetup(true)).Route("/hosts", func(r chi.Router) {
|
||||
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())).
|
||||
Post("/", handler.CreateHost())
|
||||
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.UpdateHost())).
|
||||
Put("/{hostID:[0-9]+}", handler.UpdateHost())
|
||||
})
|
||||
|
||||
// Host Templates
|
||||
r.With(middleware.EnforceSetup(true)).Route("/host-templates", func(r chi.Router) {
|
||||
r.With(middleware.Enforce(user.CapabilityHostTemplatesView), middleware.Filters(hosttemplate.GetFilterSchema())).
|
||||
Get("/", handler.GetHostTemplates())
|
||||
r.With(middleware.Enforce(user.CapabilityHostTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetHostTemplates())
|
||||
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteHostTemplate())
|
||||
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateHostTemplate())).
|
||||
Post("/", handler.CreateHostTemplate())
|
||||
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateHostTemplate())).
|
||||
Put("/{templateID:[0-9]+}", handler.UpdateHostTemplate())
|
||||
})
|
||||
|
||||
// Streams
|
||||
r.With(middleware.EnforceSetup(true)).Route("/streams", func(r chi.Router) {
|
||||
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())).
|
||||
Post("/", handler.CreateStream())
|
||||
r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.UpdateStream())).
|
||||
Put("/{hostID:[0-9]+}", handler.UpdateStream())
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
44
backend/internal/api/router_test.go
Normal file
44
backend/internal/api/router_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"npm/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
r = NewRouter()
|
||||
version = "3.0.0"
|
||||
commit = "abcdefgh"
|
||||
sentryDSN = ""
|
||||
)
|
||||
|
||||
// Tear up/down
|
||||
func TestMain(m *testing.M) {
|
||||
config.Init(&version, &commit, &sentryDSN)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestGetHealthz(t *testing.T) {
|
||||
respRec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/", nil)
|
||||
|
||||
r.ServeHTTP(respRec, req)
|
||||
assert.Equal(t, http.StatusOK, respRec.Code)
|
||||
assert.Contains(t, respRec.Body.String(), "healthy")
|
||||
}
|
||||
|
||||
func TestNonExistent(t *testing.T) {
|
||||
respRec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/non-existent-endpoint", nil)
|
||||
|
||||
r.ServeHTTP(respRec, req)
|
||||
assert.Equal(t, http.StatusNotFound, respRec.Code)
|
||||
assert.Equal(t, respRec.Body.String(), `{"result":null,"error":{"code":404,"message":"Not found"}}`, "404 Message should match")
|
||||
}
|
209
backend/internal/api/schema/certificates.go
Normal file
209
backend/internal/api/schema/certificates.go
Normal file
@ -0,0 +1,209 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"npm/internal/entity/certificate"
|
||||
)
|
||||
|
||||
// This validation is strictly for Custom certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateCustom() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, strictString("custom"), stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// This validation is strictly for HTTP certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateHTTP() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"certificate_authority_id",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"certificate_authority_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
},
|
||||
"is_ecc": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
}
|
||||
}
|
||||
}`, strictString("http"), intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// This validation is strictly for DNS certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateDNS() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"certificate_authority_id",
|
||||
"dns_provider_id",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"certificate_authority_id": %s,
|
||||
"dns_provider_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
},
|
||||
"is_ecc": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
}
|
||||
}
|
||||
}`, strictString("dns"), intMinOne, intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// This validation is strictly for MKCERT certificates
|
||||
// and the combination of values that must be defined
|
||||
func createCertificateMkcert() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"type": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, strictString("mkcert"), stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateHTTP() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"certificate_authority_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateDNS() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"certificate_authority_id": %s,
|
||||
"dns_provider_id": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, intMinOne, intMinOne, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateCustom() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
func updateCertificateMkcert() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}`, stringMinMax(1, 100), domainNames())
|
||||
}
|
||||
|
||||
// CreateCertificate is the schema for incoming data validation
|
||||
func CreateCertificate() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"oneOf": [%s, %s, %s, %s]
|
||||
}`, createCertificateHTTP(), createCertificateDNS(), createCertificateCustom(), createCertificateMkcert())
|
||||
}
|
||||
|
||||
// UpdateCertificate is the schema for incoming data validation
|
||||
func UpdateCertificate(certificateType string) string {
|
||||
switch certificateType {
|
||||
case certificate.TypeHTTP:
|
||||
return updateCertificateHTTP()
|
||||
case certificate.TypeDNS:
|
||||
return updateCertificateDNS()
|
||||
case certificate.TypeCustom:
|
||||
return updateCertificateCustom()
|
||||
case certificate.TypeMkcert:
|
||||
return updateCertificateMkcert()
|
||||
default:
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"oneOf": [%s, %s, %s, %s]
|
||||
}`, updateCertificateHTTP(), updateCertificateDNS(), updateCertificateCustom(), updateCertificateMkcert())
|
||||
}
|
||||
}
|
70
backend/internal/api/schema/common.go
Normal file
70
backend/internal/api/schema/common.go
Normal file
@ -0,0 +1,70 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
func strictString(value string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
}`, value)
|
||||
}
|
||||
|
||||
const intMinOne = `
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
`
|
||||
|
||||
const boolean = `
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
`
|
||||
|
||||
func stringMinMax(minLength, maxLength int) string {
|
||||
return fmt.Sprintf(`{
|
||||
"type": "string",
|
||||
"minLength": %d,
|
||||
"maxLength": %d
|
||||
}`, minLength, maxLength)
|
||||
}
|
||||
|
||||
func capabilties() string {
|
||||
return `{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
func domainNames() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": %s
|
||||
}`, stringMinMax(4, 255))
|
||||
}
|
||||
|
||||
const anyType = `
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
25
backend/internal/api/schema/create_certificate_authority.go
Normal file
25
backend/internal/api/schema/create_certificate_authority.go
Normal file
@ -0,0 +1,25 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateCertificateAuthority is the schema for incoming data validation
|
||||
func CreateCertificateAuthority() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"acmesh_server",
|
||||
"max_domains"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"acmesh_server": %s,
|
||||
"max_domains": %s,
|
||||
"ca_bundle": %s,
|
||||
"is_wildcard_supported": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean)
|
||||
}
|
51
backend/internal/api/schema/create_dns_provider.go
Normal file
51
backend/internal/api/schema/create_dns_provider.go
Normal file
@ -0,0 +1,51 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"npm/internal/dnsproviders"
|
||||
"npm/internal/util"
|
||||
)
|
||||
|
||||
// CreateDNSProvider is the schema for incoming data validation
|
||||
func CreateDNSProvider() string {
|
||||
allProviders := dnsproviders.GetAll()
|
||||
fmtStr := fmt.Sprintf(`{"oneOf": [%s]}`, strings.TrimRight(strings.Repeat("\n%s,", len(allProviders)), ","))
|
||||
|
||||
allSchemasWrapped := make([]string, 0)
|
||||
for providerName, provider := range allProviders {
|
||||
allSchemasWrapped = append(allSchemasWrapped, createDNSProviderType(providerName, provider.Schema))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(fmtStr, util.ConvertStringSliceToInterface(allSchemasWrapped)...)
|
||||
}
|
||||
|
||||
func createDNSProviderType(name, metaSchema string) string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"acmesh_name",
|
||||
"name",
|
||||
"meta"
|
||||
],
|
||||
"properties": {
|
||||
"acmesh_name": {
|
||||
"type": "string",
|
||||
"pattern": "^%s$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"dns_sleep": {
|
||||
"type": "integer"
|
||||
},
|
||||
"meta": %s
|
||||
}
|
||||
}
|
||||
`, name, metaSchema)
|
||||
}
|
80
backend/internal/api/schema/create_host.go
Normal file
80
backend/internal/api/schema/create_host.go
Normal file
@ -0,0 +1,80 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateHost is the schema for incoming data validation
|
||||
// This schema supports 3 possible types with different data combinations:
|
||||
// - proxy
|
||||
// - redirection
|
||||
// - dead
|
||||
func CreateHost() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"domain_names",
|
||||
"host_template_id"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^proxy$"
|
||||
},
|
||||
"host_template_id": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"listen_interface": %s,
|
||||
"domain_names": %s,
|
||||
"upstream_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"certificate_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"access_list_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ssl_forced": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"caching_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"block_exploits": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allow_websocket_upgrade": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"http2_support": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hsts_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hsts_subdomains": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"paths": {
|
||||
"type": "string"
|
||||
},
|
||||
"upstream_options": {
|
||||
"type": "string"
|
||||
},
|
||||
"advanced_config": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`, stringMinMax(0, 255), domainNames())
|
||||
}
|
30
backend/internal/api/schema/create_host_template.go
Normal file
30
backend/internal/api/schema/create_host_template.go
Normal file
@ -0,0 +1,30 @@
|
||||
package schema
|
||||
|
||||
// CreateHostTemplate is the schema for incoming data validation
|
||||
func CreateHostTemplate() string {
|
||||
return `
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"host_type",
|
||||
"template"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"host_type": {
|
||||
"type": "string",
|
||||
"pattern": "^proxy|redirect|dead|stream$"
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"minLength": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
21
backend/internal/api/schema/create_setting.go
Normal file
21
backend/internal/api/schema/create_setting.go
Normal file
@ -0,0 +1,21 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateSetting is the schema for incoming data validation
|
||||
func CreateSetting() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"value": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), anyType)
|
||||
}
|
27
backend/internal/api/schema/create_stream.go
Normal file
27
backend/internal/api/schema/create_stream.go
Normal file
@ -0,0 +1,27 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateStream is the schema for incoming data validation
|
||||
func CreateStream() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"provider",
|
||||
"name",
|
||||
"domain_names"
|
||||
],
|
||||
"properties": {
|
||||
"provider": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"expires_on": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
|
||||
}
|
42
backend/internal/api/schema/create_user.go
Normal file
42
backend/internal/api/schema/create_user.go
Normal file
@ -0,0 +1,42 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateUser is the schema for incoming data validation
|
||||
func CreateUser() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"is_disabled",
|
||||
"capabilities"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"nickname": %s,
|
||||
"email": %s,
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^password$"
|
||||
},
|
||||
"secret": %s
|
||||
}
|
||||
},
|
||||
"capabilities": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), stringMinMax(8, 255), capabilties())
|
||||
}
|
28
backend/internal/api/schema/get_token.go
Normal file
28
backend/internal/api/schema/get_token.go
Normal file
@ -0,0 +1,28 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GetToken is the schema for incoming data validation
|
||||
// nolint: gosec
|
||||
func GetToken() string {
|
||||
stdField := stringMinMax(1, 255)
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"identity",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^password$"
|
||||
},
|
||||
"identity": %s,
|
||||
"secret": %s
|
||||
}
|
||||
}
|
||||
`, stdField, stdField)
|
||||
}
|
25
backend/internal/api/schema/set_auth.go
Normal file
25
backend/internal/api/schema/set_auth.go
Normal file
@ -0,0 +1,25 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SetAuth is the schema for incoming data validation
|
||||
func SetAuth() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^password$"
|
||||
},
|
||||
"secret": %s,
|
||||
"current_secret": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(8, 225), stringMinMax(8, 225))
|
||||
}
|
21
backend/internal/api/schema/update_certificate_authority.go
Normal file
21
backend/internal/api/schema/update_certificate_authority.go
Normal file
@ -0,0 +1,21 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateCertificateAuthority is the schema for incoming data validation
|
||||
func UpdateCertificateAuthority() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"acmesh_server": %s,
|
||||
"max_domains": %s,
|
||||
"ca_bundle": %s,
|
||||
"is_wildcard_supported": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean)
|
||||
}
|
20
backend/internal/api/schema/update_dns_provider.go
Normal file
20
backend/internal/api/schema/update_dns_provider.go
Normal file
@ -0,0 +1,20 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateDNSProvider is the schema for incoming data validation
|
||||
func UpdateDNSProvider() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100))
|
||||
}
|
27
backend/internal/api/schema/update_host.go
Normal file
27
backend/internal/api/schema/update_host.go
Normal file
@ -0,0 +1,27 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateHost is the schema for incoming data validation
|
||||
func UpdateHost() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"host_template_id": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"provider": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"expires_on": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
|
||||
}
|
22
backend/internal/api/schema/update_host_template.go
Normal file
22
backend/internal/api/schema/update_host_template.go
Normal file
@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
// UpdateHostTemplate is the schema for incoming data validation
|
||||
func UpdateHostTemplate() string {
|
||||
return `
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"minLength": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
17
backend/internal/api/schema/update_setting.go
Normal file
17
backend/internal/api/schema/update_setting.go
Normal file
@ -0,0 +1,17 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateSetting is the schema for incoming data validation
|
||||
func UpdateSetting() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"value": %s
|
||||
}
|
||||
}
|
||||
`, anyType)
|
||||
}
|
23
backend/internal/api/schema/update_stream.go
Normal file
23
backend/internal/api/schema/update_stream.go
Normal file
@ -0,0 +1,23 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateStream is the schema for incoming data validation
|
||||
func UpdateStream() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"provider": %s,
|
||||
"name": %s,
|
||||
"domain_names": %s,
|
||||
"expires_on": %s,
|
||||
"meta": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
|
||||
}
|
23
backend/internal/api/schema/update_user.go
Normal file
23
backend/internal/api/schema/update_user.go
Normal file
@ -0,0 +1,23 @@
|
||||
package schema
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UpdateUser is the schema for incoming data validation
|
||||
func UpdateUser() string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"nickname": %s,
|
||||
"email": %s,
|
||||
"is_disabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"capabilities": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), capabilties())
|
||||
}
|
19
backend/internal/api/server.go
Normal file
19
backend/internal/api/server.go
Normal file
@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"npm/internal/logger"
|
||||
)
|
||||
|
||||
const httpPort = 3000
|
||||
|
||||
// StartServer creates a http server
|
||||
func StartServer() {
|
||||
logger.Info("Server starting on port %v", httpPort)
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%v", httpPort), NewRouter())
|
||||
if err != nil {
|
||||
logger.Error("HttpListenError", err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user