Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager

This commit is contained in:
Jamie Curnow
2022-05-12 08:47:31 +10:00
parent 4db34f5894
commit 2110ecc382
830 changed files with 38168 additions and 36635 deletions

View File

@ -0,0 +1,82 @@
package auth
import (
goerrors "errors"
"fmt"
"npm/internal/database"
)
// GetByID finds a auth by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// GetByUserIDType finds a user by email
func GetByUserIDType(userID int, authType string) (Model, error) {
var m Model
err := m.LoadByUserIDType(userID, authType)
return m, err
}
// Create will create a Auth from this model
func Create(auth *Model) (int, error) {
if auth.ID != 0 {
return 0, goerrors.New("Cannot create auth when model already has an ID")
}
auth.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
type,
secret,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:type,
:secret,
:is_deleted
)`, auth)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Auth from this model
func Update(auth *Model) error {
if auth.ID == 0 {
return goerrors.New("Cannot update auth when model doesn't have an ID")
}
auth.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
user_id = :user_id,
type = :type,
secret = :secret,
is_deleted = :is_deleted
WHERE id = :id`, auth)
return err
}

View File

@ -0,0 +1,98 @@
package auth
import (
goerrors "errors"
"fmt"
"time"
"npm/internal/database"
"npm/internal/types"
"golang.org/x/crypto/bcrypt"
)
const (
tableName = "auth"
// TypePassword is the Password Type
TypePassword = "password"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id"`
UserID int `json:"user_id" db:"user_id"`
Type string `json:"type" db:"type"`
Secret string `json:"secret,omitempty" db:"secret"`
CreatedOn types.DBDate `json:"created_on" db:"created_on"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", tableName)
params := []interface{}{id}
return m.getByQuery(query, params)
}
// LoadByUserIDType will load from an ID
func (m *Model) LoadByUserIDType(userID int, authType string) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE user_id = ? AND type = ? LIMIT 1", tableName)
params := []interface{}{userID, authType}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// SetPassword will generate a hashed password based on given string
func (m *Model) SetPassword(password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost+2)
if err != nil {
return err
}
m.Type = TypePassword
m.Secret = string(hash)
return nil
}
// ValidateSecret will check if a given secret matches the encrypted secret
func (m *Model) ValidateSecret(secret string) error {
if m.Type != TypePassword {
return goerrors.New("Could not validate Secret, auth type is not a Password")
}
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
if err != nil {
return goerrors.New("Invalid Password")
}
return nil
}

View File

@ -0,0 +1,25 @@
package certificate
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,174 @@
package certificate
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a row by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// Create will create a row from this model
func Create(certificate *Model) (int, error) {
if certificate.ID != 0 {
return 0, goerrors.New("Cannot create certificate when model already has an ID")
}
certificate.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
type,
certificate_authority_id,
dns_provider_id,
name,
domain_names,
expires_on,
status,
meta,
is_ecc,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:type,
:certificate_authority_id,
:dns_provider_id,
:name,
:domain_names,
:expires_on,
:status,
:meta,
:is_ecc,
:is_deleted
)`, certificate)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Auth from this model
func Update(certificate *Model) error {
if certificate.ID == 0 {
return goerrors.New("Cannot update certificate when model doesn't have an ID")
}
certificate.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
type = :type,
user_id = :user_id,
certificate_authority_id = :certificate_authority_id,
dns_provider_id = :dns_provider_id,
name = :name,
domain_names = :domain_names,
expires_on = :expires_on,
status = :status,
meta = :meta,
is_ecc = :is_ecc,
is_deleted = :is_deleted
WHERE id = :id`, certificate)
return err
}
// List will return a list of certificates
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("%s -- %+v", query, params)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}
// GetByStatus will select rows that are ready for requesting
func GetByStatus(status string) ([]Model, error) {
models := make([]Model, 0)
db := database.GetInstance()
query := fmt.Sprintf(`
SELECT
t.*
FROM "%s" t
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
WHERE
t."type" IN ("http", "dns") AND
t."status" = ? AND
t."certificate_authority_id" > 0 AND
t."is_deleted" = 0
`, tableName)
params := []interface{}{StatusReady}
err := db.Select(&models, query, params...)
if err != nil && err != sql.ErrNoRows {
logger.Error("GetByStatusError", err)
logger.Debug("Query: %s -- %+v", query, params)
}
return models, err
}

View File

@ -0,0 +1,266 @@
package certificate
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"time"
"npm/internal/acme"
"npm/internal/config"
"npm/internal/database"
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"npm/internal/logger"
"npm/internal/types"
)
const (
tableName = "certificate"
// TypeCustom custom cert type
TypeCustom = "custom"
// TypeHTTP http cert type
TypeHTTP = "http"
// TypeDNS dns cert type
TypeDNS = "dns"
// TypeMkcert mkcert cert type
TypeMkcert = "mkcert"
// StatusReady is ready for certificate to be requested
StatusReady = "ready"
// StatusRequesting is process of being requested
StatusRequesting = "requesting"
// StatusFailed is a certicifate that failed to request
StatusFailed = "failed"
// StatusProvided is a certificate provided and ready for actual use
StatusProvided = "provided"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
ExpiresOn types.NullableDBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"`
Type string `json:"type" db:"type" filter:"type,string"`
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
CertificateAuthorityID int `json:"certificate_authority_id" db:"certificate_authority_id" filter:"certificate_authority_id,integer"`
DNSProviderID int `json:"dns_provider_id" db:"dns_provider_id" filter:"dns_provider_id,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
Status string `json:"status" db:"status" filter:"status,string"`
ErrorMessage string `json:"error_message" db:"error_message" filter:"error_message,string"`
Meta types.JSONB `json:"-" db:"meta"`
IsECC int `json:"is_ecc" db:"is_ecc" filter:"is_ecc,integer"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
// Expansions:
CertificateAuthority *certificateauthority.Model `json:"certificate_authority,omitempty"`
DNSProvider *dnsprovider.Model `json:"dns_provider,omitempty"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, 0}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.UserID == 0 {
return fmt.Errorf("User ID must be specified")
}
if !m.Validate() {
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
}
if !m.ValidateWildcardSupport() {
return fmt.Errorf("Cannot use Wildcard domains with this CA")
}
m.setDefaultStatus()
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// Delete will mark a certificate as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}
// Validate will make sure the data given is expected. This object is a bit complicated,
// as there could be multiple combinations of values.
func (m *Model) Validate() bool {
switch m.Type {
case TypeCustom:
// TODO: make sure meta contains required fields
return m.DNSProviderID == 0 && m.CertificateAuthorityID == 0
case TypeHTTP:
return m.DNSProviderID == 0 && m.CertificateAuthorityID > 0
case TypeDNS:
return m.DNSProviderID > 0 && m.CertificateAuthorityID > 0
case TypeMkcert:
return true
default:
return false
}
}
// ValidateWildcardSupport will ensure that the CA given supports wildcards,
// only if the domains on this object have at least 1 wildcard
func (m *Model) ValidateWildcardSupport() bool {
domains, err := m.DomainNames.AsStringArray()
if err != nil {
logger.Error("ValidateWildcardSupportError", err)
return false
}
hasWildcard := false
for _, domain := range domains {
if strings.Contains(domain, "*") {
hasWildcard = true
}
}
if hasWildcard {
m.Expand()
if !m.CertificateAuthority.IsWildcardSupported {
return false
}
}
return true
}
func (m *Model) setDefaultStatus() {
if m.ID == 0 {
// It's a new certificate
if m.Type == TypeCustom {
m.Status = StatusProvided
} else {
m.Status = StatusReady
}
}
}
// Expand will populate attached objects for the model
func (m *Model) Expand() {
if m.CertificateAuthorityID > 0 {
certificateAuthority, _ := certificateauthority.GetByID(m.CertificateAuthorityID)
m.CertificateAuthority = &certificateAuthority
}
if m.DNSProviderID > 0 {
dnsProvider, _ := dnsprovider.GetByID(m.DNSProviderID)
m.DNSProvider = &dnsProvider
}
}
// GetCertificateLocations will return the paths on disk where the SSL
// certs should or would be.
// Returns: (key, fullchain, certFolder)
func (m *Model) GetCertificateLocations() (string, string, string) {
if m.ID == 0 {
logger.Error("GetCertificateLocationsError", errors.New("GetCertificateLocations called before certificate was saved"))
return "", "", ""
}
certFolder := fmt.Sprintf("%s/certificates", config.Configuration.DataFolder)
// Generate a unique folder name for this cert
m1 := regexp.MustCompile(`[^A-Za-z0-9\.]`)
niceName := m1.ReplaceAllString(m.Name, "_")
if len(niceName) > 20 {
niceName = niceName[:20]
}
folderName := fmt.Sprintf("%d-%s", m.ID, niceName)
return fmt.Sprintf("%s/%s/key.pem", certFolder, folderName),
fmt.Sprintf("%s/%s/fullchain.pem", certFolder, folderName),
fmt.Sprintf("%s/%s", certFolder, folderName)
}
// Request makes a certificate request
func (m *Model) Request() error {
logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name)
m.Expand()
m.Status = StatusRequesting
if err := m.Save(); err != nil {
logger.Error("CertificateSaveError", err)
return err
}
// do request
domains, err := m.DomainNames.AsStringArray()
if err != nil {
logger.Error("CertificateRequestError", err)
return err
}
certKeyFile, certFullchainFile, certFolder := m.GetCertificateLocations()
// ensure certFolder is created
if err := os.MkdirAll(certFolder, os.ModePerm); err != nil {
logger.Error("CreateFolderError", err)
return err
}
errMsg, err := acme.RequestCert(domains, m.Type, certFullchainFile, certKeyFile, m.DNSProvider, m.CertificateAuthority, true)
if err != nil {
m.Status = StatusFailed
m.ErrorMessage = errMsg
if err := m.Save(); err != nil {
logger.Error("CertificateSaveError", err)
return err
}
return nil
}
// If done
m.Status = StatusProvided
t := time.Now()
m.ExpiresOn.Time = &t // todo
if err := m.Save(); err != nil {
logger.Error("CertificateSaveError", err)
return err
}
logger.Info("Request for certificate for: #%d %v was completed", m.ID, m.Name)
return nil
}

View File

@ -0,0 +1,15 @@
package certificate
import (
"npm/internal/model"
)
// ListResponse is the JSON response for users list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,25 @@
package certificateauthority
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,134 @@
package certificateauthority
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a row by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// Create will create a row from this model
func Create(ca *Model) (int, error) {
if ca.ID != 0 {
return 0, goerrors.New("Cannot create certificate authority when model already has an ID")
}
ca.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
name,
acmesh_server,
ca_bundle,
max_domains,
is_wildcard_supported,
is_deleted
) VALUES (
:created_on,
:modified_on,
:name,
:acmesh_server,
:ca_bundle,
:max_domains,
:is_wildcard_supported,
:is_deleted
)`, ca)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a row from this model
func Update(ca *Model) error {
if ca.ID == 0 {
return goerrors.New("Cannot update certificate authority when model doesn't have an ID")
}
ca.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
name = :name,
acmesh_server = :acmesh_server,
ca_bundle = :ca_bundle,
max_domains = :max_domains,
is_wildcard_supported = :is_wildcard_supported,
is_deleted = :is_deleted
WHERE id = :id`, ca)
return err
}
// List will return a list of certificates
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Error("ListCertificateAuthoritiesError", queryErr)
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Error("ListCertificateAuthoritiesError", err)
logger.Debug("%s -- %+v", query, params)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}

View File

@ -0,0 +1,88 @@
package certificateauthority
import (
goerrors "errors"
"fmt"
"os"
"path/filepath"
"time"
"npm/internal/database"
"npm/internal/errors"
"npm/internal/types"
)
const (
tableName = "certificate_authority"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
AcmeshServer string `json:"acmesh_server" db:"acmesh_server" filter:"acmesh_server,string"`
CABundle string `json:"ca_bundle" db:"ca_bundle" filter:"ca_bundle,string"`
MaxDomains int `json:"max_domains" db:"max_domains" filter:"max_domains,integer"`
IsWildcardSupported bool `json:"is_wildcard_supported" db:"is_wildcard_supported" filter:"is_wildcard_supported,boolean"`
IsReadonly bool `json:"is_readonly" db:"is_readonly" filter:"is_readonly,boolean"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, 0}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// Delete will mark a certificate as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}
// Check will ensure the ca bundle path exists if it's set
func (m *Model) Check() error {
var err error
if m.CABundle != "" {
if _, fileerr := os.Stat(filepath.Clean(m.CABundle)); goerrors.Is(fileerr, os.ErrNotExist) {
err = errors.ErrCABundleDoesNotExist
}
}
return err
}

