From 4a57956093fcb683340e310c64ff57bee36c6899 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Tue, 30 May 2023 22:26:44 +1000 Subject: [PATCH] More work to support nullable foreign keys Adds nullable int/uint types --- .../mysql/20201013035318_initial_schema.sql | 4 +- .../20201013035318_initial_schema.sql | 2 +- .../sqlite/20201013035318_initial_schema.sql | 354 +++++++++--------- backend/internal/api/handler/certificates.go | 3 +- backend/internal/api/handler/hosts.go | 2 +- backend/internal/database/db.go | 26 +- backend/internal/entity/certificate/model.go | 31 +- backend/internal/entity/host/model.go | 64 ++-- backend/internal/nginx/template_test.go | 3 +- backend/internal/types/db_nullable_int.go | 63 ++++ backend/internal/types/db_nullable_uint.go | 64 ++++ backend/internal/types/nullable_db_date.go | 2 + backend/internal/util/time.go | 9 + backend/internal/validator/hosts.go | 16 +- 14 files changed, 387 insertions(+), 256 deletions(-) create mode 100644 backend/internal/types/db_nullable_int.go create mode 100644 backend/internal/types/db_nullable_uint.go create mode 100644 backend/internal/util/time.go diff --git a/backend/embed/migrations/mysql/20201013035318_initial_schema.sql b/backend/embed/migrations/mysql/20201013035318_initial_schema.sql index 9cfa38ab..f2ce4c63 100644 --- a/backend/embed/migrations/mysql/20201013035318_initial_schema.sql +++ b/backend/embed/migrations/mysql/20201013035318_initial_schema.sql @@ -116,7 +116,7 @@ CREATE TABLE IF NOT EXISTS `certificate` `dns_provider_id` INT, -- 0, for a http or custom cert `name` VARCHAR(50) NOT NULL, `domain_names` TEXT NOT NULL, - `expires_on` INT DEFAULT 0, + `expires_on` BIGINT NOT NULL DEFAULT 0, `status` VARCHAR(50) NOT NULL, -- ready,requesting,failed,provided `error_message` TEXT NOT NULL, `meta` TEXT NOT NULL, @@ -205,7 +205,7 @@ CREATE TABLE IF NOT EXISTS `access_list` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS host +CREATE TABLE IF NOT EXISTS `host` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `created_at` BIGINT NOT NULL DEFAULT 0, diff --git a/backend/embed/migrations/postgres/20201013035318_initial_schema.sql b/backend/embed/migrations/postgres/20201013035318_initial_schema.sql index 1404e76c..e49a2da0 100644 --- a/backend/embed/migrations/postgres/20201013035318_initial_schema.sql +++ b/backend/embed/migrations/postgres/20201013035318_initial_schema.sql @@ -102,7 +102,7 @@ CREATE TABLE "certificate" ( "dns_provider_id" INTEGER REFERENCES "dns_provider"("id") ON DELETE CASCADE, -- 0, for a http or custom cert "name" VARCHAR(50) NOT NULL, "domain_names" TEXT NOT NULL, - "expires_on" INTEGER DEFAULT 0, + "expires_on" BIGINT NOT NULL DEFAULT 0, "status" VARCHAR(50) NOT NULL, -- ready,requesting,failed,provided "error_message" TEXT NOT NULL DEFAULT '', "meta" TEXT NOT NULL, diff --git a/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql b/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql index 48c91ba1..1a896a6b 100644 --- a/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql +++ b/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql @@ -2,243 +2,243 @@ CREATE TABLE IF NOT EXISTS `jwt_keys` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - public_key TEXT NOT NULL, - private_key TEXT NOT NULL + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `public_key` TEXT NOT NULL, + `private_key` TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS `user` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - name TEXT NOT NULL, - nickname TEXT NOT NULL, - email TEXT NOT NULL, - is_system INTEGER NOT NULL DEFAULT 0, - is_disabled INTEGER NOT NULL DEFAULT 0 + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `nickname` TEXT NOT NULL, + `email` TEXT NOT NULL, + `is_system` INTEGER NOT NULL DEFAULT 0, + `is_disabled` INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS `capability` ( - name TEXT PRIMARY KEY, - UNIQUE (name) + `name` TEXT PRIMARY KEY, + UNIQUE (`name`) ); CREATE TABLE IF NOT EXISTS `user_has_capability` ( - user_id INTEGER NOT NULL, - capability_name TEXT NOT NULL, - UNIQUE (user_id, capability_name), - FOREIGN KEY (capability_name) REFERENCES capability (name) + `user_id` INTEGER NOT NULL, + `capability_name` TEXT NOT NULL, + UNIQUE (`user_id`, `capability_name`), + FOREIGN KEY (`capability_name`) REFERENCES `capability` (`name`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `auth` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - type TEXT NOT NULL, - secret TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES user (id), - UNIQUE (user_id, type) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `type` TEXT NOT NULL, + `secret` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + UNIQUE (`user_id`, `type`) ); CREATE TABLE IF NOT EXISTS `setting` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - name TEXT NOT NULL, - description TEXT NOT NULL DEFAULT "", - value TEXT NOT NULL, - UNIQUE (name) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `description` TEXT NOT NULL DEFAULT "", + `value` TEXT NOT NULL, + UNIQUE (`name`) ); CREATE TABLE IF NOT EXISTS `audit_log` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - object_type TEXT NOT NULL, - object_id INTEGER NOT NULL, - action TEXT NOT NULL, - meta TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES user (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `object_type` TEXT NOT NULL, + `object_id` INTEGER NOT NULL, + `action` TEXT NOT NULL, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `certificate_authority` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - name TEXT NOT NULL, - acmesh_server TEXT NOT NULL DEFAULT "", - ca_bundle TEXT NOT NULL DEFAULT "", - is_wildcard_supported INTEGER NOT NULL DEFAULT 0, -- specific to each CA, acme v1 doesn't usually have wildcards - max_domains INTEGER NOT NULL DEFAULT 5, -- per request - is_readonly INTEGER NOT NULL DEFAULT 0 + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `acmesh_server` TEXT NOT NULL DEFAULT "", + `ca_bundle` TEXT NOT NULL DEFAULT "", + `is_wildcard_supported` INTEGER NOT NULL DEFAULT 0, -- specific to each CA, acme v1 doesn't usually have wildcards + `max_domains` INTEGER NOT NULL DEFAULT 5, -- per request + `is_readonly` INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS `dns_provider` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - name TEXT NOT NULL, - acmesh_name TEXT NOT NULL, - dns_sleep INTEGER NOT NULL DEFAULT 0, - meta TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES user (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `acmesh_name` TEXT NOT NULL, + `dns_sleep` INTEGER NOT NULL DEFAULT 0, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `certificate` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - type TEXT NOT NULL, -- custom,dns,http - certificate_authority_id INTEGER, -- 0 for a custom cert - dns_provider_id INTEGER, -- 0, for a http or custom cert - name TEXT NOT NULL, - domain_names TEXT NOT NULL, - expires_on INTEGER DEFAULT 0, - status TEXT NOT NULL, -- ready,requesting,failed,provided - error_message TEXT NOT NULL DEFAULT "", - meta TEXT NOT NULL, - is_ecc INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES user (id), - FOREIGN KEY (certificate_authority_id) REFERENCES certificate_authority (id), - FOREIGN KEY (dns_provider_id) REFERENCES dns_provider (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `type` TEXT NOT NULL, -- custom,dns,http + `certificate_authority_id` INTEGER, -- 0 for a custom cert + `dns_provider_id` INTEGER, -- 0, for a http or custom cert + `name` TEXT NOT NULL, + `domain_names` TEXT NOT NULL, + `expires_on` INTEGER NOT NULL DEFAULT 0, + `status` TEXT NOT NULL, -- ready,requesting,failed,provided + `error_message` TEXT NOT NULL DEFAULT "", + `meta` TEXT NOT NULL, + `is_ecc` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`certificate_authority_id`) REFERENCES `certificate_authority` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`dns_provider_id`) REFERENCES `dns_provider` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `stream` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - listen_interface TEXT NOT NULL, - incoming_port INTEGER NOT NULL, - tcp_forwarding INTEGER NOT NULL DEFAULT 0, - udp_forwarding INTEGER NOT NULL DEFAULT 0, - advanced_config TEXT NOT NULL, - is_disabled INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES user (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `listen_interface` TEXT NOT NULL, + `incoming_port` INTEGER NOT NULL, + `tcp_forwarding` INTEGER NOT NULL DEFAULT 0, + `udp_forwarding` INTEGER NOT NULL DEFAULT 0, + `advanced_config` TEXT NOT NULL, + `is_disabled` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `nginx_template` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL, - template TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES user (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `type` TEXT NOT NULL, + `template` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `upstream` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - name TEXT NOT NULL, - nginx_template_id INTEGER NOT NULL, - ip_hash INTEGER NOT NULL DEFAULT 0, - ntlm INTEGER NOT NULL DEFAULT 0, - keepalive INTEGER NOT NULL DEFAULT 0, - keepalive_requests INTEGER NOT NULL DEFAULT 0, - keepalive_time TEXT NOT NULL DEFAULT "", - keepalive_timeout TEXT NOT NULL DEFAULT "", - advanced_config TEXT NOT NULL, - status TEXT NOT NULL DEFAULT "", - error_message TEXT NOT NULL DEFAULT "", - FOREIGN KEY (user_id) REFERENCES user (id), - FOREIGN KEY (nginx_template_id) REFERENCES nginx_template (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `nginx_template_id` INTEGER NOT NULL, + `ip_hash` INTEGER NOT NULL DEFAULT 0, + `ntlm` INTEGER NOT NULL DEFAULT 0, + `keepalive` INTEGER NOT NULL DEFAULT 0, + `keepalive_requests` INTEGER NOT NULL DEFAULT 0, + `keepalive_time` TEXT NOT NULL DEFAULT "", + `keepalive_timeout` TEXT NOT NULL DEFAULT "", + `advanced_config` TEXT NOT NULL, + `status` TEXT NOT NULL DEFAULT "", + `error_message` TEXT NOT NULL DEFAULT "", + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`nginx_template_id`) REFERENCES `nginx_template` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `upstream_server` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - upstream_id INTEGER NOT NULL, - server TEXT NOT NULL, - weight INTEGER NOT NULL DEFAULT 0, - max_conns INTEGER NOT NULL DEFAULT 0, - max_fails INTEGER NOT NULL DEFAULT 0, - fail_timeout INTEGER NOT NULL DEFAULT 0, - is_backup INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (upstream_id) REFERENCES upstream (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `upstream_id` INTEGER NOT NULL, + `server` TEXT NOT NULL, + `weight` INTEGER NOT NULL DEFAULT 0, + `max_conns` INTEGER NOT NULL DEFAULT 0, + `max_fails` INTEGER NOT NULL DEFAULT 0, + `fail_timeout` INTEGER NOT NULL DEFAULT 0, + `is_backup` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`upstream_id`) REFERENCES `upstream` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `access_list` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - name TEXT NOT NULL, - meta TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES user (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS `host` ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT 0, - is_deleted INTEGER NOT NULL DEFAULT 0, - user_id INTEGER NOT NULL, - type TEXT NOT NULL, - nginx_template_id INTEGER NOT NULL, - listen_interface TEXT NOT NULL DEFAULT "", - domain_names TEXT NOT NULL, - upstream_id INTEGER, - proxy_scheme TEXT NOT NULL DEFAULT "", - proxy_host TEXT NOT NULL DEFAULT "", - proxy_port INTEGER NOT NULL DEFAULT 0, - certificate_id INTEGER, - access_list_id INTEGER, - ssl_forced INTEGER NOT NULL DEFAULT 0, - caching_enabled INTEGER NOT NULL DEFAULT 0, - block_exploits INTEGER NOT NULL DEFAULT 0, - allow_websocket_upgrade INTEGER NOT NULL DEFAULT 0, - http2_support INTEGER NOT NULL DEFAULT 0, - hsts_enabled INTEGER NOT NULL DEFAULT 0, - hsts_subdomains INTEGER NOT NULL DEFAULT 0, - paths TEXT NOT NULL DEFAULT "", - advanced_config TEXT NOT NULL DEFAULT "", - status TEXT NOT NULL DEFAULT "", - error_message TEXT NOT NULL DEFAULT "", - is_disabled INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES user (id), - FOREIGN KEY (nginx_template_id) REFERENCES nginx_template (id), - FOREIGN KEY (upstream_id) REFERENCES upstream (id), - FOREIGN KEY (certificate_id) REFERENCES certificate (id), - FOREIGN KEY (access_list_id) REFERENCES access_list (id) + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `type` TEXT NOT NULL, + `nginx_template_id` INTEGER NOT NULL, + `listen_interface` TEXT NOT NULL DEFAULT "", + `domain_names` TEXT NOT NULL, + `upstream_id` INTEGER, + `proxy_scheme` TEXT NOT NULL DEFAULT "", + `proxy_host` TEXT NOT NULL DEFAULT "", + `proxy_port` INTEGER NOT NULL DEFAULT 0, + `certificate_id` INTEGER, + `access_list_id` INTEGER, + `ssl_forced` INTEGER NOT NULL DEFAULT 0, + `caching_enabled` INTEGER NOT NULL DEFAULT 0, + `block_exploits` INTEGER NOT NULL DEFAULT 0, + `allow_websocket_upgrade` INTEGER NOT NULL DEFAULT 0, + `http2_support` INTEGER NOT NULL DEFAULT 0, + `hsts_enabled` INTEGER NOT NULL DEFAULT 0, + `hsts_subdomains` INTEGER NOT NULL DEFAULT 0, + `paths` TEXT NOT NULL DEFAULT "", + `advanced_config` TEXT NOT NULL DEFAULT "", + `status` TEXT NOT NULL DEFAULT "", + `error_message` TEXT NOT NULL DEFAULT "", + `is_disabled` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`nginx_template_id`) REFERENCES `nginx_template` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`upstream_id`) REFERENCES `upstream` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`certificate_id`) REFERENCES `certificate` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`access_list_id`) REFERENCES `access_list` (`id`) ON DELETE CASCADE ); -- migrate:down diff --git a/backend/internal/api/handler/certificates.go b/backend/internal/api/handler/certificates.go index 7fc1caf2..aae8b2b6 100644 --- a/backend/internal/api/handler/certificates.go +++ b/backend/internal/api/handler/certificates.go @@ -148,7 +148,7 @@ func getCertificateFromRequest(w http.ResponseWriter, r *http.Request) *certific return nil } -// getCertificateFromRequest has some reusable code for all endpoints that +// fillObjectFromBody has some reusable code for all endpoints that // have a certificate id in the url. it will write errors to the output. func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema string, o interface{}) bool { bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) @@ -167,6 +167,7 @@ func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema err := json.Unmarshal(bodyBytes, o) if err != nil { + logger.Debug("Unmarshal Error: %+v", err) h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) return false } diff --git a/backend/internal/api/handler/hosts.go b/backend/internal/api/handler/hosts.go index 2c6125b5..4275f0d7 100644 --- a/backend/internal/api/handler/hosts.go +++ b/backend/internal/api/handler/hosts.go @@ -87,7 +87,7 @@ func CreateHost() func(http.ResponseWriter, *http.Request) { return } - if newHost.UpstreamID > 0 { + if newHost.UpstreamID.Uint > 0 { // nolint: errcheck, gosec newHost.Expand([]string{"upstream"}) } diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 0dcae235..a77e10fc 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -56,27 +56,19 @@ func connect() (*gorm.DB, error) { return nil, eris.New(fmt.Sprintf("Database driver %s is not supported. Valid options are: %s, %s or %s", config.Configuration.DB.Driver, config.DatabaseSqlite, config.DatabasePostgres, config.DatabaseMysql)) } - return gorm.Open(d, &gorm.Config{ - // see: https://gorm.io/docs/gorm_config.html + // see: https://gorm.io/docs/gorm_config.html + cfg := gorm.Config{ NamingStrategy: schema.NamingStrategy{ SingularTable: true, NoLowerCase: true, }, PrepareStmt: false, - Logger: gormlogger.Default.LogMode(gormlogger.Silent), - }) -} - -/* -func autocreate(dbFile string) { - if _, err := os.Stat(dbFile); os.IsNotExist(err) { - // Create it - logger.Info("Creating Sqlite DB: %s", dbFile) - // nolint: gosec - _, err = os.Create(dbFile) - if err != nil { - logger.Error("FileCreateError", err) - } } + + // Silence gorm query errors unless when not in debug mode + if config.GetLogLevel() != logger.DebugLevel { + cfg.Logger = gormlogger.Default.LogMode(gormlogger.Silent) + } + + return gorm.Open(d, &cfg) } -*/ diff --git a/backend/internal/entity/certificate/model.go b/backend/internal/entity/certificate/model.go index 2b6f6473..329c7a48 100644 --- a/backend/internal/entity/certificate/model.go +++ b/backend/internal/entity/certificate/model.go @@ -45,13 +45,13 @@ const ( // Model is the model type Model struct { entity.ModelBase - ExpiresOn types.NullableDBDate `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"` - Type string `json:"type" gorm:"column:type" filter:"type,string"` UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` - CertificateAuthorityID uint `json:"certificate_authority_id" gorm:"column:certificate_authority_id" filter:"certificate_authority_id,integer"` - DNSProviderID uint `json:"dns_provider_id" gorm:"column:dns_provider_id" filter:"dns_provider_id,integer"` + Type string `json:"type" gorm:"column:type" filter:"type,string"` + CertificateAuthorityID types.NullableDBUint `json:"certificate_authority_id" gorm:"column:certificate_authority_id" filter:"certificate_authority_id,integer"` + DNSProviderID types.NullableDBUint `json:"dns_provider_id" gorm:"column:dns_provider_id" filter:"dns_provider_id,integer"` Name string `json:"name" gorm:"column:name" filter:"name,string"` DomainNames types.JSONB `json:"domain_names" gorm:"column:domain_names" filter:"domain_names,string"` + ExpiresOn int64 `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"` Status string `json:"status" gorm:"column:status" filter:"status,string"` ErrorMessage string `json:"error_message" gorm:"column:error_message" filter:"error_message,string"` Meta types.JSONB `json:"-" gorm:"column:meta"` @@ -117,13 +117,13 @@ func (m *Model) Validate() bool { switch m.Type { case TypeCustom: // TODO: make sure meta contains required fields - return m.DNSProviderID == 0 && m.CertificateAuthorityID == 0 + return m.DNSProviderID.Uint == 0 && m.CertificateAuthorityID.Uint == 0 case TypeHTTP: - return m.DNSProviderID == 0 && m.CertificateAuthorityID > 0 + return m.DNSProviderID.Uint == 0 && m.CertificateAuthorityID.Uint > 0 case TypeDNS: - return m.DNSProviderID > 0 && m.CertificateAuthorityID > 0 + return m.DNSProviderID.Uint > 0 && m.CertificateAuthorityID.Uint > 0 case TypeMkcert: return true @@ -175,15 +175,15 @@ func (m *Model) setDefaultStatus() { func (m *Model) Expand(items []string) error { var err error - if util.SliceContainsItem(items, "certificate-authority") && m.CertificateAuthorityID > 0 { + if util.SliceContainsItem(items, "certificate-authority") && m.CertificateAuthorityID.Uint > 0 { var certificateAuthority certificateauthority.Model - certificateAuthority, err = certificateauthority.GetByID(m.CertificateAuthorityID) + certificateAuthority, err = certificateauthority.GetByID(m.CertificateAuthorityID.Uint) m.CertificateAuthority = &certificateAuthority } - if util.SliceContainsItem(items, "dns-provider") && m.DNSProviderID > 0 { + if util.SliceContainsItem(items, "dns-provider") && m.DNSProviderID.Uint > 0 { var dnsProvider dnsprovider.Model - dnsProvider, err = dnsprovider.GetByID(m.DNSProviderID) + dnsProvider, err = dnsprovider.GetByID(m.DNSProviderID.Uint) m.DNSProvider = &dnsProvider } @@ -262,8 +262,7 @@ func (m *Model) Request() error { // If done m.Status = StatusProvided - t := time.Now() - m.ExpiresOn.Time = &t // todo + m.ExpiresOn = time.Now().UnixMilli() if err := m.Save(); err != nil { logger.Error("CertificateSaveError", err) return err @@ -287,11 +286,11 @@ func (m *Model) GetTemplate() Template { ID: m.ID, CreatedAt: fmt.Sprintf("%d", m.CreatedAt), // todo: nice date string UpdatedAt: fmt.Sprintf("%d", m.UpdatedAt), // todo: nice date string - ExpiresOn: m.ExpiresOn.AsString(), + ExpiresOn: util.UnixMilliToNiceFormat(m.ExpiresOn), Type: m.Type, UserID: m.UserID, - CertificateAuthorityID: m.CertificateAuthorityID, - DNSProviderID: m.DNSProviderID, + CertificateAuthorityID: m.CertificateAuthorityID.Uint, + DNSProviderID: m.DNSProviderID.Uint, Name: m.Name, DomainNames: domainNames, Status: m.Status, diff --git a/backend/internal/entity/host/model.go b/backend/internal/entity/host/model.go index b3db092a..7457b487 100644 --- a/backend/internal/entity/host/model.go +++ b/backend/internal/entity/host/model.go @@ -27,29 +27,29 @@ const ( // Model is the model type Model struct { entity.ModelBase - UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` - Type string `json:"type" gorm:"column:type" filter:"type,string"` - NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"` - ListenInterface string `json:"listen_interface" gorm:"column:listen_interface" filter:"listen_interface,string"` - DomainNames types.JSONB `json:"domain_names" gorm:"column:domain_names" filter:"domain_names,string"` - UpstreamID uint `json:"upstream_id" gorm:"column:upstream_id" filter:"upstream_id,integer"` - ProxyScheme string `json:"proxy_scheme" gorm:"column:proxy_scheme" filter:"proxy_scheme,string"` - ProxyHost string `json:"proxy_host" gorm:"column:proxy_host" filter:"proxy_host,string"` - ProxyPort int `json:"proxy_port" gorm:"column:proxy_port" filter:"proxy_port,integer"` - CertificateID uint `json:"certificate_id" gorm:"column:certificate_id" filter:"certificate_id,integer"` - AccessListID uint `json:"access_list_id" gorm:"column:access_list_id" filter:"access_list_id,integer"` - SSLForced bool `json:"ssl_forced" gorm:"column:ssl_forced" filter:"ssl_forced,boolean"` - CachingEnabled bool `json:"caching_enabled" gorm:"column:caching_enabled" filter:"caching_enabled,boolean"` - BlockExploits bool `json:"block_exploits" gorm:"column:block_exploits" filter:"block_exploits,boolean"` - AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" gorm:"column:allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"` - HTTP2Support bool `json:"http2_support" gorm:"column:http2_support" filter:"http2_support,boolean"` - HSTSEnabled bool `json:"hsts_enabled" gorm:"column:hsts_enabled" filter:"hsts_enabled,boolean"` - HSTSSubdomains bool `json:"hsts_subdomains" gorm:"column:hsts_subdomains" filter:"hsts_subdomains,boolean"` - Paths string `json:"paths" gorm:"column:paths" filter:"paths,string"` - AdvancedConfig string `json:"advanced_config" gorm:"column:advanced_config" filter:"advanced_config,string"` - Status string `json:"status" gorm:"column:status" filter:"status,string"` - ErrorMessage string `json:"error_message" gorm:"column:error_message" filter:"error_message,string"` - IsDisabled bool `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"` + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Type string `json:"type" gorm:"column:type" filter:"type,string"` + NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"` + ListenInterface string `json:"listen_interface" gorm:"column:listen_interface" filter:"listen_interface,string"` + DomainNames types.JSONB `json:"domain_names" gorm:"column:domain_names" filter:"domain_names,string"` + UpstreamID types.NullableDBUint `json:"upstream_id" gorm:"column:upstream_id" filter:"upstream_id,integer"` + ProxyScheme string `json:"proxy_scheme" gorm:"column:proxy_scheme" filter:"proxy_scheme,string"` + ProxyHost string `json:"proxy_host" gorm:"column:proxy_host" filter:"proxy_host,string"` + ProxyPort int `json:"proxy_port" gorm:"column:proxy_port" filter:"proxy_port,integer"` + CertificateID types.NullableDBUint `json:"certificate_id" gorm:"column:certificate_id" filter:"certificate_id,integer"` + AccessListID types.NullableDBUint `json:"access_list_id" gorm:"column:access_list_id" filter:"access_list_id,integer"` + SSLForced bool `json:"ssl_forced" gorm:"column:ssl_forced" filter:"ssl_forced,boolean"` + CachingEnabled bool `json:"caching_enabled" gorm:"column:caching_enabled" filter:"caching_enabled,boolean"` + BlockExploits bool `json:"block_exploits" gorm:"column:block_exploits" filter:"block_exploits,boolean"` + AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" gorm:"column:allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"` + HTTP2Support bool `json:"http2_support" gorm:"column:http2_support" filter:"http2_support,boolean"` + HSTSEnabled bool `json:"hsts_enabled" gorm:"column:hsts_enabled" filter:"hsts_enabled,boolean"` + HSTSSubdomains bool `json:"hsts_subdomains" gorm:"column:hsts_subdomains" filter:"hsts_subdomains,boolean"` + Paths string `json:"paths" gorm:"column:paths" filter:"paths,string"` + AdvancedConfig string `json:"advanced_config" gorm:"column:advanced_config" filter:"advanced_config,string"` + Status string `json:"status" gorm:"column:status" filter:"status,string"` + ErrorMessage string `json:"error_message" gorm:"column:error_message" filter:"error_message,string"` + IsDisabled bool `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"` // Expansions Certificate *certificate.Model `json:"certificate,omitempty" gorm:"-"` NginxTemplate *nginxtemplate.Model `json:"nginx_template,omitempty" gorm:"-"` @@ -101,9 +101,9 @@ func (m *Model) Expand(items []string) error { var err error // Always expand the upstream - if m.UpstreamID > 0 { + if m.UpstreamID.Uint > 0 { var u upstream.Model - u, err = upstream.GetByID(m.UpstreamID) + u, err = upstream.GetByID(m.UpstreamID.Uint) m.Upstream = &u } @@ -113,9 +113,9 @@ func (m *Model) Expand(items []string) error { m.User = &usr } - if util.SliceContainsItem(items, "certificate") && m.CertificateID > 0 { + if util.SliceContainsItem(items, "certificate") && m.CertificateID.Uint > 0 { var cert certificate.Model - cert, err = certificate.GetByID(m.CertificateID) + cert, err = certificate.GetByID(m.CertificateID.Uint) m.Certificate = &cert } @@ -125,9 +125,9 @@ func (m *Model) Expand(items []string) error { m.NginxTemplate = &templ } - if util.SliceContainsItem(items, "upstream") && m.UpstreamID > 0 { + if util.SliceContainsItem(items, "upstream") && m.UpstreamID.Uint > 0 { var ups upstream.Model - ups, err = upstream.GetByID(m.UpstreamID) + ups, err = upstream.GetByID(m.UpstreamID.Uint) m.Upstream = &ups } @@ -150,9 +150,9 @@ func (m *Model) GetTemplate() Template { ProxyPort: m.ProxyPort, ListenInterface: m.ListenInterface, DomainNames: domainNames, - UpstreamID: m.UpstreamID, - CertificateID: m.CertificateID, - AccessListID: m.AccessListID, + UpstreamID: m.UpstreamID.Uint, + CertificateID: m.CertificateID.Uint, + AccessListID: m.AccessListID.Uint, SSLForced: m.SSLForced, CachingEnabled: m.CachingEnabled, BlockExploits: m.BlockExploits, diff --git a/backend/internal/nginx/template_test.go b/backend/internal/nginx/template_test.go index 3768709f..789d9ad7 100644 --- a/backend/internal/nginx/template_test.go +++ b/backend/internal/nginx/template_test.go @@ -6,6 +6,7 @@ import ( "npm/internal/entity" "npm/internal/entity/certificate" "npm/internal/entity/host" + "npm/internal/types" "github.com/stretchr/testify/assert" ) @@ -51,7 +52,7 @@ server { }, Status: certificate.StatusProvided, Type: certificate.TypeHTTP, - CertificateAuthorityID: 99, + CertificateAuthorityID: types.NullableDBUint{Uint: 99}, }, want: want{ output: "\nserver {\n include /etc/nginx/conf.d/npm/conf.d/acme-challenge.conf;\n include /etc/nginx/conf.d/npm/conf.d/include/ssl-ciphers.conf;\n ssl_certificate /npm-77/fullchain.pem;\n ssl_certificate_key /npm-77/privkey.pem;\n}\n", diff --git a/backend/internal/types/db_nullable_int.go b/backend/internal/types/db_nullable_int.go new file mode 100644 index 00000000..4283a07b --- /dev/null +++ b/backend/internal/types/db_nullable_int.go @@ -0,0 +1,63 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "strconv" +) + +// NullableDBInt works with database values that can be null or an integer value +type NullableDBInt struct { + Int int +} + +// Value encodes the type ready for the database +func (d NullableDBInt) Value() (driver.Value, error) { + if d.Int == 0 { + return nil, nil + } + // According to current database/sql docs, the sql has four builtin functions that + // returns driver.Value, and the underlying types are `int64`, `float64`, `string` and `bool` + return driver.Value(int64(d.Int)), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBInt) Scan(src interface{}) error { + var i int + switch v := src.(type) { + case int: + i = v + case int64: + i = int(v) + case float32: + i = int(v) + case float64: + i = int(v) + case string: + i, _ = strconv.Atoi(v) + } + d.Int = i + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *NullableDBInt) UnmarshalJSON(data []byte) error { + // total_deploy_time: 10, + // total_deploy_time: null, + + var i int + if err := json.Unmarshal(data, &i); err != nil { + i = 0 + return nil + } + d.Int = i + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d NullableDBInt) MarshalJSON() ([]byte, error) { + if d.Int == 0 { + return json.Marshal(nil) + } + return json.Marshal(d.Int) +} diff --git a/backend/internal/types/db_nullable_uint.go b/backend/internal/types/db_nullable_uint.go new file mode 100644 index 00000000..8ac49ab1 --- /dev/null +++ b/backend/internal/types/db_nullable_uint.go @@ -0,0 +1,64 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "strconv" +) + +// NullableDBUint works with database values that can be null or an integer value +type NullableDBUint struct { + Uint uint +} + +// Value encodes the type ready for the database +func (d NullableDBUint) Value() (driver.Value, error) { + if d.Uint == 0 { + return nil, nil + } + // According to current database/sql docs, the sql has four builtin functions that + // returns driver.Value, and the underlying types are `int64`, `float64`, `string` and `bool` + return driver.Value(int64(d.Uint)), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBUint) Scan(src interface{}) error { + var i uint + switch v := src.(type) { + case int: + i = uint(v) + case int64: + i = uint(v) + case float32: + i = uint(v) + case float64: + i = uint(v) + case string: + a, _ := strconv.Atoi(v) + i = uint(a) + } + d.Uint = i + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *NullableDBUint) UnmarshalJSON(data []byte) error { + // total_deploy_time: 10, + // total_deploy_time: null, + + var i uint + if err := json.Unmarshal(data, &i); err != nil { + i = 0 + return nil + } + d.Uint = i + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d NullableDBUint) MarshalJSON() ([]byte, error) { + if d.Uint == 0 { + return json.Marshal(nil) + } + return json.Marshal(d.Uint) +} diff --git a/backend/internal/types/nullable_db_date.go b/backend/internal/types/nullable_db_date.go index f9f75171..528599db 100644 --- a/backend/internal/types/nullable_db_date.go +++ b/backend/internal/types/nullable_db_date.go @@ -17,6 +17,8 @@ func (d NullableDBDate) Value() (driver.Value, error) { if d.Time == nil { return nil, nil } + // According to current database/sql docs, the sql has four builtin functions that + // returns driver.Value, and the underlying types are `int64`, `float64`, `string` and `bool` return driver.Value(d.Time.Unix()), nil } diff --git a/backend/internal/util/time.go b/backend/internal/util/time.go new file mode 100644 index 00000000..b1aed588 --- /dev/null +++ b/backend/internal/util/time.go @@ -0,0 +1,9 @@ +package util + +import "time" + +// UnixMilliToNiceFormat converts a millisecond to nice string +func UnixMilliToNiceFormat(milli int64) string { + t := time.Unix(0, milli*int64(time.Millisecond)) + return t.Format("2006-01-02 15:04:05") +} diff --git a/backend/internal/validator/hosts.go b/backend/internal/validator/hosts.go index f92b6a80..0eb191a8 100644 --- a/backend/internal/validator/hosts.go +++ b/backend/internal/validator/hosts.go @@ -12,27 +12,27 @@ import ( // ValidateHost will check if associated objects exist and other checks // will return a nil error if things are OK func ValidateHost(h host.Model) error { - if h.CertificateID > 0 { + if h.CertificateID.Uint > 0 { // Check certificate exists and is valid // This will not determine if the certificate is Ready to use, // as this validation only cares that the row exists. - if _, cErr := certificate.GetByID(h.CertificateID); cErr != nil { - return eris.Wrapf(cErr, "Certificate #%d does not exist", h.CertificateID) + if _, cErr := certificate.GetByID(h.CertificateID.Uint); cErr != nil { + return eris.Wrapf(cErr, "Certificate #%d does not exist", h.CertificateID.Uint) } } - if h.UpstreamID > 0 { + if h.UpstreamID.Uint > 0 { // Check upstream exists - if _, uErr := upstream.GetByID(h.UpstreamID); uErr != nil { - return eris.Wrapf(uErr, "Upstream #%d does not exist", h.UpstreamID) + if _, uErr := upstream.GetByID(h.UpstreamID.Uint); uErr != nil { + return eris.Wrapf(uErr, "Upstream #%d does not exist", h.UpstreamID.Uint) } } // Ensure either UpstreamID is set or appropriate proxy host params are set - if h.UpstreamID > 0 && (h.ProxyHost != "" || h.ProxyPort > 0) { + if h.UpstreamID.Uint > 0 && (h.ProxyHost != "" || h.ProxyPort > 0) { return eris.Errorf("Proxy Host or Port cannot be set when using an Upstream") } - if h.UpstreamID == 0 && (h.ProxyHost == "" || h.ProxyPort < 1) { + if h.UpstreamID.Uint == 0 && (h.ProxyHost == "" || h.ProxyPort < 1) { return eris.Errorf("Proxy Host and Port must be specified, unless using an Upstream") }