View File

@ -0,0 +1,15 @@
package certificateauthority
import (
"npm/internal/model"
)
// ListResponse is the JSON response for users list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,25 @@
package dnsprovider
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,134 @@
package dnsprovider
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a row by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// Create will create a row from this model
func Create(provider *Model) (int, error) {
if provider.ID != 0 {
return 0, goerrors.New("Cannot create dns provider when model already has an ID")
}
provider.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
name,
acmesh_name,
dns_sleep,
meta,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:name,
:acmesh_name,
:dns_sleep,
:meta,
:is_deleted
)`, provider)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a row from this model
func Update(provider *Model) error {
if provider.ID == 0 {
return goerrors.New("Cannot update dns provider when model doesn't have an ID")
}
provider.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
user_id = :user_id,
name = :name,
acmesh_name = :acmesh_name,
dns_sleep = :dns_sleep,
meta = :meta,
is_deleted = :is_deleted
WHERE id = :id`, provider)
return err
}
// List will return a list of certificates
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Error("ListDnsProvidersError", queryErr)
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Error("ListDnsProvidersError", err)
logger.Debug("%s -- %+v", query, params)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}

View File

@ -0,0 +1,101 @@
package dnsprovider
import (
"fmt"
"time"
"npm/internal/database"
"npm/internal/dnsproviders"
"npm/internal/logger"
"npm/internal/types"
)
const (
tableName = "dns_provider"
)
// Model is the user model
// Also see: https://github.com/acmesh-official/acme.sh/wiki/dnscheck
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
AcmeshName string `json:"acmesh_name" db:"acmesh_name" filter:"acmesh_name,string"`
DNSSleep int `json:"dns_sleep" db:"dns_sleep" filter:"dns_sleep,integer"`
Meta types.JSONB `json:"meta" db:"meta"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, 0}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.UserID == 0 {
return fmt.Errorf("User ID must be specified")
}
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// Delete will mark a certificate as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}
// GetAcmeShEnvVars returns the env vars required for acme.sh dns cert requests
func (m *Model) GetAcmeShEnvVars() ([]string, error) {
logger.Debug("GetAcmeShEnvVars for: %s", m.AcmeshName)
// First, fetch the provider obj with this AcmeShName
acmeDNSProvider, err := dnsproviders.Get(m.AcmeshName)
logger.Debug("acmeDNSProvider: %+v", acmeDNSProvider)
if err != nil {
logger.Error("GetAcmeShEnvVarsError", err)
return nil, err
}
envs := make([]string, 0)
// Then, using the meta, convert to env vars
envPairs := acmeDNSProvider.GetAcmeEnvVars(m.Meta.Decoded)
logger.Debug("meta: %+v", m.Meta)
logger.Debug("envPairs: %+v", envPairs)
for envName, envValue := range envPairs {
envs = append(envs, fmt.Sprintf(`%s=%v`, envName, envValue))
}
return envs, nil
}

View File

@ -0,0 +1,82 @@
package dnsprovider
import (
"encoding/json"
"npm/internal/types"
"testing"
"github.com/stretchr/testify/assert"
)
func TestModelGetAcmeShEnvVars(t *testing.T) {
type want struct {
envs []string
err error
}
tests := []struct {
name string
dnsProvider Model
metaJSON string
want want
}{
{
name: "dns_aws",
dnsProvider: Model{
AcmeshName: "dns_aws",
},
metaJSON: `{"access_key_id":"sdfsdfsdfljlbjkljlkjsdfoiwje","access_key":"xxxxxxx"}`,
want: want{
envs: []string{
`AWS_ACCESS_KEY_ID=sdfsdfsdfljlbjkljlkjsdfoiwje`,
`AWS_SECRET_ACCESS_KEY=xxxxxxx`,
},
err: nil,
},
},
{
name: "dns_cf",
dnsProvider: Model{
AcmeshName: "dns_cf",
},
metaJSON: `{"api_key":"sdfsdfsdfljlbjkljlkjsdfoiwje","email":"me@example.com","token":"dkfjghdk","account_id":"hgbdjfg","zone_id":"ASDASD"}`,
want: want{
envs: []string{
`CF_Token=dkfjghdk`,
`CF_Account_ID=hgbdjfg`,
`CF_Zone_ID=ASDASD`,
`CF_Key=sdfsdfsdfljlbjkljlkjsdfoiwje`,
`CF_Email=me@example.com`,
},
err: nil,
},
},
{
name: "dns_duckdns",
dnsProvider: Model{
AcmeshName: "dns_duckdns",
},
metaJSON: `{"api_key":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}`,
want: want{
envs: []string{
`DuckDNS_Token=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`,
},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var meta types.JSONB
err := json.Unmarshal([]byte(tt.metaJSON), &meta.Decoded)
assert.Equal(t, nil, err)
tt.dnsProvider.Meta = meta
envs, err := tt.dnsProvider.GetAcmeShEnvVars()
assert.Equal(t, tt.want.err, err)
for _, i := range tt.want.envs {
assert.Contains(t, envs, i)
}
})
}
}

View File

@ -0,0 +1,15 @@
package dnsprovider
import (
"npm/internal/model"
)
// ListResponse is the JSON response for the list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,158 @@
package entity
import (
"fmt"
"reflect"
"strings"
"npm/internal/model"
)
// FilterMapFunction is a filter map function
type FilterMapFunction func(value []string) []string
// FilterTagName tag name user for filter pickups
const FilterTagName = "filter"
// DBTagName tag name user for field name pickups
const DBTagName = "db"
// GenerateSQLFromFilters will return a Query and params for use as WHERE clause in SQL queries
// This will use a AND where clause approach.
func GenerateSQLFromFilters(filters []model.Filter, fieldMap map[string]string, fieldMapFunctions map[string]FilterMapFunction) (string, []interface{}) {
clauses := make([]string, 0)
params := make([]interface{}, 0)
for _, filter := range filters {
// Lookup this filter field from the functions map
if _, ok := fieldMapFunctions[filter.Field]; ok {
filter.Value = fieldMapFunctions[filter.Field](filter.Value)
}
// Lookup this filter field from the name map
if _, ok := fieldMap[filter.Field]; ok {
filter.Field = fieldMap[filter.Field]
}
// Special case for LIKE queries, the column needs to be uppercase for comparison
fieldName := fmt.Sprintf("`%s`", filter.Field)
if strings.ToLower(filter.Modifier) == "contains" || strings.ToLower(filter.Modifier) == "starts" || strings.ToLower(filter.Modifier) == "ends" {
fieldName = fmt.Sprintf("UPPER(`%s`)", filter.Field)
}
clauses = append(clauses, fmt.Sprintf("%s %s", fieldName, getSQLAssignmentFromModifier(filter, &params)))
}
return strings.Join(clauses, " AND "), params
}
func getSQLAssignmentFromModifier(filter model.Filter, params *[]interface{}) string {
var clause string
// Quick hacks
if filter.Modifier == "in" && len(filter.Value) == 1 {
filter.Modifier = "equals"
} else if filter.Modifier == "notin" && len(filter.Value) == 1 {
filter.Modifier = "not"
}
switch strings.ToLower(filter.Modifier) {
default:
clause = "= ?"
case "not":
clause = "!= ?"
case "min":
clause = ">= ?"
case "max":
clause = "<= ?"
case "greater":
clause = "> ?"
case "lesser":
clause = "< ?"
// LIKE modifiers:
case "contains":
*params = append(*params, strings.ToUpper(filter.Value[0]))
return "LIKE '%' || ? || '%'"
case "starts":
*params = append(*params, strings.ToUpper(filter.Value[0]))
return "LIKE ? || '%'"
case "ends":
*params = append(*params, strings.ToUpper(filter.Value[0]))
return "LIKE '%' || ?"
// Array parameter modifiers:
case "in":
s, p := buildInArray(filter.Value)
*params = append(*params, p...)
return fmt.Sprintf("IN (%s)", s)
case "notin":
s, p := buildInArray(filter.Value)
*params = append(*params, p...)
return fmt.Sprintf("NOT IN (%s)", s)
}
*params = append(*params, filter.Value[0])
return clause
}
// GetFilterMap returns the filter map
func GetFilterMap(m interface{}) map[string]string {
var filterMap = make(map[string]string)
// TypeOf returns the reflection Type that represents the dynamic type of variable.
// If variable is a nil interface value, TypeOf returns nil.
t := reflect.TypeOf(m)
// Iterate over all available fields and read the tag value
for i := 0; i < t.NumField(); i++ {
// Get the field, returns https://golang.org/pkg/reflect/#StructField
field := t.Field(i)
// Get the field tag value
filterTag := field.Tag.Get(FilterTagName)
dbTag := field.Tag.Get(DBTagName)
if filterTag != "" && dbTag != "" && dbTag != "-" && filterTag != "-" {
// Filter tag can be a 2 part thing: name,type
// ie: account_id,integer
// So we need to split and use the first part
parts := strings.Split(filterTag, ",")
filterMap[parts[0]] = dbTag
filterMap[filterTag] = dbTag
}
}
return filterMap
}
// GetDBColumns returns the db columns
func GetDBColumns(m interface{}) []string {
var columns []string
t := reflect.TypeOf(m)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
dbTag := field.Tag.Get(DBTagName)
if dbTag != "" && dbTag != "-" {
columns = append(columns, dbTag)
}
}
return columns
}
func buildInArray(items []string) (string, []interface{}) {
// Query string placeholder
strs := make([]string, len(items))
for i := 0; i < len(items); i++ {
strs[i] = "?"
}
// Params as interface
params := make([]interface{}, len(items))
for i, v := range items {
params[i] = v
}
return strings.Join(strs, ", "), params
}

View File

@ -0,0 +1,223 @@
package entity
import (
"fmt"
"reflect"
"strings"
)
// GetFilterSchema creates a jsonschema for validating filters, based on the model
// object given and by reading the struct "filter" tags.
func GetFilterSchema(m interface{}) string {
var schemas []string
t := reflect.TypeOf(m)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
filterTag := field.Tag.Get(FilterTagName)
if filterTag != "" && filterTag != "-" {
// split out tag value "field,filtreType"
// with a default filter type of string
items := strings.Split(filterTag, ",")
if len(items) == 1 {
items = append(items, "string")
}
switch items[1] {
case "int":
fallthrough
case "integer":
schemas = append(schemas, intFieldSchema(items[0]))
case "bool":
fallthrough
case "boolean":
schemas = append(schemas, boolFieldSchema(items[0]))
case "date":
schemas = append(schemas, dateFieldSchema(items[0]))
case "regex":
if len(items) < 3 {
items = append(items, ".*")
}
schemas = append(schemas, regexFieldSchema(items[0], items[2]))
default:
schemas = append(schemas, stringFieldSchema(items[0]))
}
}
}
return newFilterSchema(schemas)
}
// 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)
}
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
]
}
}`

View File

@ -0,0 +1,25 @@
package host
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,183 @@
package host
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a Host by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// create will create a Host from this model
func create(host *Model) (int, error) {
if host.ID != 0 {
return 0, goerrors.New("Cannot create host when model already has an ID")
}
host.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
type,
host_template_id,
listen_interface,
domain_names,
upstream_id,
certificate_id,
access_list_id,
ssl_forced,
caching_enabled,
block_exploits,
allow_websocket_upgrade,
http2_support,
hsts_enabled,
hsts_subdomains,
paths,
upstream_options,
advanced_config,
is_disabled,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:type,
:host_template_id,
:listen_interface,
:domain_names,
:upstream_id,
:certificate_id,
:access_list_id,
:ssl_forced,
:caching_enabled,
:block_exploits,
:allow_websocket_upgrade,
:http2_support,
:hsts_enabled,
:hsts_subdomains,
:paths,
:upstream_options,
:advanced_config,
:is_disabled,
:is_deleted
)`, host)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// update will Update a Host from this model
func update(host *Model) error {
if host.ID == 0 {
return goerrors.New("Cannot update host when model doesn't have an ID")
}
host.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
user_id = :user_id,
type = :type,
host_template_id = :host_template_id,
listen_interface = :listen_interface,
domain_names = :domain_names,
upstream_id = :upstream_id,
certificate_id = :certificate_id,
access_list_id = :access_list_id,
ssl_forced = :ssl_forced,
caching_enabled = :caching_enabled,
block_exploits = :block_exploits,
allow_websocket_upgrade = :allow_websocket_upgrade,
http2_support = :http2_support,
hsts_enabled = :hsts_enabled,
hsts_subdomains = :hsts_subdomains,
paths = :paths,
upstream_options = :upstream_options,
advanced_config = :advanced_config,
is_disabled = :is_disabled,
is_deleted = :is_deleted
WHERE id = :id`, host)
return err
}
// List will return a list of hosts
func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "domain_names",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("%s -- %+v", query, params)
return result, err
}
if expand != nil {
for idx := range items {
expandErr := items[idx].Expand(expand)
if expandErr != nil {
logger.Error("HostsExpansionError", expandErr)
}
}
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}

View File

@ -0,0 +1,120 @@
package host
import (
"fmt"
"time"
"npm/internal/database"
"npm/internal/entity/certificate"
"npm/internal/entity/user"
"npm/internal/types"
"npm/internal/util"
)
const (
tableName = "host"
// ProxyHostType is self explanatory
ProxyHostType = "proxy"
// RedirectionHostType is self explanatory
RedirectionHostType = "redirection"
// DeadHostType is self explanatory
DeadHostType = "dead"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
Type string `json:"type" db:"type" filter:"type,string"`
HostTemplateID int `json:"host_template_id" db:"host_template_id" filter:"host_template_id,integer"`
ListenInterface string `json:"listen_interface" db:"listen_interface" filter:"listen_interface,string"`
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
UpstreamID int `json:"upstream_id" db:"upstream_id" filter:"upstream_id,integer"`
CertificateID int `json:"certificate_id" db:"certificate_id" filter:"certificate_id,integer"`
AccessListID int `json:"access_list_id" db:"access_list_id" filter:"access_list_id,integer"`
SSLForced bool `json:"ssl_forced" db:"ssl_forced" filter:"ssl_forced,boolean"`
CachingEnabled bool `json:"caching_enabled" db:"caching_enabled" filter:"caching_enabled,boolean"`
BlockExploits bool `json:"block_exploits" db:"block_exploits" filter:"block_exploits,boolean"`
AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" db:"allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"`
HTTP2Support bool `json:"http2_support" db:"http2_support" filter:"http2_support,boolean"`
HSTSEnabled bool `json:"hsts_enabled" db:"hsts_enabled" filter:"hsts_enabled,boolean"`
HSTSSubdomains bool `json:"hsts_subdomains" db:"hsts_subdomains" filter:"hsts_subdomains,boolean"`
Paths string `json:"paths" db:"paths" filter:"paths,string"`
UpstreamOptions string `json:"upstream_options" db:"upstream_options" filter:"upstream_options,string"`
AdvancedConfig string `json:"advanced_config" db:"advanced_config" filter:"advanced_config,string"`
IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
// Expansions
Certificate *certificate.Model `json:"certificate,omitempty"`
User *user.Model `json:"user,omitempty"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, 0}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.UserID == 0 {
return fmt.Errorf("User ID must be specified")
}
if m.ID == 0 {
m.ID, err = create(m)
} else {
err = update(m)
}
return err
}
// Delete will mark a host as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}
// Expand will fill in more properties
func (m *Model) Expand(items []string) error {
var err error
if util.SliceContainsItem(items, "user") && m.ID > 0 {
var usr user.Model
usr, err = user.GetByID(m.UserID)
m.User = &usr
}
if util.SliceContainsItem(items, "certificate") && m.CertificateID > 0 {
var cert certificate.Model
cert, err = certificate.GetByID(m.CertificateID)
m.Certificate = &cert
}
return err
}

View File

@ -0,0 +1,15 @@
package host
import (
"npm/internal/model"
)
// ListResponse is the JSON response for this list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,25 @@
package hosttemplate
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,129 @@
package hosttemplate
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a Host by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// Create will create a Host from this model
func Create(host *Model) (int, error) {
if host.ID != 0 {
return 0, goerrors.New("Cannot create host template when model already has an ID")
}
host.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
name,
host_type,
template,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:name,
:host_type,
:template,
:is_deleted
)`, host)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Host from this model
func Update(host *Model) error {
if host.ID == 0 {
return goerrors.New("Cannot update host template when model doesn't have an ID")
}
host.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
user_id = :user_id,
name = :name,
host_type = :host_type,
template = :template,
is_deleted = :is_deleted
WHERE id = :id`, host)
return err
}
// List will return a list of hosts
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "created_on",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("%s -- %+v", query, params)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}

View File

@ -0,0 +1,73 @@
package hosttemplate
import (
"fmt"
"time"
"npm/internal/database"
"npm/internal/types"
)
const (
tableName = "host_template"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
Type string `json:"host_type" db:"host_type" filter:"host_type,string"`
Template string `json:"template" db:"template" filter:"template,string"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, 0}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.UserID == 0 {
return fmt.Errorf("User ID must be specified")
}
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// Delete will mark a host as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}

View File

@ -0,0 +1,15 @@
package hosttemplate
import (
"npm/internal/model"
)
// ListResponse is the JSON response for this list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,80 @@
package entity
import (
"fmt"
"reflect"
"strings"
"npm/internal/database"
"npm/internal/model"
)
// ListQueryBuilder should be able to return the query and params to get items agnostically based
// on given params.
func ListQueryBuilder(modelExample interface{}, tableName string, pageInfo *model.PageInfo, defaultSort model.Sort, filters []model.Filter, filterMapFunctions map[string]FilterMapFunction, returnCount bool) (string, []interface{}) {
var queryStrings []string
var whereStrings []string
var params []interface{}
if returnCount {
queryStrings = append(queryStrings, "SELECT COUNT(*)")
} else {
queryStrings = append(queryStrings, "SELECT *")
}
// nolint: gosec
queryStrings = append(queryStrings, fmt.Sprintf("FROM `%s`", tableName))
// Append filters to where clause:
if filters != nil {
filterMap := GetFilterMap(modelExample)
filterQuery, filterParams := GenerateSQLFromFilters(filters, filterMap, filterMapFunctions)
whereStrings = []string{filterQuery}
params = append(params, filterParams...)
}
// Add is deletee check if model has the field
if hasDeletedField(modelExample) {
params = append(params, 0)
whereStrings = append(whereStrings, "`is_deleted` = ?")
}
// Append where clauses to query
if len(whereStrings) > 0 {
// nolint: gosec
queryStrings = append(queryStrings, fmt.Sprintf("WHERE %s", strings.Join(whereStrings, " AND ")))
}
if !returnCount {
var orderBy string
columns := GetDBColumns(modelExample)
orderBy, pageInfo.Sort = database.BuildOrderBySQL(columns, &pageInfo.Sort)
if orderBy != "" {
queryStrings = append(queryStrings, orderBy)
} else {
pageInfo.Sort = append(pageInfo.Sort, defaultSort)
queryStrings = append(queryStrings, fmt.Sprintf("ORDER BY `%v` COLLATE NOCASE %v", defaultSort.Field, defaultSort.Direction))
}
params = append(params, pageInfo.Offset)
params = append(params, pageInfo.Limit)
queryStrings = append(queryStrings, "LIMIT ?, ?")
}
return strings.Join(queryStrings, " "), params
}
func hasDeletedField(modelExample interface{}) bool {
t := reflect.TypeOf(modelExample)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
dbTag := field.Tag.Get(DBTagName)
if dbTag == "is_deleted" {
return true
}
}
return false
}

View File

@ -0,0 +1,19 @@
package setting
import (
"npm/internal/config"
"npm/internal/logger"
)
// ApplySettings will load settings from the DB and apply them where required
func ApplySettings() {
logger.Debug("Applying Settings")
// Error-reporting
m, err := GetByName("error-reporting")
if err != nil {
logger.Error("ApplySettingsError", err)
} else {
config.ErrorReporting = m.Value.Decoded.(bool)
}
}

View File

@ -0,0 +1,25 @@
package setting
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,127 @@
package setting
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a setting by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// GetByName finds a setting by name
func GetByName(name string) (Model, error) {
var m Model
err := m.LoadByName(name)
return m, err
}
// Create will Create a Setting from this model
func Create(setting *Model) (int, error) {
if setting.ID != 0 {
return 0, goerrors.New("Cannot create setting when model already has an ID")
}
setting.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
name,
value
) VALUES (
:created_on,
:modified_on,
:name,
:value
)`, setting)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Setting from this model
func Update(setting *Model) error {
if setting.ID == 0 {
return goerrors.New("Cannot update setting when model doesn't have an ID")
}
setting.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
name = :name,
value = :value
WHERE id = :id`, setting)
return err
}
// List will return a list of settings
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("%+v", queryErr)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("%+v", err)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}

View File

@ -0,0 +1,70 @@
package setting
import (
"fmt"
"strings"
"time"
"npm/internal/database"
"npm/internal/types"
)
const (
tableName = "setting"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
Description string `json:"description" db:"description" filter:"description,string"`
Value types.JSONB `json:"value" db:"value"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE `id` = ? LIMIT 1", tableName)
params := []interface{}{id}
return m.getByQuery(query, params)
}
// LoadByName will load from a Name
func (m *Model) LoadByName(name string) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE LOWER(`name`) = ? LIMIT 1", tableName)
params := []interface{}{strings.TrimSpace(strings.ToLower(name))}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
// Reapply settings
if err == nil {
ApplySettings()
}
return err
}

View File

@ -0,0 +1,15 @@
package setting
import (
"npm/internal/model"
)
// ListResponse is the JSON response for settings list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,25 @@
package stream
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,135 @@
package stream
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a auth by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// Create will create a Auth from this model
func Create(host *Model) (int, error) {
if host.ID != 0 {
return 0, goerrors.New("Cannot create stream when model already has an ID")
}
host.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
provider,
name,
domain_names,
expires_on,
meta,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:provider,
:name,
:domain_names,
:expires_on,
:meta,
:is_deleted
)`, host)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Host from this model
func Update(host *Model) error {
if host.ID == 0 {
return goerrors.New("Cannot update stream when model doesn't have an ID")
}
host.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
user_id = :user_id,
provider = :provider,
name = :name,
domain_names = :domain_names,
expires_on = :expires_on,
meta = :meta,
is_deleted = :is_deleted
WHERE id = :id`, host)
return err
}
// List will return a list of hosts
func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("%s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("%s -- %+v", query, params)
return result, err
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}

View File

@ -0,0 +1,75 @@
package stream
import (
"fmt"
"time"
"npm/internal/database"
"npm/internal/types"
)
const (
tableName = "stream"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
ExpiresOn types.DBDate `json:"expires_on" db:"expires_on" filter:"expires_on,integer"`
UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"`
Provider string `json:"provider" db:"provider" filter:"provider,string"`
Name string `json:"name" db:"name" filter:"name,string"`
DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"`
Meta types.JSONB `json:"-" db:"meta"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, 0}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.UserID == 0 {
return fmt.Errorf("User ID must be specified")
}
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// Delete will mark a host as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}

View File

@ -0,0 +1,15 @@
package stream
import (
"npm/internal/model"
)
// ListResponse is the JSON response for this list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}

View File

@ -0,0 +1,40 @@
package user
const (
// CapabilityFullAdmin can do anything
CapabilityFullAdmin = "full-admin"
// CapabilityAccessListsView access lists view
CapabilityAccessListsView = "access-lists.view"
// CapabilityAccessListsManage access lists manage
CapabilityAccessListsManage = "access-lists.manage"
// CapabilityAuditLogView audit log view
CapabilityAuditLogView = "audit-log.view"
// CapabilityCertificatesView certificates view
CapabilityCertificatesView = "certificates.view"
// CapabilityCertificatesManage certificates manage
CapabilityCertificatesManage = "certificates.manage"
// CapabilityCertificateAuthoritiesView certificate authorities view
CapabilityCertificateAuthoritiesView = "certificate-authorities.view"
// CapabilityCertificateAuthoritiesManage certificate authorities manage
CapabilityCertificateAuthoritiesManage = "certificate-authorities.manage"
// CapabilityDNSProvidersView dns providers view
CapabilityDNSProvidersView = "dns-providers.view"
// CapabilityDNSProvidersManage dns providers manage
CapabilityDNSProvidersManage = "dns-providers.manage"
// CapabilityHostsView hosts view
CapabilityHostsView = "hosts.view"
// CapabilityHostsManage hosts manage
CapabilityHostsManage = "hosts.manage"
// CapabilityHostTemplatesView host-templates view
CapabilityHostTemplatesView = "host-templates.view"
// CapabilityHostTemplatesManage host-templates manage
CapabilityHostTemplatesManage = "host-templates.manage"
// CapabilitySettingsManage settings manage
CapabilitySettingsManage = "settings.manage"
// CapabilityStreamsView streams view
CapabilityStreamsView = "streams.view"
// CapabilityStreamsManage streams manage
CapabilityStreamsManage = "streams.manage"
// CapabilityUsersManage users manage
CapabilityUsersManage = "users.manage"
)

View File

@ -0,0 +1,25 @@
package user
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

View File

@ -0,0 +1,229 @@
package user
import (
"database/sql"
goerrors "errors"
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/model"
)
// GetByID finds a user by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// GetByEmail finds a user by email
func GetByEmail(email string) (Model, error) {
var m Model
err := m.LoadByEmail(email)
return m, err
}
// Create will create a User from given model
func Create(user *Model) (int, error) {
// We need to ensure that a user can't be created with the same email
// as an existing non-deleted user. Usually you would do this with the
// database schema, but it's a bit more complex because of the is_deleted field.
if user.ID != 0 {
return 0, goerrors.New("Cannot create user when model already has an ID")
}
// Check if an existing user with this email exists
_, err := GetByEmail(user.Email)
if err == nil {
return 0, errors.ErrDuplicateEmailUser
}
user.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
name,
nickname,
email,
is_disabled
) VALUES (
:created_on,
:modified_on,
:name,
:nickname,
:email,
:is_disabled
)`, user)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a User from this model
func Update(user *Model) error {
if user.ID == 0 {
return goerrors.New("Cannot update user when model doesn't have an ID")
}
// Check that the email address isn't associated with another user
if existingUser, _ := GetByEmail(user.Email); existingUser.ID != 0 && existingUser.ID != user.ID {
return errors.ErrDuplicateEmailUser
}
user.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
name = :name,
nickname = :nickname,
email = :email,
is_disabled = :is_disabled,
is_deleted = :is_deleted
WHERE id = :id`, user)
return err
}
// IsEnabled is used by middleware to ensure the user is still enabled
// returns (userExist, isEnabled)
func IsEnabled(userID int) (bool, bool) {
// nolint: gosec
query := `SELECT is_disabled FROM ` + fmt.Sprintf("`%s`", tableName) + ` WHERE id = ? AND is_deleted = ?`
disabled := true
db := database.GetInstance()
err := db.QueryRowx(query, userID, 0).Scan(&disabled)
if err == sql.ErrNoRows {
return false, false
} else if err != nil {
logger.Error("QueryError", err)
}
return true, !disabled
}
// List will return a list of users
func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ListResponse, error) {
var result ListResponse
var exampleModel Model
defaultSort := model.Sort{
Field: "name",
Direction: "ASC",
}
db := database.GetInstance()
if db == nil {
return result, errors.ErrDatabaseUnavailable
}
/*
filters = append(filters, model.Filter{
Field: "is_system",
Modifier: "equals",
Value: []string{"0"},
})
*/
// Get count of items in this search
query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true)
countRow := db.QueryRowx(query, params...)
var totalRows int
queryErr := countRow.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Debug("Query: %s -- %+v", query, params)
return result, queryErr
}
// Get rows
var items []Model
query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false)
err := db.Select(&items, query, params...)
if err != nil {
logger.Debug("Query: %s -- %+v", query, params)
return result, err
}
for idx := range items {
items[idx].generateGravatar()
}
if expand != nil {
for idx := range items {
expandErr := items[idx].Expand(expand)
if expandErr != nil {
logger.Error("UsersExpansionError", expandErr)
}
}
}
result = ListResponse{
Items: items,
Total: totalRows,
Limit: pageInfo.Limit,
Offset: pageInfo.Offset,
Sort: pageInfo.Sort,
Filter: filters,
}
return result, nil
}
// DeleteAll will do just that, and should only be used for testing purposes.
func DeleteAll() error {
db := database.GetInstance()
_, err := db.Exec(fmt.Sprintf("DELETE FROM `%s`", tableName))
return err
}
// GetCapabilities gets capabilities for a user
func GetCapabilities(userID int) ([]string, error) {
var capabilities []string
db := database.GetInstance()
if db == nil {
return []string{}, errors.ErrDatabaseUnavailable
}
query := `SELECT c.name FROM "user_has_capability" h
INNER JOIN "capability" c ON c.id = h.capability_id
WHERE h.user_id = ?`
rows, err := db.Query(query, userID)
if err != nil && err != sql.ErrNoRows {
logger.Debug("QUERY: %v -- %v", query, userID)
return []string{}, err
}
// nolint: errcheck
defer rows.Close()
for rows.Next() {
var name string
err := rows.Scan(&name)
if err != nil {
return []string{}, err
}
capabilities = append(capabilities, name)
}
return capabilities, nil
}

View File

@ -0,0 +1,191 @@
package user
import (
goerrors "errors"
"fmt"
"strings"
"time"
"npm/internal/database"
"npm/internal/entity/auth"
"npm/internal/errors"
"npm/internal/logger"
"npm/internal/types"
"npm/internal/util"
"github.com/drexedam/gravatar"
)
const (
tableName = "user"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
Nickname string `json:"nickname" db:"nickname" filter:"nickname,string"`
Email string `json:"email" db:"email" filter:"email,email"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
GravatarURL string `json:"gravatar_url"`
IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"`
IsSystem bool `json:"is_system,omitempty" db:"is_system"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
// Expansions
Auth *auth.Model `json:"auth,omitempty" db:"-"`
Capabilities []string `json:"capabilities,omitempty"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
err := database.GetByQuery(m, query, params)
m.generateGravatar()
return err
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName)
params := []interface{}{id, false}
return m.getByQuery(query, params)
}
// LoadByEmail will load from an Email
func (m *Model) LoadByEmail(email string) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE email = ? AND is_deleted = ? AND is_system = ? LIMIT 1", tableName)
params := []interface{}{strings.TrimSpace(strings.ToLower(email)), false, false}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
m.generateGravatar()
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
// Ensure email is nice
m.Email = strings.TrimSpace(strings.ToLower(m.Email))
if m.IsSystem {
return errors.ErrSystemUserReadonly
}
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// Delete will mark a user as deleted
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
return false
}
return true
}
// SetPermissions will wipe out any existing permissions and add new ones for this user
func (m *Model) SetPermissions(permissions []string) error {
if m.ID == 0 {
return fmt.Errorf("Cannot set permissions without first saving the User")
}
db := database.GetInstance()
// Wipe out previous permissions
query := `DELETE FROM "user_has_capability" WHERE "user_id" = ?`
if _, err := db.Exec(query, m.ID); err != nil {
logger.Debug("QUERY: %v -- %v", query, m.ID)
return err
}
if len(permissions) > 0 {
// Add new permissions
for _, permission := range permissions {
query = `INSERT INTO "user_has_capability" (
"user_id", "capability_id"
) VALUES (
?,
(SELECT id FROM capability WHERE name = ?)
)`
_, err := db.Exec(query, m.ID, permission)
if err != nil {
logger.Debug("QUERY: %v -- %v -- %v", query, m.ID, permission)
return err
}
}
}
return nil
}
// Expand will fill in more properties
func (m *Model) Expand(items []string) error {
var err error
if util.SliceContainsItem(items, "capabilities") && m.ID > 0 {
m.Capabilities, err = GetCapabilities(m.ID)
}
return err
}
func (m *Model) generateGravatar() {
m.GravatarURL = gravatar.New(m.Email).
Size(128).
Default(gravatar.MysteryMan).
Rating(gravatar.Pg).
AvatarURL()
}
// SaveCapabilities will save the capabilities of the user.
func (m *Model) SaveCapabilities() error {
// m.Capabilities
if m.ID == 0 {
return fmt.Errorf("Cannot save capabilities on unsaved user")
}
// there must be at least 1 capability
if len(m.Capabilities) == 0 {
return goerrors.New("At least 1 capability required for a user")
}
db := database.GetInstance()
// Get a full list of capabilities
var capabilities []string
query := `SELECT "name" from "capability"`
err := db.Select(&capabilities, query)
if err != nil {
return err
}
// Check that the capabilities defined exist in the db
for _, cap := range m.Capabilities {
found := false
for _, a := range capabilities {
if a == cap {
found = true
}
}
if !found {
return fmt.Errorf("Capability `%s` is not valid", cap)
}
}
return m.SetPermissions(m.Capabilities)
}

View File

@ -0,0 +1,15 @@
package user
import (
"npm/internal/model"
)
// ListResponse is the JSON response for users list
type ListResponse struct {
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Sort []model.Sort `json:"sort"`
Filter []model.Filter `json:"filter,omitempty"`
Items []Model `json:"items,omitempty"`
}