diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 7f0d08570..be373de5d 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -1,4 +1,17 @@ --- +version: "2" +run: + go: "1.24" + # If set, we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the + # implicit automatic updating of go.mod described above. Instead, it + # fails when any changes to go.mod are needed. This setting is most + # useful to check that go.mod does not need updates, such as in a + # continuous integration and testing system. + # + # Allowed values: readonly|vendor|mod + # Default: "" + modules-download-mode: readonly linters: enable: # Prevents against memory leaks in production caused by not closing @@ -7,42 +20,19 @@ linters: # Detects cloned code. DRY is good programming practice. Can cause issues # with testing code where simplicity is preferred over duplication. # Disabled for test code. - # - dupl - # Detects unchecked errors in go programs. These unchecked errors can be - # critical bugs in some cases. - - errcheck - # Simplifies go code. - - gosimple - # Controls Go package import order and makes it always deterministic. - - gci - # Reports suspicious constructs, maintained by goteam. e.g. Printf unused - # params not caught at compile time. - - govet - # Detect security issues with gocode. Use of secrets in code or obsolete - # security algorithms. It's imaged heuristic methods are used in finding - # problems. If issues with rules are found particular rules can be disabled - # as required. Could possibility cause issues with testing. - # Disabled for test code. - - gosec - # Detect repeated strings that could be replaced by a constant + - dupl - goconst # Misc linters missing from other projects. Grouped into 3 categories # diagnostics, style and performance - gocritic # Limits code cyclomatic complexity - gocyclo - # Detects if code needs to be gofmt'd - - gofmt - # Detects unused go package imports - - goimports - # Detects style mistakes not correctness. Golint is meant to carry out the - # stylistic conventions put forth in Effective Go and CodeReviewComments. - # golint has false positives and false negatives and can be tweaked. - - revive - # Detects ineffectual assignments in code - - ineffassign - # Reports long lines - # - lll + # Detect security issues with gocode. Use of secrets in code or obsolete + # security algorithms. It's imaged heuristic methods are used in finding + # problems. If issues with rules are found particular rules can be disabled + # as required. Could possibility cause issues with testing. + # Disabled for test code. + - gosec # Detect commonly misspelled english words in comments - misspell # Detect naked returns on non-trivial functions, and conform with @@ -50,117 +40,138 @@ linters: - nakedret # Detect slice allocations that can be preallocated - prealloc - # Misc collection of static analysis tools - - staticcheck - # Detects unused struct fields - # - structcheck - # Parses and typechecks the code like the go compiler - - typecheck - # Detects unused constants, variables, functions and types - - unused + # Detects style mistakes not correctness. Golint is meant to carry out the + # stylistic conventions put forth in Effective Go and CodeReviewComments. + # golint has false positives and false negatives and can be tweaked. + - revive # Remove unnecessary type conversions - unconvert # Remove unnecessary(unused) function parameters - unparam -linters-settings: - errcheck: - exclude-functions: - - fmt.Fprint - - fmt.Fprintf - gci: - sections: - - standard # Standard section: captures all standard packages. - - localmodule # Local module section: contains all local packages. - # - prefix(gogs.jc21.com) # Prefixed gerrit.lan packages (jumgo). - - default # Everything else (github.com, golang.org, etc). - - blank # Blank section: contains all blank imports. - custom-order: true - goconst: - # minimal length of string constant - # default: 3 - min-len: 2 - # minimum number of occurrences of string constant - # default: 3 - min-occurences: 2 - revive: - enable-all-rules: true + settings: + # Detects unchecked errors in go programs. These unchecked errors can be + # critical bugs in some cases. + errcheck: + exclude-functions: + - fmt.Fprint + - fmt.Fprintf + # Detect repeated strings that could be replaced by a constant + goconst: + # minimal length of string constant + # default: 3 + min-len: 2 + gosec: + excludes: + - G201 # Allow formatted string for queries + revive: + enable-all-rules: true + rules: + # handled by goconst + - name: add-constant + disabled: true + # cant limit this arbitrarily + - name: argument-limit + disabled: true + # handled by gocyclo + - name: cognitive-complexity + disabled: true + # false positive for Exported vs non-exported functions of the same name + - name: confusing-naming + disabled: true + # false positives for "" - which is the nil value of a string (also 0) + - name: confusing-results + disabled: true + # handled by gocyclo + - name: cyclomatic + disabled: true + # some cli programs want to exit on error + - name: deep-exit + disabled: true + # have comments on exported functions but not on vars/types/constants + - name: exported + arguments: + - disableChecksOnConstants + - disableChecksOnTypes + - disableChecksOnVariables + # false positives on bool params + - name: flag-parameter + disabled: true + # extreme verticalization can happen + - name: function-length + disabled: true + # return as many as you like + - name: function-result-limit + disabled: true + # can false positive for non-getters + - name: get-return + disabled: true + # only allows lowercase names + - name: import-alias-naming + disabled: true + # handled by lll + - name: line-length-limit + disabled: true + # handled by gocyclo + - name: max-control-nesting + disabled: true + # don't want to arbitrarily limit this + # many places have specific model.go files to contain all structs + - name: max-public-structs + disabled: true + # disable package-comments + - name: package-comments + disabled: true + # allow ignoring assertion result + - name: unchecked-type-assertion + arguments: + - acceptIgnoredAssertionResult: true + # this is handled by errcheck + - name: unhandled-error + disabled: true + - name: enforce-switch-style + disabled: true + - name: package-naming + disabled: true + exclusions: + generated: lax rules: - - name: unchecked-type-assertion - disabled: true - # handled by goconst - - name: add-constant - disabled: true - # cant limit this arbitrarily - - name: argument-limit - disabled: true - # handled by gocyclo - - name: cognitive-complexity - disabled: true - # false positive for Exported vs non-exported functions of the same name - - name: confusing-naming - disabled: true - # false positives for "" - which is the nil value of a string (also 0) - - name: confusing-results - disabled: true - # handled by gocyclo - - name: cyclomatic - disabled: true - # have comments on exported functions but not on vars/types/constants - - name: exported - arguments: - - "disableChecksOnConstants" - - "disableChecksOnTypes" - - "disableChecksOnVariables" - # false positives on bool params - - name: flag-parameter - disabled: true - # extreme verticalization can happen - - name: function-length - disabled: true - # can false positive for non-getters - - name: get-return - disabled: true - # only allows lowercase names - - name: import-alias-naming - disabled: true - # handled by lll - - name: line-length-limit - disabled: true - # don't want to arbitrarily limit this - # many places have specific model.go files to contain all structs - - name: max-public-structs - disabled: true - # disable package-comments - - name: package-comments - disabled: true - # this is handled by errcheck - - name: unhandled-error - disabled: true - - name: function-result-limit - disabled: true -issues: - # Maximum count of issues with the same text. Set to 0 to disable. Default - # is 3. We have chosen an arbitrary value that works based on practical usage. - max-same: 20 - # See cmdline flag documentation for more info about default excludes - # --exclude-use-default. Nothing is excluded by default - exclude-use-default: false - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - # TODO: Add examples why this is good - - path: _test\.go - linters: - # Tests should be simple? Add example why this is good? - - gocyclo - # Error checking adds verbosity and complexity for minimal value - - errcheck - # Table test encourage duplication in defining the table tests. - - dupl - # Hard coded example tokens, SQL injection and other bad practices may - # want to be tested - - gosec - # Test data can long - # - lll -run: - go: '1.23' + - linters: + # Table test encourage duplication in defining the table tests. + - dupl + # Test fixtures read better as literals than as constants + - goconst + # Error checking adds verbosity and complexity for minimal value + - errcheck + # Tests should be simple? Add example why this is good? + - gocyclo + # Hard coded example tokens, SQL injection and other bad practices may + # want to be tested + - gosec + # Test data can long + - lll + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + # Controls Go package import order and makes it always deterministic. + - gci + # Detects if code needs to be gofmt'd + - gofmt + settings: + gci: + sections: + - standard # Standard section: captures all standard packages + - localmodule # Local module section contains all local packages + - prefix(gerrit.lan) # Prefixed gerrit.lan packages (jumgo) + - default # Everything else (github.com, golang.org, etc) + - blank # Blank section: contains all blank imports + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/backend/embed/main.go b/backend/embed/main.go index 465b76714..686a94803 100644 --- a/backend/embed/main.go +++ b/backend/embed/main.go @@ -12,6 +12,11 @@ var APIDocFiles embed.FS //go:embed assets var Assets embed.FS +// MigrationFiles is for database schema migrations +// +//go:embed migrations +var MigrationFiles embed.FS + // NginxFiles hold nginx config templates // //go:embed nginx diff --git a/backend/embed/migrations/20260925000000_initial_schema.yml b/backend/embed/migrations/20260925000000_initial_schema.yml new file mode 100644 index 000000000..939695e8d --- /dev/null +++ b/backend/embed/migrations/20260925000000_initial_schema.yml @@ -0,0 +1,224 @@ +# The application's initial schema. +# +# Columns are nullable unless marked otherwise. Two columns use a bounded +# string rather than text because MySQL can't make a TEXT column a primary +# key (capability.name) or give it a literal default (auth.type). They're +# still TEXT on SQLite. +operations: + - createTable: + name: access_list + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: name, type: text } + - { name: meta, type: text } + + - createTable: + name: audit_log + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: object_type, type: text } + - { name: object_id, type: bigint } + - { name: action, type: text } + - { name: meta, type: text } + + - createTable: + name: auth + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: type, type: string, length: 191, default: local } + - { name: identity, type: text } + - { name: secret, type: text } + + - createTable: + name: capability + columns: + - { name: name, type: string, length: 191, primaryKey: true } + + - createTable: + name: certificate + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: type, type: text } + - { name: certificate_authority_id, type: bigint } + - { name: dns_provider_id, type: bigint } + - { name: name, type: text } + - { name: domain_names, type: text } + - { name: expires_on, type: bigint } + - { name: status, type: text } + - { name: error_message, type: text } + - { name: meta, type: text } + - { name: is_ecc, type: boolean } + + - createTable: + name: certificate_authority + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: name, type: text } + - { name: acmesh_server, type: text } + - { name: ca_bundle, type: text } + - { name: max_domains, type: bigint } + - { name: is_wildcard_supported, type: boolean } + - { name: is_readonly, type: boolean } + + - createTable: + name: dns_provider + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: name, type: text } + - { name: acmesh_name, type: text } + - { name: dns_sleep, type: bigint } + - { name: meta, type: text } + + - createTable: + name: host + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: type, type: text } + - { name: nginx_template_id, type: bigint } + - { name: listen_interface, type: text } + - { name: domain_names, type: text } + - { name: upstream_id, type: bigint } + - { name: proxy_scheme, type: text } + - { name: proxy_host, type: text } + - { name: proxy_port, type: bigint } + - { name: certificate_id, type: bigint } + - { name: access_list_id, type: bigint } + - { name: ssl_forced, type: boolean } + - { name: caching_enabled, type: boolean } + - { name: block_exploits, type: boolean } + - { name: allow_websocket_upgrade, type: boolean } + - { name: http2_support, type: boolean } + - { name: hsts_enabled, type: boolean } + - { name: hsts_subdomains, type: boolean } + - { name: paths, type: text } + - { name: advanced_config, type: text } + - { name: status, type: text } + - { name: error_message, type: text } + - { name: is_disabled, type: boolean } + + - createTable: + name: jwt_keys + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: public_key, type: text } + - { name: private_key, type: text } + + - createTable: + name: nginx_template + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: name, type: text } + - { name: type, type: text } + - { name: template, type: text } + + - createTable: + name: setting + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: name, type: text } + - { name: description, type: text } + - { name: value, type: json } + + - createTable: + name: stream + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: expires_on, type: bigint } + - { name: user_id, type: bigint } + - { name: provider, type: text } + - { name: name, type: text } + - { name: domain_names, type: text } + - { name: meta, type: text } + + - createTable: + name: upstream + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: user_id, type: bigint } + - { name: name, type: text } + - { name: nginx_template_id, type: bigint } + - { name: ip_hash, type: boolean } + - { name: ntlm, type: boolean } + - { name: keepalive, type: bigint } + - { name: keepalive_requests, type: bigint } + - { name: keepalive_time, type: text } + - { name: keepalive_timeout, type: text } + - { name: advanced_config, type: text } + - { name: status, type: text } + - { name: error_message, type: text } + + - createTable: + name: upstream_server + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: upstream_id, type: bigint } + - { name: server, type: text } + - { name: weight, type: bigint } + - { name: max_conns, type: bigint } + - { name: max_fails, type: bigint } + - { name: fail_timeout, type: bigint } + - { name: is_backup, type: boolean } + + - createTable: + name: user + columns: + - { name: id, type: bigint, primaryKey: true, autoIncrement: true } + - { name: created_at, type: bigint } + - { name: updated_at, type: bigint } + - { name: is_deleted, type: bigint } + - { name: name, type: text } + - { name: email, type: text } + - { name: is_disabled, type: boolean } + - { name: is_system, type: boolean } + + - createTable: + name: user_has_capability + columns: + - { name: user_id, type: bigint } + - { name: capability_name, type: text } diff --git a/backend/embed/migrations/20260925000001_initial_data.yml b/backend/embed/migrations/20260925000001_initial_data.yml new file mode 100644 index 000000000..86f7f782c --- /dev/null +++ b/backend/embed/migrations/20260925000001_initial_data.yml @@ -0,0 +1,253 @@ +# Seed data for a new database. +# +# created_at/updated_at hold unix epoch milliseconds. polyschema always sends +# insert values as bind parameters, so an SQL expression for "now" can't be +# used as a value. The rows are inserted without them, and the sql operations +# at the end fill them in using each engine's own clock. +operations: + # Capabilities + - insert: + table: capability + rows: + - { name: 'full-admin' } + - { name: 'access-lists.view' } + - { name: 'access-lists.manage' } + - { name: 'audit-log.view' } + - { name: 'certificates.view' } + - { name: 'certificates.manage' } + - { name: 'certificate-authorities.view' } + - { name: 'certificate-authorities.manage' } + - { name: 'dns-providers.view' } + - { name: 'dns-providers.manage' } + - { name: 'hosts.view' } + - { name: 'hosts.manage' } + - { name: 'nginx-templates.view' } + - { name: 'nginx-templates.manage' } + - { name: 'settings.manage' } + - { name: 'streams.view' } + - { name: 'streams.manage' } + - { name: 'users.manage' } + # Default settings + - insert: + table: setting + rows: + - { is_deleted: 0, name: 'default-site', description: 'What to show users who hit your Nginx server by default', value: '"welcome"' } + - { is_deleted: 0, name: 'auth-methods', description: 'Which methods are enabled for authentication', value: '["local"]' } + - { is_deleted: 0, name: 'oauth-auth', description: 'Configuration for OAuth authentication', value: '{}' } + - { is_deleted: 0, name: 'ldap-auth', description: 'Configuration for LDAP authentication', value: '{"host": "", "dn": "", "sync_by": "uid"}' } + # Built-in certificate authorities + - insert: + table: certificate_authority + rows: + - { is_deleted: 0, name: 'ZeroSSL', acmesh_server: 'zerossl', max_domains: 10, is_wildcard_supported: true, is_readonly: true } + - { is_deleted: 0, name: 'Let''s Encrypt', acmesh_server: 'https://acme-v02.api.letsencrypt.org/directory', max_domains: 10, is_wildcard_supported: true, is_readonly: true } + - { is_deleted: 0, name: 'Buypass Go SSL', acmesh_server: 'https://api.buypass.com/acme/directory', max_domains: 5, is_wildcard_supported: false, is_readonly: true } + - { is_deleted: 0, name: 'SSL.com', acmesh_server: 'ssl.com', max_domains: 10, is_wildcard_supported: false, is_readonly: true } + - { is_deleted: 0, name: 'Let''s Encrypt (Testing)', acmesh_server: 'https://acme-staging-v02.api.letsencrypt.org/directory', max_domains: 10, is_wildcard_supported: true, is_readonly: true } + - { is_deleted: 0, name: 'Buypass Go SSL (Testing)', acmesh_server: 'https://api.test4.buypass.no/acme/directory', max_domains: 5, is_wildcard_supported: false, is_readonly: true } + # The system user (id 1, owns the built-in nginx templates) + - insert: + table: user + rows: + - { is_deleted: 0, name: 'System', email: 'system@localhost', is_disabled: false, is_system: true } + # Built-in nginx templates + - insert: + table: nginx_template + rows: + - is_deleted: 0 + user_id: 1 + name: 'Default Proxy Template' + type: 'proxy' + template: | + # ------------------------------------------------------------ + {{#each Host.DomainNames}} + # {{this}} + {{/each}} + # ------------------------------------------------------------ + + server { + {{#if Config.Ipv4}} + listen 80; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:80; + {{/if}} + + {{#if Certificate.ID}} + {{#if Config.Ipv4}} + listen 443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{/if}} + + server_name {{#each Host.DomainNames}}{{this}} {{/each}}; + + {{#if Certificate.ID}} + include conf.d/include/ssl-ciphers.conf; + {{#if Certificate.IsAcme}} + ssl_certificate {{Certificate.Folder}}/fullchain.pem; + ssl_certificate_key {{Certificate.Folder}}/privkey.pem; + {{else}} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{Certicicate.ID}}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{Certificate.ID}}/privkey.pem; + {{/if}} + {{/if}} + + {{#if Host.CachingEnabled}} + include conf.d/include/assets.conf; + {{/if}} + + {{#if Host.BlockExploits}} + include conf.d/include/block-exploits.conf; + {{/if}} + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security "max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload" always; + {{/if}} + # Force SSL + include conf.d/include/force-ssl.conf; + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log /data/logs/host-{{Host.ID}}_access.log proxy; + error_log /data/logs/host-{{Host.ID}}_error.log warn; + + {{Host.AdvancedConfig}} + + # locations ? + + # default location: + location / { + {{#if Host.AccessListID}} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file /data/access/{{Host.AccessListID}}; + # access_list.passauth ? todo + {{/if}} + + # Access Rules ? todo + + # Access checks must...? todo + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security "max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload" always; + {{/if}} + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + {{/if}} + + # Proxy! + add_header X-Served-By $host; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_http_version 1.1; + + {{#if Upstream.ID}} + # upstream + proxy_pass {{Host.ProxyScheme}}://npm_upstream_{{Upstream.ID}}; + {{else}} + # proxy a single host + proxy_pass {{Host.ProxyScheme}}://{{Host.ProxyHost}}:{{Host.ProxyPort}}; + {{/if}} + } + + # Legacy Custom Configuration + include /data/nginx/custom/server_proxy[.]conf; + } + - is_deleted: 0 + user_id: 1 + name: 'Default Redirect Template' + type: 'redirect' + template: '# this is a redirect template' + - is_deleted: 0 + user_id: 1 + name: 'Default Dead Template' + type: 'dead' + template: '# this is a dead template' + - is_deleted: 0 + user_id: 1 + name: 'Default Stream Template' + type: 'stream' + template: '# this is a stream template' + - is_deleted: 0 + user_id: 1 + name: 'Default Upstream Template' + type: 'upstream' + template: | + # ------------------------------------------------------------ + # Upstream {{Upstream.ID}}: {{Upstream.Name}} + # ------------------------------------------------------------ + + upstream npm_upstream_{{Upstream.ID}} { + + {{#if Upstream.IPHash~}} + ip_hash; + {{~/if}} + + {{#if Upstream.NTLM~}} + ntlm; + {{~/if}} + + {{#if Upstream.Keepalive~}} + keepalive {{Upstream.Keepalive}}; + {{~/if}} + + {{#if Upstream.KeepaliveRequests~}} + keepalive_requests {{Upstream.KeepaliveRequests}}; + {{~/if}} + + {{#if Upstream.KeepaliveTime~}} + keepalive_time {{Upstream.KeepaliveTime}}; + {{~/if}} + + {{#if Upstream.KeepaliveTimeout~}} + keepalive_timeout {{Upstream.KeepaliveTimeout}}; + {{~/if}} + + {{Upstream.AdvancedConfig}} + + {{#each Upstream.Servers~}} + {{#unless IsDeleted~}} + server {{Server}} {{#if Weight}}weight={{Weight}} {{/if}}{{#if MaxConns}}max_conns={{MaxConns}} {{/if}}{{#if MaxFails}}max_fails={{MaxFails}} {{/if}}{{#if FailTimeout}}fail_timeout={{FailTimeout}} {{/if}}{{#if Backup}}backup{{/if}}; + {{/unless}} + {{/each}} + } + + # Timestamps for everything above + - sql: + postgres: 'UPDATE "setting" SET created_at = (extract(epoch from clock_timestamp()) * 1000)::bigint, updated_at = (extract(epoch from clock_timestamp()) * 1000)::bigint' + mysql: 'UPDATE `setting` SET created_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED), updated_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED)' + sqlite: 'UPDATE "setting" SET created_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER), updated_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER)' + - sql: + postgres: 'UPDATE "certificate_authority" SET created_at = (extract(epoch from clock_timestamp()) * 1000)::bigint, updated_at = (extract(epoch from clock_timestamp()) * 1000)::bigint' + mysql: 'UPDATE `certificate_authority` SET created_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED), updated_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED)' + sqlite: 'UPDATE "certificate_authority" SET created_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER), updated_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER)' + - sql: + postgres: 'UPDATE "user" SET created_at = (extract(epoch from clock_timestamp()) * 1000)::bigint, updated_at = (extract(epoch from clock_timestamp()) * 1000)::bigint' + mysql: 'UPDATE `user` SET created_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED), updated_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED)' + sqlite: 'UPDATE "user" SET created_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER), updated_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER)' + - sql: + postgres: 'UPDATE "nginx_template" SET created_at = (extract(epoch from clock_timestamp()) * 1000)::bigint, updated_at = (extract(epoch from clock_timestamp()) * 1000)::bigint' + mysql: 'UPDATE `nginx_template` SET created_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED), updated_at = CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED)' + sqlite: 'UPDATE "nginx_template" SET created_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER), updated_at = CAST((julianday(''now'') - 2440587.5) * 86400000 AS INTEGER)' diff --git a/backend/go.mod b/backend/go.mod index 4b4ab4b63..70c7d007e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module npm -go 1.26.0 +go 1.27.1 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 @@ -12,11 +12,11 @@ require ( github.com/go-chi/chi/v5 v5.3.2 github.com/go-chi/cors v1.2.2 github.com/go-chi/jwtauth/v5 v5.4.0 - github.com/go-gormigrate/gormigrate/v2 v2.1.7 github.com/go-ldap/ldap/v3 v3.4.14 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jc21/go-sse v1.7.0 github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96 + github.com/jc21/polyschema v0.0.0-20260925050705-5d30b3cbbc99 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/qri-io/jsonschema v0.2.1 github.com/rotisserie/eris v0.5.4 @@ -34,19 +34,19 @@ require ( ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/glebarez/go-sqlite v1.23.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.10.0 // indirect + github.com/jackc/pgx/v5 v5.11.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -60,21 +60,22 @@ require ( github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/qri-io/jsonpointer v0.1.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/stretchr/objx v0.5.3 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/sync v0.23.0 // indirect golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.42.0 // indirect - modernc.org/libc v1.61.0 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.74.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.55.0 // indirect ) replace modernc.org/sqlite => gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 diff --git a/backend/go.sum b/backend/go.sum index b4fcee8d6..432838ab2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,5 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -23,8 +23,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= -github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/go-sqlite v1.23.0 h1:FyhIq4jqmgphQAUlY79zPldYGwISEZikaDfhiGWkkaI= +github.com/glebarez/go-sqlite v1.23.0/go.mod h1:IIYrOH3L0rHY3jb4IXOHoWdklNajSGUN2eJcvK8WrnI= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ= @@ -35,12 +35,10 @@ github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTwYI= github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ= -github.com/go-gormigrate/gormigrate/v2 v2.1.7 h1:PdT4jVPbRb4R+0Ey2R0yJOdctVf4Whiq1Qi4necaZdg= -github.com/go-gormigrate/gormigrate/v2 v2.1.7/go.mod h1:3ouXglTuPrKF5+7cQyVGfvAXTU4vLMaYh9+EPl03uog= github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs= github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -49,24 +47,28 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= -github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jc21/go-sse v1.7.0 h1:Lavb7FGssS2UdJMK1P3r4rQJgg2JZx7BINi8pd/QzKg= github.com/jc21/go-sse v1.7.0/go.mod h1:dbA0LtDgvSEhlAXz+meN1KAcdvcCLiF0aVdhjhZjbGI= github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96 h1:LzBXyNdALQHC7DUy8PE7IFTihmnSRop4Ps2cBIr/WP8= github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96/go.mod h1:Vno1sw0yqqImXnYy3q6ueIG+h+Vwwfbkfw9DTHCrHfY= +github.com/jc21/polyschema v0.0.0-20260925050705-5d30b3cbbc99 h1:HOjJaUJjZehxSMoLxhg8sMnTkk/5ncFy+5gYnijZg60= +github.com/jc21/polyschema v0.0.0-20260925050705-5d30b3cbbc99/go.mod h1:H8UH9o8FM1uvKiA8kd9daeeKLC5x5xxqiPtyIYoxFns= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -86,8 +88,11 @@ github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= @@ -111,12 +116,12 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= @@ -131,6 +136,8 @@ github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97W github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -165,8 +172,6 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= @@ -184,9 +189,12 @@ golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= @@ -205,25 +213,29 @@ gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU= gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk= -modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= -modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= -modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= -modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= -modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= +modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= +modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/internal/api/handler/access_lists.go b/backend/internal/api/handler/access_lists.go index 6a8335d13..6d5e81887 100644 --- a/backend/internal/api/handler/access_lists.go +++ b/backend/internal/api/handler/access_lists.go @@ -2,32 +2,17 @@ package handler import ( "encoding/json" - "fmt" "net/http" c "npm/internal/api/context" h "npm/internal/api/http" - "npm/internal/api/middleware" "npm/internal/entity/accesslist" ) // GetAccessLists will return a list of Access Lists // Route: GET /access-lists func GetAccessLists() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - pageInfo, err := getPageInfoFromRequest(r) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - items, err := accesslist.List(pageInfo, middleware.GetFiltersFromContext(r)) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, items) - } - } + return listHandler(accesslist.List) } // GetAccessList will return a single access list @@ -54,25 +39,7 @@ func GetAccessList() func(http.ResponseWriter, *http.Request) { // Route: POST /access-lists func CreateAccessList() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - - var newItem accesslist.Model - err := json.Unmarshal(bodyBytes, &newItem) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - // Get userID from token - userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) - newItem.UserID = userID - - if err = newItem.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Access List: %s", err.Error()), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, newItem) + createUserOwned(w, r, &accesslist.Model{}, "Access List") } } diff --git a/backend/internal/api/handler/auth.go b/backend/internal/api/handler/auth.go index e16183e8d..97b7289e2 100644 --- a/backend/internal/api/handler/auth.go +++ b/backend/internal/api/handler/auth.go @@ -231,7 +231,7 @@ func RefreshToken() func(http.ResponseWriter, *http.Request) { // Route: POST /auth/sse func NewSSEToken() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - userID := r.Context().Value(c.UserIDCtxKey).(uint) + userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) // Find user userObj, userErr := user.GetByID(userID) diff --git a/backend/internal/api/handler/crud.go b/backend/internal/api/handler/crud.go new file mode 100644 index 000000000..ca617532a --- /dev/null +++ b/backend/internal/api/handler/crud.go @@ -0,0 +1,169 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity" + "npm/internal/model" + + "gorm.io/gorm" +) + +// userOwned is an entity that belongs to the user that created it +type userOwned interface { + SetUserID(userID uint) + Save() error +} + +// createUserOwned decodes the request body into item, assigns it to the +// requesting user and saves it. label is used in the error message. +func createUserOwned(w http.ResponseWriter, r *http.Request, item userOwned, label string) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + if err := json.Unmarshal(bodyBytes, item); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) + item.SetUserID(userID) + + if err := item.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save %s: %s", label, err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, item) +} + +// getNginxConfig returns a handler that loads an entity by the ID in the +// paramName url param and responds with its nginx config from disk, as +// plain text when format is "text" or JSON otherwise +func getNginxConfig[T any]( + format string, + paramName string, + getByID func(uint) (T, error), + getContent func(T) (string, error), +) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + id, err := getURLParamInt(r, paramName) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := getByID(id) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // Get the config from disk + content, nErr := getContent(item) + if nErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, nErr.Error(), nil) + return + } + if format == "text" { + h.ResultResponseText(w, http.StatusOK, content) + return + } + h.ResultResponseJSON(w, r, http.StatusOK, content) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// getByIDHandler returns a handler that responds with the entity loaded by +// the ID in the paramName url param +func getByIDHandler[T any](paramName string, getByID func(uint) (T, error)) func(http.ResponseWriter, *http.Request) { + return withEntity(paramName, getByID, func(w http.ResponseWriter, r *http.Request, item *T) { + h.ResultResponseJSON(w, r, http.StatusOK, item) + }) +} + +// updateByIDHandler returns a handler that loads the entity by the ID in the +// paramName url param, decodes the request body over it and saves it +func updateByIDHandler[T any]( + paramName string, + getByID func(uint) (T, error), + save func(*T) error, +) func(http.ResponseWriter, *http.Request) { + return withEntity(paramName, getByID, func(w http.ResponseWriter, r *http.Request, item *T) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + if err := json.Unmarshal(bodyBytes, item); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err := save(item); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, item) + }) +} + +// deleteByIDHandler returns a handler that loads the entity by the ID in the +// paramName url param, deletes it and responds with the result of del +func deleteByIDHandler[T, R any]( + paramName string, + getByID func(uint) (T, error), + del func(*T) R, +) func(http.ResponseWriter, *http.Request) { + return withEntity(paramName, getByID, func(w http.ResponseWriter, r *http.Request, item *T) { + h.ResultResponseJSON(w, r, http.StatusOK, del(item)) + }) +} + +// withEntity returns a handler that loads the entity by the ID in the +// paramName url param and passes it to fn, responding with not found or +// bad request when it can't be loaded +func withEntity[T any]( + paramName string, + getByID func(uint) (T, error), + fn func(http.ResponseWriter, *http.Request, *T), +) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + id, err := getURLParamInt(r, paramName) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := getByID(id) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + fn(w, r, &item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// listHandler returns a handler that responds with a page of entities +// matching the request's pagination and filters +func listHandler(list func(model.PageInfo, []model.Filter) (entity.ListResponse, error)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + items, err := list(pageInfo, middleware.GetFiltersFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, items) + } + } +} diff --git a/backend/internal/api/handler/dns_providers.go b/backend/internal/api/handler/dns_providers.go index 517d14bee..3b6a5954b 100644 --- a/backend/internal/api/handler/dns_providers.go +++ b/backend/internal/api/handler/dns_providers.go @@ -1,144 +1,44 @@ package handler import ( - "encoding/json" - "fmt" "net/http" - c "npm/internal/api/context" h "npm/internal/api/http" - "npm/internal/api/middleware" "npm/internal/dnsproviders" "npm/internal/entity/dnsprovider" "npm/internal/errors" - - "gorm.io/gorm" ) // GetDNSProviders will return a list of DNS Providers // Route: GET /dns-providers func GetDNSProviders() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - pageInfo, err := getPageInfoFromRequest(r) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r)) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, items) - } - } + return listHandler(dnsprovider.List) } // GetDNSProvider will return a single DNS Provider // Route: GET /dns-providers/{providerID} func GetDNSProvider() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var providerID uint - if providerID, err = getURLParamInt(r, "providerID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := dnsprovider.GetByID(providerID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - h.ResultResponseJSON(w, r, http.StatusOK, item) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return getByIDHandler("providerID", dnsprovider.GetByID) } // CreateDNSProvider will create a DNS Provider // Route: POST /dns-providers func CreateDNSProvider() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - - var newItem dnsprovider.Model - err := json.Unmarshal(bodyBytes, &newItem) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - // Get userID from token - userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) - newItem.UserID = userID - - if err = newItem.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, newItem) + createUserOwned(w, r, &dnsprovider.Model{}, "DNS Provider") } } // UpdateDNSProvider updates a provider // Route: PUT /dns-providers/{providerID} func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var providerID uint - if providerID, err = getURLParamInt(r, "providerID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := dnsprovider.GetByID(providerID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - err := json.Unmarshal(bodyBytes, &item) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - if err = item.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, item) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return updateByIDHandler("providerID", dnsprovider.GetByID, (*dnsprovider.Model).Save) } // DeleteDNSProvider removes a provider // Route: DELETE /dns-providers/{providerID} func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var providerID uint - if providerID, err = getURLParamInt(r, "providerID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := dnsprovider.GetByID(providerID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return deleteByIDHandler("providerID", dnsprovider.GetByID, (*dnsprovider.Model).Delete) } // GetAcmeshProviders will return a list of acme.sh providers diff --git a/backend/internal/api/handler/hosts.go b/backend/internal/api/handler/hosts.go index c902b57db..a41d57f14 100644 --- a/backend/internal/api/handler/hosts.go +++ b/backend/internal/api/handler/hosts.go @@ -172,34 +172,7 @@ func DeleteHost() func(http.ResponseWriter, *http.Request) { // Route: GET /hosts/{hostID}/nginx-config // Route: GET /hosts/{hostID}/nginx-config.txt func GetHostNginxConfig(format string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var hostID uint - if hostID, err = getURLParamInt(r, "hostID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := host.GetByID(hostID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - // Get the config from disk - content, nErr := nginx.GetHostConfigContent(item) - if nErr != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, nErr.Error(), nil) - return - } - if format == "text" { - h.ResultResponseText(w, http.StatusOK, content) - return - } - h.ResultResponseJSON(w, r, http.StatusOK, content) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return getNginxConfig(format, "hostID", host.GetByID, nginx.GetHostConfigContent) } func configureHost(hst host.Model) { diff --git a/backend/internal/api/handler/nginx_templates.go b/backend/internal/api/handler/nginx_templates.go index f5df7e06c..cfcbd6143 100644 --- a/backend/internal/api/handler/nginx_templates.go +++ b/backend/internal/api/handler/nginx_templates.go @@ -1,142 +1,40 @@ +//nolint:dupl // thin wiring of the shared CRUD helpers in crud.go package handler import ( - "encoding/json" - "fmt" "net/http" - c "npm/internal/api/context" - h "npm/internal/api/http" - "npm/internal/api/middleware" "npm/internal/entity/nginxtemplate" - - "gorm.io/gorm" ) // GetNginxTemplates will return a list of Nginx Templates // Route: GET /nginx-templates func GetNginxTemplates() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - pageInfo, err := getPageInfoFromRequest(r) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - items, err := nginxtemplate.List(pageInfo, middleware.GetFiltersFromContext(r)) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, items) - } - } + return listHandler(nginxtemplate.List) } // GetNginxTemplate will return a single Nginx Template // Route: GET /nginx-templates/{templateID} func GetNginxTemplate() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var templateID uint - if templateID, err = getURLParamInt(r, "templateID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := nginxtemplate.GetByID(templateID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - h.ResultResponseJSON(w, r, http.StatusOK, item) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return getByIDHandler("templateID", nginxtemplate.GetByID) } // CreateNginxTemplate will create a Nginx Template // Route: POST /nginx-templates func CreateNginxTemplate() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - - var newNginxTemplate nginxtemplate.Model - err := json.Unmarshal(bodyBytes, &newNginxTemplate) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - // Get userID from token - userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) - newNginxTemplate.UserID = userID - - if err = newNginxTemplate.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Nginx Template: %s", err.Error()), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, newNginxTemplate) + createUserOwned(w, r, &nginxtemplate.Model{}, "Nginx Template") } } // UpdateNginxTemplate updates a nginx template // Route: PUT /nginx-templates/{templateID} func UpdateNginxTemplate() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var templateID uint - if templateID, err = getURLParamInt(r, "templateID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - // reconfigure, _ := getQueryVarBool(r, "reconfigure", false, false) - - nginxTemplate, err := nginxtemplate.GetByID(templateID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - err := json.Unmarshal(bodyBytes, &nginxTemplate) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - if err = nginxTemplate.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, nginxTemplate) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return updateByIDHandler("templateID", nginxtemplate.GetByID, (*nginxtemplate.Model).Save) } // DeleteNginxTemplate removes a nginx template // Route: DELETE /nginx-templates/{templateID} func DeleteNginxTemplate() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var templateID uint - if templateID, err = getURLParamInt(r, "templateID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := nginxtemplate.GetByID(templateID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return deleteByIDHandler("templateID", nginxtemplate.GetByID, (*nginxtemplate.Model).Delete) } diff --git a/backend/internal/api/handler/not_found.go b/backend/internal/api/handler/not_found.go index 8b92cff5d..2ad8ce107 100644 --- a/backend/internal/api/handler/not_found.go +++ b/backend/internal/api/handler/not_found.go @@ -35,12 +35,13 @@ func NotFound() func(http.ResponseWriter, *http.Request) { } err := tryRead(assetsSub, path, w) - if err == errIsDir { + switch err { + case errIsDir: err = tryRead(assetsSub, defaultFile, w) if err != nil { h.NotFound(w, r) } - } else if err == nil { + case nil: return } diff --git a/backend/internal/api/handler/oauth.go b/backend/internal/api/handler/oauth.go index f88c5b846..5cfce690c 100644 --- a/backend/internal/api/handler/oauth.go +++ b/backend/internal/api/handler/oauth.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/url" - "strings" h "npm/internal/api/http" "npm/internal/entity/auth" @@ -15,17 +14,15 @@ import ( njwt "npm/internal/jwt" "npm/internal/logger" + chiMiddleware "github.com/go-chi/chi/v5/middleware" "gorm.io/gorm" ) -// getRequestIPAddress will use X-FORWARDED-FOR header if it exists -// otherwise it will use RemoteAddr +// getRequestIPAddress will use the client IP set by the router's client IP +// middleware if it exists, otherwise it will use RemoteAddr func getRequestIPAddress(r *http.Request) string { - // this Get is case insensitive - xff := r.Header.Get("X-FORWARDED-FOR") - if xff != "" { - ip, _, _ := strings.Cut(xff, ",") - return strings.TrimSpace(ip) + if ip := chiMiddleware.GetClientIP(r.Context()); ip != "" { + return ip } return r.RemoteAddr } diff --git a/backend/internal/api/handler/streams.go b/backend/internal/api/handler/streams.go index 2b097705f..5aafe1b47 100644 --- a/backend/internal/api/handler/streams.go +++ b/backend/internal/api/handler/streams.go @@ -1,140 +1,40 @@ +//nolint:dupl // thin wiring of the shared CRUD helpers in crud.go package handler import ( - "encoding/json" - "fmt" "net/http" - c "npm/internal/api/context" - h "npm/internal/api/http" - "npm/internal/api/middleware" "npm/internal/entity/stream" - - "gorm.io/gorm" ) // GetStreams will return a list of Streams // Route: GET /hosts/streams func GetStreams() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - pageInfo, err := getPageInfoFromRequest(r) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r)) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, hosts) - } - } + return listHandler(stream.List) } // GetStream will return a single Streams // Route: GET /hosts/streams/{hostID} func GetStream() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var hostID uint - if hostID, err = getURLParamInt(r, "hostID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := stream.GetByID(hostID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - h.ResultResponseJSON(w, r, http.StatusOK, item) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return getByIDHandler("hostID", stream.GetByID) } // CreateStream will create a Stream // Route: POST /hosts/steams func CreateStream() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - - var newHost stream.Model - err := json.Unmarshal(bodyBytes, &newHost) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - // Get userID from token - userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) - newHost.UserID = userID - - if err = newHost.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, newHost) + createUserOwned(w, r, &stream.Model{}, "Stream") } } // UpdateStream updates a stream // Route: PUT /hosts/streams/{hostID} func UpdateStream() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var hostID uint - if hostID, err = getURLParamInt(r, "hostID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - host, err := stream.GetByID(hostID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - err := json.Unmarshal(bodyBytes, &host) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) - return - } - - if err = host.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - h.ResultResponseJSON(w, r, http.StatusOK, host) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return updateByIDHandler("hostID", stream.GetByID, (*stream.Model).Save) } // DeleteStream removes a stream // Route: DELETE /hosts/streams/{hostID} func DeleteStream() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var hostID uint - if hostID, err = getURLParamInt(r, "hostID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := stream.GetByID(hostID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return deleteByIDHandler("hostID", stream.GetByID, (*stream.Model).Delete) } diff --git a/backend/internal/api/handler/upstreams.go b/backend/internal/api/handler/upstreams.go index 4b1a4e785..97af2671e 100644 --- a/backend/internal/api/handler/upstreams.go +++ b/backend/internal/api/handler/upstreams.go @@ -171,34 +171,7 @@ func DeleteUpstream() func(http.ResponseWriter, *http.Request) { // Route: GET /upstreams/{upstreamID}/nginx-config // Route: GET /upstreams/{upstreamID}/nginx-config.txt func GetUpstreamNginxConfig(format string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - var err error - var upstreamID uint - if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - return - } - - item, err := upstream.GetByID(upstreamID) - switch err { - case gorm.ErrRecordNotFound: - h.NotFound(w, r) - case nil: - // Get the config from disk - content, nErr := nginx.GetUpstreamConfigContent(item) - if nErr != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, nErr.Error(), nil) - return - } - if format == "text" { - h.ResultResponseText(w, http.StatusOK, content) - return - } - h.ResultResponseJSON(w, r, http.StatusOK, content) - default: - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } - } + return getNginxConfig(format, "upstreamID", upstream.GetByID, nginx.GetUpstreamConfigContent) } func configureUpstream(u upstream.Model) { diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go index 739bbb3f8..9ac74dac7 100644 --- a/backend/internal/api/middleware/auth.go +++ b/backend/internal/api/middleware/auth.go @@ -70,7 +70,7 @@ func Enforce(permissions ...string) func(http.Handler) http.Handler { var userCapabilities []string if found { - userCapabilities = cacheItem.([]string) + userCapabilities, _ = cacheItem.([]string) } else { // Get from db and store it userCapabilities, err = user.GetCapabilities(userID) diff --git a/backend/internal/api/middleware/body_context_test.go b/backend/internal/api/middleware/body_context_test.go index 70fe71364..d54cc5b18 100644 --- a/backend/internal/api/middleware/body_context_test.go +++ b/backend/internal/api/middleware/body_context_test.go @@ -27,7 +27,7 @@ func TestBodyContext(t *testing.T) { // Create a test handler that checks the context for the body data handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - bodyData := r.Context().Value(c.BodyCtxKey).([]byte) + bodyData, _ := r.Context().Value(c.BodyCtxKey).([]byte) assert.Equal(t, body, bodyData) }) diff --git a/backend/internal/api/middleware/list_query.go b/backend/internal/api/middleware/list_query.go index 9660f70b8..a125a9c80 100644 --- a/backend/internal/api/middleware/list_query.go +++ b/backend/internal/api/middleware/list_query.go @@ -58,16 +58,13 @@ func listQuerySort( return ctx, 0, "" } - // Split sort fields up in to slice - sorts := strings.Split(sortString, ",") - for _, sortItem := range sorts { + // Split sort fields up + for sortItem := range strings.SplitSeq(sortString, ",") { if strings.Contains(sortItem, ".") { theseItems := strings.Split(sortItem, ".") switch strings.ToLower(theseItems[1]) { - case "desc": - fallthrough - case "descending": + case "desc", "descending": theseItems[1] = "DESC" default: theseItems[1] = "ASC" diff --git a/backend/internal/api/middleware/sse_auth.go b/backend/internal/api/middleware/sse_auth.go index 9b4a508ec..3f2852296 100644 --- a/backend/internal/api/middleware/sse_auth.go +++ b/backend/internal/api/middleware/sse_auth.go @@ -38,8 +38,8 @@ func SSEAuth(next http.Handler) http.Handler { return } - iss, _ := token.Get("iss") - if iss != "sse" { + iss, ok := token.Issuer() + if !ok || iss != "sse" { h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) return } diff --git a/backend/internal/api/resource_routes.go b/backend/internal/api/resource_routes.go new file mode 100644 index 000000000..0303f9190 --- /dev/null +++ b/backend/internal/api/resource_routes.go @@ -0,0 +1,63 @@ +package api + +import ( + "net/http" + + "npm/internal/api/middleware" + + "github.com/go-chi/chi/v5" +) + +// resourceRoutes describes the standard list/create/get/update/delete +// endpoints for an entity +type resourceRoutes struct { + // idParam is the url param name for a specific item, eg "hostID" + idParam string + // model is the entity used to validate list query sorting and filtering + model any + // viewCapability is required to list and get items + viewCapability string + // manageCapability is required to create, update and delete items + manageCapability string + createSchema string + // updateSchema is optional, the request body isn't validated when empty + updateSchema string + list http.HandlerFunc + create http.HandlerFunc + get http.HandlerFunc + update http.HandlerFunc + del http.HandlerFunc + // manageItem optionally registers extra routes on a specific item that + // require manageCapability + manageItem func(chi.Router) +} + +// mount registers the resource's routes on r +func (rr resourceRoutes) mount(r chi.Router) { + // List + r.With( + middleware.Enforce(rr.viewCapability), + middleware.ListQuery(rr.model), + ).Get("/", rr.list) + + // Create + r.With(middleware.Enforce(rr.manageCapability), middleware.EnforceRequestSchema(rr.createSchema)). + Post("/", rr.create) + + // Specific Item + r.Route("/{"+rr.idParam+":[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(rr.viewCapability)). + Get("/", rr.get) + r.With(middleware.Enforce(rr.manageCapability)).Route("/", func(r chi.Router) { + r.Delete("/", rr.del) + if rr.updateSchema != "" { + r.With(middleware.EnforceRequestSchema(rr.updateSchema)).Put("/", rr.update) + } else { + r.Put("/", rr.update) + } + if rr.manageItem != nil { + rr.manageItem(r) + } + }) + }) +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 869fc8987..fd9d18ffa 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -53,7 +53,9 @@ func NewRouter() http.Handler { corss.Handler, middleware.Cors(r), middleware.Options(r), - chiMiddleware.RealIP, + // nginx in front of the api overwrites X-Forwarded-For with the + // connecting address, so the last value is the only trusted one + chiMiddleware.ClientIPFromHeader("X-Forwarded-For"), chiMiddleware.Recoverer, chiMiddleware.Throttle(5), middleware.PrettyPrint, @@ -170,51 +172,35 @@ func applyRoutes(r chi.Router) chi.Router { }) // Access Lists - r.With(middleware.EnforceSetup()).Route("/access-lists", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityAccessListsView), - middleware.ListQuery(accesslist.Model{}), - ).Get("/", handler.GetAccessLists()) - - // Create - r.With(middleware.Enforce(user.CapabilityAccessListsManage), middleware.EnforceRequestSchema(schema.CreateAccessList())). - Post("/", handler.CreateAccessList()) - - // Specific Item - r.Route("/{accessListID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityAccessListsView)). - Get("/", handler.GetAccessList()) - r.With(middleware.Enforce(user.CapabilityAccessListsManage)).Route("/", func(r chi.Router) { - r.Delete("/{accessListID:[0-9]+}", handler.DeleteAccessList()) - r.With(middleware.EnforceRequestSchema(schema.UpdateAccessList())). - Put("/{accessListID:[0-9]+}", handler.UpdateAccessList()) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/access-lists", resourceRoutes{ + idParam: "accessListID", + model: accesslist.Model{}, + viewCapability: user.CapabilityAccessListsView, + manageCapability: user.CapabilityAccessListsManage, + createSchema: schema.CreateAccessList(), + updateSchema: schema.UpdateAccessList(), + list: handler.GetAccessLists(), + create: handler.CreateAccessList(), + get: handler.GetAccessList(), + update: handler.UpdateAccessList(), + del: handler.DeleteAccessList(), + }.mount) // DNS Providers r.With(middleware.EnforceSetup()).Route("/dns-providers", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityDNSProvidersView), - middleware.ListQuery(dnsprovider.Model{}), - ).Get("/", handler.GetDNSProviders()) - - // Create - r.With(middleware.Enforce(user.CapabilityDNSProvidersManage), middleware.EnforceRequestSchema(schema.CreateDNSProvider())). - Post("/", handler.CreateDNSProvider()) - - // Specific Item - r.Route("/{providerID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityDNSProvidersView)). - Get("/{providerID:[0-9]+}", handler.GetDNSProvider()) - r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).Route("/", func(r chi.Router) { - r.Delete("/", handler.DeleteDNSProvider()) - r.With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())). - Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider()) - }) - }) + resourceRoutes{ + idParam: "providerID", + model: dnsprovider.Model{}, + viewCapability: user.CapabilityDNSProvidersView, + manageCapability: user.CapabilityDNSProvidersManage, + createSchema: schema.CreateDNSProvider(), + updateSchema: schema.UpdateDNSProvider(), + list: handler.GetDNSProviders(), + create: handler.CreateDNSProvider(), + get: handler.GetDNSProvider(), + update: handler.UpdateDNSProvider(), + del: handler.DeleteDNSProvider(), + }.mount(r) // List Acme DNS Providers r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).Route("/acmesh", func(r chi.Router) { @@ -224,161 +210,106 @@ func applyRoutes(r chi.Router) chi.Router { }) // Certificate Authorities - r.With(middleware.EnforceSetup()).Route("/certificate-authorities", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityCertificateAuthoritiesView), - middleware.ListQuery(certificateauthority.Model{}), - ).Get("/", handler.GetCertificateAuthorities()) - - // Create - r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage), middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())). - Post("/", handler.CreateCertificateAuthority()) - - // Specific Item - r.Route("/{caID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView)). - Get("/", handler.GetCertificateAuthority()) - - r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())). - Put("/", handler.UpdateCertificateAuthority()) - r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)). - Delete("/", handler.DeleteCertificateAuthority()) - - r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).Route("/", func(r chi.Router) { - r.Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority()) - r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())). - Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority()) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/certificate-authorities", resourceRoutes{ + idParam: "caID", + model: certificateauthority.Model{}, + viewCapability: user.CapabilityCertificateAuthoritiesView, + manageCapability: user.CapabilityCertificateAuthoritiesManage, + createSchema: schema.CreateCertificateAuthority(), + updateSchema: schema.UpdateCertificateAuthority(), + list: handler.GetCertificateAuthorities(), + create: handler.CreateCertificateAuthority(), + get: handler.GetCertificateAuthority(), + update: handler.UpdateCertificateAuthority(), + del: handler.DeleteCertificateAuthority(), + }.mount) // Certificates - r.With(middleware.EnforceSetup()).Route("/certificates", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityCertificatesView), - middleware.ListQuery(certificate.Model{}), - ).Get("/", handler.GetCertificates()) - - // Create - r.With(middleware.Enforce(user.CapabilityCertificatesManage), middleware.EnforceRequestSchema(schema.CreateCertificate())). - Post("/", handler.CreateCertificate()) - - // Specific Item - r.Route("/{certificateID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityCertificatesView)). - Get("/", handler.GetCertificate()) - r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Route("/", func(r chi.Router) { - r.Delete("/", handler.DeleteCertificate()) - r.Put("/", handler.UpdateCertificate()) - // r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())). - // Put("/", handler.UpdateCertificate()) - r.Post("/renew", handler.RenewCertificate()) - r.Get("/download", handler.DownloadCertificate()) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/certificates", resourceRoutes{ + idParam: "certificateID", + model: certificate.Model{}, + viewCapability: user.CapabilityCertificatesView, + manageCapability: user.CapabilityCertificatesManage, + createSchema: schema.CreateCertificate(), + // updateSchema: schema.UpdateCertificate(), + list: handler.GetCertificates(), + create: handler.CreateCertificate(), + get: handler.GetCertificate(), + update: handler.UpdateCertificate(), + del: handler.DeleteCertificate(), + manageItem: func(r chi.Router) { + r.Post("/renew", handler.RenewCertificate()) + r.Get("/download", handler.DownloadCertificate()) + }, + }.mount) // Hosts - r.With(middleware.EnforceSetup()).Route("/hosts", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityHostsView), - middleware.ListQuery(host.Model{}), - ).Get("/", handler.GetHosts()) - - // Create - r.With(middleware.Enforce(user.CapabilityHostsManage), middleware.EnforceRequestSchema(schema.CreateHost())). - Post("/", handler.CreateHost()) - - // Specific Item - r.Route("/{hostID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityHostsView)). - Get("/", handler.GetHost()) - r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { - r.Delete("/", handler.DeleteHost()) - r.With(middleware.EnforceRequestSchema(schema.UpdateHost())). - Put("/", handler.UpdateHost()) - r.Get("/nginx-config", handler.GetHostNginxConfig("json")) - r.Get("/nginx-config.txt", handler.GetHostNginxConfig("text")) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/hosts", resourceRoutes{ + idParam: "hostID", + model: host.Model{}, + viewCapability: user.CapabilityHostsView, + manageCapability: user.CapabilityHostsManage, + createSchema: schema.CreateHost(), + updateSchema: schema.UpdateHost(), + list: handler.GetHosts(), + create: handler.CreateHost(), + get: handler.GetHost(), + update: handler.UpdateHost(), + del: handler.DeleteHost(), + manageItem: func(r chi.Router) { + r.Get("/nginx-config", handler.GetHostNginxConfig("json")) + r.Get("/nginx-config.txt", handler.GetHostNginxConfig("text")) + }, + }.mount) // Nginx Templates - r.With(middleware.EnforceSetup()).Route("/nginx-templates", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityNginxTemplatesView), - middleware.ListQuery(nginxtemplate.Model{}), - ).Get("/", handler.GetNginxTemplates()) - - // Create - r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage), middleware.EnforceRequestSchema(schema.CreateNginxTemplate())). - Post("/", handler.CreateNginxTemplate()) - - // Specific Item - r.Route("/{templateID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityNginxTemplatesView)). - Get("/", handler.GetNginxTemplates()) - r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { - r.Delete("/", handler.DeleteNginxTemplate()) - r.With(middleware.EnforceRequestSchema(schema.UpdateNginxTemplate())). - Put("/", handler.UpdateNginxTemplate()) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/nginx-templates", resourceRoutes{ + idParam: "templateID", + model: nginxtemplate.Model{}, + viewCapability: user.CapabilityNginxTemplatesView, + manageCapability: user.CapabilityNginxTemplatesManage, + createSchema: schema.CreateNginxTemplate(), + updateSchema: schema.UpdateNginxTemplate(), + list: handler.GetNginxTemplates(), + create: handler.CreateNginxTemplate(), + get: handler.GetNginxTemplate(), + update: handler.UpdateNginxTemplate(), + del: handler.DeleteNginxTemplate(), + }.mount) // Streams - r.With(middleware.EnforceSetup()).Route("/streams", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityStreamsView), - middleware.ListQuery(stream.Model{}), - ).Get("/", handler.GetStreams()) - - // Create - r.With(middleware.Enforce(user.CapabilityStreamsManage), middleware.EnforceRequestSchema(schema.CreateStream())). - Post("/", handler.CreateStream()) - - // Specific Item - r.Route("/{hostID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityStreamsView)). - Get("/", handler.GetStream()) - r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { - r.Delete("/", handler.DeleteStream()) - r.With(middleware.EnforceRequestSchema(schema.UpdateStream())). - Put("/", handler.UpdateStream()) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/streams", resourceRoutes{ + idParam: "hostID", + model: stream.Model{}, + viewCapability: user.CapabilityStreamsView, + manageCapability: user.CapabilityStreamsManage, + createSchema: schema.CreateStream(), + updateSchema: schema.UpdateStream(), + list: handler.GetStreams(), + create: handler.CreateStream(), + get: handler.GetStream(), + update: handler.UpdateStream(), + del: handler.DeleteStream(), + }.mount) // Upstreams - r.With(middleware.EnforceSetup()).Route("/upstreams", func(r chi.Router) { - // List - r.With( - middleware.Enforce(user.CapabilityHostsView), - middleware.ListQuery(upstream.Model{}), - ).Get("/", handler.GetUpstreams()) - - // Create - r.With(middleware.Enforce(user.CapabilityHostsManage), middleware.EnforceRequestSchema(schema.CreateUpstream())). - Post("/", handler.CreateUpstream()) - - // Specific Item - r.Route("/{upstreamID:[0-9]+}", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityHostsView)). - Get("/", handler.GetUpstream()) - r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { - r.Delete("/", handler.DeleteUpstream()) - r.With(middleware.EnforceRequestSchema(schema.UpdateUpstream())). - Put("/", handler.UpdateUpstream()) - r.Get("/nginx-config", handler.GetUpstreamNginxConfig("json")) - r.Get("/nginx-config.txt", handler.GetUpstreamNginxConfig("text")) - }) - }) - }) + r.With(middleware.EnforceSetup()).Route("/upstreams", resourceRoutes{ + idParam: "upstreamID", + model: upstream.Model{}, + viewCapability: user.CapabilityHostsView, + manageCapability: user.CapabilityHostsManage, + createSchema: schema.CreateUpstream(), + updateSchema: schema.UpdateUpstream(), + list: handler.GetUpstreams(), + create: handler.CreateUpstream(), + get: handler.GetUpstream(), + update: handler.UpdateUpstream(), + del: handler.DeleteUpstream(), + manageItem: func(r chi.Router) { + r.Get("/nginx-config", handler.GetUpstreamNginxConfig("json")) + r.Get("/nginx-config.txt", handler.GetUpstreamNginxConfig("text")) + }, + }.mount) }) return r diff --git a/backend/internal/api/router_test.go b/backend/internal/api/router_test.go index 22ac857fc..aaf88cff3 100644 --- a/backend/internal/api/router_test.go +++ b/backend/internal/api/router_test.go @@ -3,7 +3,6 @@ package api import ( "net/http" "net/http/httptest" - "os" "testing" "npm/internal/config" @@ -21,8 +20,7 @@ var ( // Tear up/down func TestMain(m *testing.M) { config.Init(&version, &commit) - code := m.Run() - os.Exit(code) + m.Run() } func TestGetHealthz(t *testing.T) { diff --git a/backend/internal/dnsproviders/common.go b/backend/internal/dnsproviders/common.go index fef52e16e..9634192f4 100644 --- a/backend/internal/dnsproviders/common.go +++ b/backend/internal/dnsproviders/common.go @@ -6,6 +6,23 @@ import ( "npm/internal/errors" ) +// JSON schema types and common field titles used by providers +const ( + typeString = "string" + typeObject = "object" + typeInteger = "integer" + typeBoolean = "boolean" + + titleAPIKey = "api-key" + titleAPIURL = "api-url" + titleKey = "key" + titlePassword = "password" + titleSecret = "secret" + titleToken = "token" + titleUser = "user" + titleUsername = "username" +) + // providerField should mimick jsonschema, so that // the ui can render a field and validate it // before we do. diff --git a/backend/internal/dnsproviders/dns_acmedns.go b/backend/internal/dnsproviders/dns_acmedns.go index 6730240dc..7f218a154 100644 --- a/backend/internal/dnsproviders/dns_acmedns.go +++ b/backend/internal/dnsproviders/dns_acmedns.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSAcmeDNS() Provider { return Provider{ Title: "dns_acmedns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "ACMEDNS_BASE_URL", @@ -14,19 +14,19 @@ func getDNSAcmeDNS() Provider { Properties: map[string]providerField{ "ACMEDNS_BASE_URL": { Title: "base-url", - Type: "string", + Type: typeString, }, "ACMEDNS_SUBDOMAIN": { Title: "subdomain", - Type: "string", + Type: typeString, }, "ACMEDNS_USERNAME": { - Title: "username", - Type: "string", + Title: titleUsername, + Type: typeString, }, "ACMEDNS_PASSWORD": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, IsSecret: true, }, }, diff --git a/backend/internal/dnsproviders/dns_ad.go b/backend/internal/dnsproviders/dns_ad.go index 243f23ca7..e0d647a8e 100644 --- a/backend/internal/dnsproviders/dns_ad.go +++ b/backend/internal/dnsproviders/dns_ad.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSAd() Provider { return Provider{ Title: "dns_ad", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "AD_API_KEY", }, Properties: map[string]providerField{ "AD_API_KEY": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_ali.go b/backend/internal/dnsproviders/dns_ali.go index e338f2768..ace46728d 100644 --- a/backend/internal/dnsproviders/dns_ali.go +++ b/backend/internal/dnsproviders/dns_ali.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSAli() Provider { return Provider{ Title: "dns_ali", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "Ali_Key", @@ -11,13 +11,13 @@ func getDNSAli() Provider { }, Properties: map[string]providerField{ "Ali_Key": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, }, "Ali_Secret": { - Title: "secret", - Type: "string", + Title: titleSecret, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_autodns.go b/backend/internal/dnsproviders/dns_autodns.go index bb04f7c4e..d27dfde59 100644 --- a/backend/internal/dnsproviders/dns_autodns.go +++ b/backend/internal/dnsproviders/dns_autodns.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSAutoDNS() Provider { return Provider{ Title: "dns_autodns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "AUTODNS_USER", @@ -12,19 +12,19 @@ func getDNSAutoDNS() Provider { }, Properties: map[string]providerField{ "AUTODNS_USER": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "AUTODNS_PASSWORD": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, "AUTODNS_CONTEXT": { Title: "context", - Type: "string", + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_aws.go b/backend/internal/dnsproviders/dns_aws.go index aed069452..3c5c7fb6e 100644 --- a/backend/internal/dnsproviders/dns_aws.go +++ b/backend/internal/dnsproviders/dns_aws.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSAws() Provider { return Provider{ Title: "dns_aws", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "AWS_ACCESS_KEY_ID", @@ -12,18 +12,18 @@ func getDNSAws() Provider { Properties: map[string]providerField{ "AWS_ACCESS_KEY_ID": { Title: "access-key-id", - Type: "string", + Type: typeString, MinLength: 10, }, "AWS_SECRET_ACCESS_KEY": { Title: "secret-access-key", - Type: "string", + Type: typeString, MinLength: 10, IsSecret: true, }, "AWS_DNS_SLOWRATE": { Title: "slow-rate", - Type: "integer", + Type: typeInteger, }, }, } diff --git a/backend/internal/dnsproviders/dns_azure.go b/backend/internal/dnsproviders/dns_azure.go index a8c5a4377..cb527f0bd 100644 --- a/backend/internal/dnsproviders/dns_azure.go +++ b/backend/internal/dnsproviders/dns_azure.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSAzure() Provider { return Provider{ Title: "dns_azure", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "AZUREDNS_SUBSCRIPTIONID", @@ -14,22 +14,22 @@ func getDNSAzure() Provider { Properties: map[string]providerField{ "AZUREDNS_SUBSCRIPTIONID": { Title: "subscription-id", - Type: "string", + Type: typeString, MinLength: 1, }, "AZUREDNS_TENANTID": { Title: "tenant-id", - Type: "string", + Type: typeString, MinLength: 1, }, "AZUREDNS_APPID": { Title: "app-id", - Type: "string", + Type: typeString, MinLength: 1, }, "AZUREDNS_CLIENTSECRET": { Title: "client-secret", - Type: "string", + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_cf.go b/backend/internal/dnsproviders/dns_cf.go index b35695c28..d9d898342 100644 --- a/backend/internal/dnsproviders/dns_cf.go +++ b/backend/internal/dnsproviders/dns_cf.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSCf() Provider { return Provider{ Title: "dns_cf", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "CF_Key", @@ -13,29 +13,29 @@ func getDNSCf() Provider { }, Properties: map[string]providerField{ "CF_Key": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, }, "CF_Email": { Title: "email", - Type: "string", + Type: typeString, MinLength: 5, }, "CF_Token": { - Title: "token", - Type: "string", + Title: titleToken, + Type: typeString, MinLength: 5, IsSecret: true, }, "CF_Account_ID": { Title: "account-id", - Type: "string", + Type: typeString, MinLength: 1, }, "CF_Zone_ID": { Title: "zone-id", - Type: "string", + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_cloudns.go b/backend/internal/dnsproviders/dns_cloudns.go index 297de58de..b4ea098ac 100644 --- a/backend/internal/dnsproviders/dns_cloudns.go +++ b/backend/internal/dnsproviders/dns_cloudns.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSCloudns() Provider { return Provider{ Title: "dns_cloudns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "CLOUDNS_AUTH_ID", @@ -13,17 +13,17 @@ func getDNSCloudns() Provider { Properties: map[string]providerField{ "CLOUDNS_AUTH_ID": { Title: "auth-id", - Type: "string", + Type: typeString, MinLength: 1, }, "CLOUDNS_SUB_AUTH_ID": { Title: "sub-auth-id", - Type: "string", + Type: typeString, MinLength: 1, }, "CLOUDNS_AUTH_PASSWORD": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_conoha.go b/backend/internal/dnsproviders/dns_conoha.go index d84aacd41..21de6bd76 100644 --- a/backend/internal/dnsproviders/dns_conoha.go +++ b/backend/internal/dnsproviders/dns_conoha.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSConoha() Provider { return Provider{ Title: "dns_conoha", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "CONOHA_IdentityServiceApi", @@ -13,24 +13,24 @@ func getDNSConoha() Provider { }, Properties: map[string]providerField{ "CONOHA_IdentityServiceApi": { - Title: "api-url", - Type: "string", + Title: titleAPIURL, + Type: typeString, MinLength: 4, }, "CONOHA_Username": { - Title: "username", - Type: "string", + Title: titleUsername, + Type: typeString, MinLength: 1, }, "CONOHA_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, "CONOHA_TenantId": { Title: "tenant-id", - Type: "string", + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_cx.go b/backend/internal/dnsproviders/dns_cx.go index 2750124cf..242aabf75 100644 --- a/backend/internal/dnsproviders/dns_cx.go +++ b/backend/internal/dnsproviders/dns_cx.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSCx() Provider { return Provider{ Title: "dns_cx", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "CX_Key", @@ -11,12 +11,12 @@ func getDNSCx() Provider { }, Properties: map[string]providerField{ "CX_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, }, "CX_Secret": { - Title: "secret", - Type: "string", + Title: titleSecret, + Type: typeString, IsSecret: true, }, }, diff --git a/backend/internal/dnsproviders/dns_cyon.go b/backend/internal/dnsproviders/dns_cyon.go index 87a5ab379..30cf567f9 100644 --- a/backend/internal/dnsproviders/dns_cyon.go +++ b/backend/internal/dnsproviders/dns_cyon.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSCyon() Provider { return Provider{ Title: "dns_cyon", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "CY_Username", @@ -12,19 +12,19 @@ func getDNSCyon() Provider { }, Properties: map[string]providerField{ "CY_Username": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "CY_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, "CY_OTP_Secret": { Title: "otp-secret", - Type: "string", + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_da.go b/backend/internal/dnsproviders/dns_da.go index ee9e0f613..2959407e6 100644 --- a/backend/internal/dnsproviders/dns_da.go +++ b/backend/internal/dnsproviders/dns_da.go @@ -3,20 +3,20 @@ package dnsproviders func getDNSDa() Provider { return Provider{ Title: "dns_da", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DA_Api", }, Properties: map[string]providerField{ "DA_Api": { - Title: "api-url", - Type: "string", + Title: titleAPIURL, + Type: typeString, MinLength: 4, }, "DA_Api_Insecure": { Title: "insecure", - Type: "boolean", + Type: typeBoolean, }, }, } diff --git a/backend/internal/dnsproviders/dns_dgon.go b/backend/internal/dnsproviders/dns_dgon.go index 91113cceb..a511c9e77 100644 --- a/backend/internal/dnsproviders/dns_dgon.go +++ b/backend/internal/dnsproviders/dns_dgon.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSDgon() Provider { return Provider{ Title: "dns_dgon", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DO_API_KEY", }, Properties: map[string]providerField{ "DO_API_KEY": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, IsSecret: true, }, }, diff --git a/backend/internal/dnsproviders/dns_dnsimple.go b/backend/internal/dnsproviders/dns_dnsimple.go index a4a02ae16..0a37a9b83 100644 --- a/backend/internal/dnsproviders/dns_dnsimple.go +++ b/backend/internal/dnsproviders/dns_dnsimple.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSDNSimple() Provider { return Provider{ Title: "dns_dnsimple", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DNSimple_OAUTH_TOKEN", @@ -11,7 +11,7 @@ func getDNSDNSimple() Provider { Properties: map[string]providerField{ "DNSimple_OAUTH_TOKEN": { Title: "oauth-token", - Type: "string", + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_dp.go b/backend/internal/dnsproviders/dns_dp.go index cece1a013..5926f5715 100644 --- a/backend/internal/dnsproviders/dns_dp.go +++ b/backend/internal/dnsproviders/dns_dp.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSDp() Provider { return Provider{ Title: "dns_dp", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DP_Id", @@ -12,12 +12,12 @@ func getDNSDp() Provider { Properties: map[string]providerField{ "DP_Id": { Title: "id", - Type: "string", + Type: typeString, MinLength: 1, }, "DP_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_dpi.go b/backend/internal/dnsproviders/dns_dpi.go index 0b5255c58..beeffb47e 100644 --- a/backend/internal/dnsproviders/dns_dpi.go +++ b/backend/internal/dnsproviders/dns_dpi.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSDpi() Provider { return Provider{ Title: "dns_dpi", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DPI_Id", @@ -12,12 +12,12 @@ func getDNSDpi() Provider { Properties: map[string]providerField{ "DPI_Id": { Title: "id", - Type: "string", + Type: typeString, MinLength: 1, }, "DPI_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_dreamhost.go b/backend/internal/dnsproviders/dns_dreamhost.go index 037c5f84e..73b0ee319 100644 --- a/backend/internal/dnsproviders/dns_dreamhost.go +++ b/backend/internal/dnsproviders/dns_dreamhost.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSDreamhost() Provider { return Provider{ Title: "dns_dreamhost", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DH_API_KEY", }, Properties: map[string]providerField{ "DH_API_KEY": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_duckdns.go b/backend/internal/dnsproviders/dns_duckdns.go index 3407ca0cb..081f42db1 100644 --- a/backend/internal/dnsproviders/dns_duckdns.go +++ b/backend/internal/dnsproviders/dns_duckdns.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSDuckDNS() Provider { return Provider{ Title: "dns_duckdns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DuckDNS_Token", }, Properties: map[string]providerField{ "DuckDNS_Token": { - Title: "token", - Type: "string", + Title: titleToken, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_dyn.go b/backend/internal/dnsproviders/dns_dyn.go index be595ce31..abdf05d03 100644 --- a/backend/internal/dnsproviders/dns_dyn.go +++ b/backend/internal/dnsproviders/dns_dyn.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSDyn() Provider { return Provider{ Title: "dns_dyn", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "DYN_Customer", @@ -13,17 +13,17 @@ func getDNSDyn() Provider { Properties: map[string]providerField{ "DYN_Customer": { Title: "customer", - Type: "string", + Type: typeString, MinLength: 1, }, "DYN_Username": { - Title: "username", - Type: "string", + Title: titleUsername, + Type: typeString, MinLength: 1, }, "DYN_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_dynu.go b/backend/internal/dnsproviders/dns_dynu.go index 1f3024c19..1187b4284 100644 --- a/backend/internal/dnsproviders/dns_dynu.go +++ b/backend/internal/dnsproviders/dns_dynu.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSDynu() Provider { return Provider{ Title: "dns_dynu", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "Dynu_ClientId", @@ -11,12 +11,12 @@ func getDNSDynu() Provider { Properties: map[string]providerField{ "Dynu_ClientId": { Title: "client-id", - Type: "string", + Type: typeString, MinLength: 1, }, "Dynu_Secret": { - Title: "secret", - Type: "string", + Title: titleSecret, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_euserv.go b/backend/internal/dnsproviders/dns_euserv.go index be500480c..3cbdb73e0 100644 --- a/backend/internal/dnsproviders/dns_euserv.go +++ b/backend/internal/dnsproviders/dns_euserv.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSEuserv() Provider { return Provider{ Title: "dns_euserv", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "EUSERV_Username", @@ -11,13 +11,13 @@ func getDNSEuserv() Provider { }, Properties: map[string]providerField{ "EUSERV_Username": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "EUSERV_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_freedns.go b/backend/internal/dnsproviders/dns_freedns.go index 7c6849b10..3af487a32 100644 --- a/backend/internal/dnsproviders/dns_freedns.go +++ b/backend/internal/dnsproviders/dns_freedns.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSFreeDNS() Provider { return Provider{ Title: "dns_freedns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "FREEDNS_User", @@ -11,13 +11,13 @@ func getDNSFreeDNS() Provider { }, Properties: map[string]providerField{ "FREEDNS_User": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "FREEDNS_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_gandi_livedns.go b/backend/internal/dnsproviders/dns_gandi_livedns.go index a22ebbff6..cc6ba8157 100644 --- a/backend/internal/dnsproviders/dns_gandi_livedns.go +++ b/backend/internal/dnsproviders/dns_gandi_livedns.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSGandiLiveDNS() Provider { return Provider{ Title: "dns_gandi_livedns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "GANDI_LIVEDNS_KEY", }, Properties: map[string]providerField{ "GANDI_LIVEDNS_KEY": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_gd.go b/backend/internal/dnsproviders/dns_gd.go index 11248948f..351fe22cf 100644 --- a/backend/internal/dnsproviders/dns_gd.go +++ b/backend/internal/dnsproviders/dns_gd.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSGd() Provider { return Provider{ Title: "dns_gd", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "GD_Key", @@ -11,13 +11,13 @@ func getDNSGd() Provider { }, Properties: map[string]providerField{ "GD_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, }, "GD_Secret": { - Title: "secret", - Type: "string", + Title: titleSecret, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_he.go b/backend/internal/dnsproviders/dns_he.go index f4aed32bc..ba1b3ad22 100644 --- a/backend/internal/dnsproviders/dns_he.go +++ b/backend/internal/dnsproviders/dns_he.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSHe() Provider { return Provider{ Title: "dns_he", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "HE_Username", @@ -11,13 +11,13 @@ func getDNSHe() Provider { }, Properties: map[string]providerField{ "HE_Username": { - Title: "username", - Type: "string", + Title: titleUsername, + Type: typeString, MinLength: 1, }, "HE_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_infoblox.go b/backend/internal/dnsproviders/dns_infoblox.go index f356f36c5..379a2a41b 100644 --- a/backend/internal/dnsproviders/dns_infoblox.go +++ b/backend/internal/dnsproviders/dns_infoblox.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSInfoblox() Provider { return Provider{ Title: "dns_infoblox", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "Infoblox_Creds", @@ -12,13 +12,13 @@ func getDNSInfoblox() Provider { Properties: map[string]providerField{ "Infoblox_Creds": { Title: "credentials", - Type: "string", + Type: typeString, MinLength: 1, IsSecret: true, }, "Infoblox_Server": { Title: "server", - Type: "string", + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_inwx.go b/backend/internal/dnsproviders/dns_inwx.go index c0f75cca2..451b309cd 100644 --- a/backend/internal/dnsproviders/dns_inwx.go +++ b/backend/internal/dnsproviders/dns_inwx.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSInwx() Provider { return Provider{ Title: "dns_inwx", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "INWX_User", @@ -11,13 +11,13 @@ func getDNSInwx() Provider { }, Properties: map[string]providerField{ "INWX_User": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "INWX_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_ispconfig.go b/backend/internal/dnsproviders/dns_ispconfig.go index f10e0f799..6c6499f7b 100644 --- a/backend/internal/dnsproviders/dns_ispconfig.go +++ b/backend/internal/dnsproviders/dns_ispconfig.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSIspconfig() Provider { return Provider{ Title: "dns_ispconfig", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "ISPC_User", @@ -12,24 +12,24 @@ func getDNSIspconfig() Provider { }, Properties: map[string]providerField{ "ISPC_User": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "ISPC_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, "ISPC_Api": { - Title: "api-url", - Type: "string", + Title: titleAPIURL, + Type: typeString, MinLength: 1, }, "ISPC_Api_Insecure": { Title: "insecure", - Type: "boolean", + Type: typeBoolean, }, }, } diff --git a/backend/internal/dnsproviders/dns_kinghost.go b/backend/internal/dnsproviders/dns_kinghost.go index a497acc8c..807c6f96d 100644 --- a/backend/internal/dnsproviders/dns_kinghost.go +++ b/backend/internal/dnsproviders/dns_kinghost.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSKinghost() Provider { return Provider{ Title: "dns_kinghost", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "KINGHOST_Username", @@ -11,13 +11,13 @@ func getDNSKinghost() Provider { }, Properties: map[string]providerField{ "KINGHOST_Username": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "KINGHOST_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_linode_v4.go b/backend/internal/dnsproviders/dns_linode_v4.go index ddafeca17..3239bbb99 100644 --- a/backend/internal/dnsproviders/dns_linode_v4.go +++ b/backend/internal/dnsproviders/dns_linode_v4.go @@ -5,15 +5,15 @@ package dnsproviders func getDNSLinodeV4() Provider { return Provider{ Title: "dns_linode_v4", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "LINODE_V4_API_KEY", }, Properties: map[string]providerField{ "LINODE_V4_API_KEY": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_loopia.go b/backend/internal/dnsproviders/dns_loopia.go index 64d22d329..adf0763f5 100644 --- a/backend/internal/dnsproviders/dns_loopia.go +++ b/backend/internal/dnsproviders/dns_loopia.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSLoopia() Provider { return Provider{ Title: "dns_loopia", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "LOOPIA_Api", @@ -12,18 +12,18 @@ func getDNSLoopia() Provider { }, Properties: map[string]providerField{ "LOOPIA_Api": { - Title: "api-url", - Type: "string", + Title: titleAPIURL, + Type: typeString, MinLength: 4, }, "LOOPIA_User": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "LOOPIA_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_lua.go b/backend/internal/dnsproviders/dns_lua.go index b79a87a7b..9379388e4 100644 --- a/backend/internal/dnsproviders/dns_lua.go +++ b/backend/internal/dnsproviders/dns_lua.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSLua() Provider { return Provider{ Title: "dns_lua", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "LUA_Key", @@ -11,14 +11,14 @@ func getDNSLua() Provider { }, Properties: map[string]providerField{ "LUA_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, IsSecret: true, }, "LUA_Email": { Title: "email", - Type: "string", + Type: typeString, MinLength: 5, }, }, diff --git a/backend/internal/dnsproviders/dns_me.go b/backend/internal/dnsproviders/dns_me.go index 96b3d4a2a..027e8a664 100644 --- a/backend/internal/dnsproviders/dns_me.go +++ b/backend/internal/dnsproviders/dns_me.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSMe() Provider { return Provider{ Title: "dns_me", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "ME_Key", @@ -11,13 +11,13 @@ func getDNSMe() Provider { }, Properties: map[string]providerField{ "ME_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, }, "ME_Secret": { - Title: "secret", - Type: "string", + Title: titleSecret, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_namecom.go b/backend/internal/dnsproviders/dns_namecom.go index 9a72de6cb..cd7198fe4 100644 --- a/backend/internal/dnsproviders/dns_namecom.go +++ b/backend/internal/dnsproviders/dns_namecom.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSNamecom() Provider { return Provider{ Title: "dns_namecom", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "Namecom_Username", @@ -11,13 +11,13 @@ func getDNSNamecom() Provider { }, Properties: map[string]providerField{ "Namecom_Username": { - Title: "username", - Type: "string", + Title: titleUsername, + Type: typeString, MinLength: 1, }, "Namecom_Token": { - Title: "token", - Type: "string", + Title: titleToken, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_namesilo.go b/backend/internal/dnsproviders/dns_namesilo.go index d44482131..9b37017cb 100644 --- a/backend/internal/dnsproviders/dns_namesilo.go +++ b/backend/internal/dnsproviders/dns_namesilo.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSNamesilo() Provider { return Provider{ Title: "dns_namesilo", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "Namesilo_Key", }, Properties: map[string]providerField{ "Namesilo_Key": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_nsone.go b/backend/internal/dnsproviders/dns_nsone.go index 36c4594ff..c7761e785 100644 --- a/backend/internal/dnsproviders/dns_nsone.go +++ b/backend/internal/dnsproviders/dns_nsone.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSOne() Provider { return Provider{ Title: "dns_nsone", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "NS1_Key", }, Properties: map[string]providerField{ "NS1_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_pdns.go b/backend/internal/dnsproviders/dns_pdns.go index ee3802839..848bf93fc 100644 --- a/backend/internal/dnsproviders/dns_pdns.go +++ b/backend/internal/dnsproviders/dns_pdns.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSPDNS() Provider { return Provider{ Title: "dns_pdns", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "PDNS_Url", @@ -14,22 +14,22 @@ func getDNSPDNS() Provider { Properties: map[string]providerField{ "PDNS_Url": { Title: "url", - Type: "string", + Type: typeString, MinLength: 1, }, "PDNS_ServerId": { Title: "server-id", - Type: "string", + Type: typeString, MinLength: 1, }, "PDNS_Token": { - Title: "token", - Type: "string", + Title: titleToken, + Type: typeString, MinLength: 1, }, "PDNS_Ttl": { Title: "ttl", - Type: "integer", + Type: typeInteger, Minimum: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_selectel.go b/backend/internal/dnsproviders/dns_selectel.go index 7e97dce12..d93b92026 100644 --- a/backend/internal/dnsproviders/dns_selectel.go +++ b/backend/internal/dnsproviders/dns_selectel.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSSelectel() Provider { return Provider{ Title: "dns_selectel", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "SL_Key", }, Properties: map[string]providerField{ "SL_Key": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_servercow.go b/backend/internal/dnsproviders/dns_servercow.go index 007d49d13..c0a1528c6 100644 --- a/backend/internal/dnsproviders/dns_servercow.go +++ b/backend/internal/dnsproviders/dns_servercow.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSServercow() Provider { return Provider{ Title: "dns_servercow", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "SERVERCOW_API_Username", @@ -11,13 +11,13 @@ func getDNSServercow() Provider { }, Properties: map[string]providerField{ "SERVERCOW_API_Username": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, }, "SERVERCOW_API_Password": { - Title: "password", - Type: "string", + Title: titlePassword, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_tele3.go b/backend/internal/dnsproviders/dns_tele3.go index c40b8c7be..9f485f09d 100644 --- a/backend/internal/dnsproviders/dns_tele3.go +++ b/backend/internal/dnsproviders/dns_tele3.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSTele3() Provider { return Provider{ Title: "dns_tele3", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "TELE3_Key", @@ -11,13 +11,13 @@ func getDNSTele3() Provider { }, Properties: map[string]providerField{ "TELE3_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, }, "TELE3_Secret": { - Title: "secret", - Type: "string", + Title: titleSecret, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_unoeuro.go b/backend/internal/dnsproviders/dns_unoeuro.go index 63b6fa3fc..f718d4142 100644 --- a/backend/internal/dnsproviders/dns_unoeuro.go +++ b/backend/internal/dnsproviders/dns_unoeuro.go @@ -3,7 +3,7 @@ package dnsproviders func getDNSUnoeuro() Provider { return Provider{ Title: "dns_unoeuro", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "UNO_Key", @@ -11,14 +11,14 @@ func getDNSUnoeuro() Provider { }, Properties: map[string]providerField{ "UNO_Key": { - Title: "key", - Type: "string", + Title: titleKey, + Type: typeString, MinLength: 1, IsSecret: true, }, "UNO_User": { - Title: "user", - Type: "string", + Title: titleUser, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_vscale.go b/backend/internal/dnsproviders/dns_vscale.go index 86f1cbf5d..d1a41bf27 100644 --- a/backend/internal/dnsproviders/dns_vscale.go +++ b/backend/internal/dnsproviders/dns_vscale.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSVscale() Provider { return Provider{ Title: "dns_vscale", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "VSCALE_API_KEY", }, Properties: map[string]providerField{ "VSCALE_API_KEY": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, }, }, diff --git a/backend/internal/dnsproviders/dns_yandex.go b/backend/internal/dnsproviders/dns_yandex.go index 88ab7b470..0c0a9bb6b 100644 --- a/backend/internal/dnsproviders/dns_yandex.go +++ b/backend/internal/dnsproviders/dns_yandex.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSYandex() Provider { return Provider{ Title: "dns_yandex", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "PDD_Token", }, Properties: map[string]providerField{ "PDD_Token": { - Title: "token", - Type: "string", + Title: titleToken, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_zilore.go b/backend/internal/dnsproviders/dns_zilore.go index b35f21591..b94138515 100644 --- a/backend/internal/dnsproviders/dns_zilore.go +++ b/backend/internal/dnsproviders/dns_zilore.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSDNZilore() Provider { return Provider{ Title: "dns_zilore", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "Zilore_Key", }, Properties: map[string]providerField{ "Zilore_Key": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/dnsproviders/dns_zonomi.go b/backend/internal/dnsproviders/dns_zonomi.go index f93f3ef84..a7c3d0e67 100644 --- a/backend/internal/dnsproviders/dns_zonomi.go +++ b/backend/internal/dnsproviders/dns_zonomi.go @@ -3,15 +3,15 @@ package dnsproviders func getDNSZonomi() Provider { return Provider{ Title: "dns_zonomi", - Type: "object", + Type: typeObject, AdditionalProperties: false, Required: []string{ "ZM_Key", }, Properties: map[string]providerField{ "ZM_Key": { - Title: "api-key", - Type: "string", + Title: titleAPIKey, + Type: typeString, MinLength: 1, IsSecret: true, }, diff --git a/backend/internal/entity/accesslist/model.go b/backend/internal/entity/accesslist/model.go index 64e0a0587..680e7f8a1 100644 --- a/backend/internal/entity/accesslist/model.go +++ b/backend/internal/entity/accesslist/model.go @@ -31,6 +31,11 @@ func (m *Model) LoadByID(id uint) error { return result.Error } +// SetUserID sets the owner of this model +func (m *Model) SetUserID(userID uint) { + m.UserID = userID +} + // Save will save this model to the DB func (m *Model) Save() error { if m.UserID == 0 { diff --git a/backend/internal/entity/auth/oauth.go b/backend/internal/entity/auth/oauth.go index 438eb87ea..82726c23a 100644 --- a/backend/internal/entity/auth/oauth.go +++ b/backend/internal/entity/auth/oauth.go @@ -41,7 +41,8 @@ type OAuthUser struct { func (m *OAuthUser) GetResourceField(field string) string { if m.Resource != nil { if value, ok := m.Resource[field]; ok { - return value.(string) + str, _ := value.(string) + return str } } return "" diff --git a/backend/internal/entity/certificateauthority/entity_test.go b/backend/internal/entity/certificateauthority/entity_test.go index 3aa52f03d..aebe92c67 100644 --- a/backend/internal/entity/certificateauthority/entity_test.go +++ b/backend/internal/entity/certificateauthority/entity_test.go @@ -210,9 +210,7 @@ func (s *testsuite) TestDelete() { assert.Equal(s.T(), "Unable to delete a new object", err.Error()) m2 := Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, } err2 := m2.Delete() require.NoError(s.T(), err2) diff --git a/backend/internal/entity/dnsprovider/entity_test.go b/backend/internal/entity/dnsprovider/entity_test.go index 40c34eabe..e9445a3bc 100644 --- a/backend/internal/entity/dnsprovider/entity_test.go +++ b/backend/internal/entity/dnsprovider/entity_test.go @@ -296,9 +296,7 @@ func (s *testsuite) TestDelete() { assert.Equal(s.T(), "Unable to delete a new object", err.Error()) m2 := Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, } err2 := m2.Delete() require.NoError(s.T(), err2) diff --git a/backend/internal/entity/dnsprovider/model.go b/backend/internal/entity/dnsprovider/model.go index e128922b9..b6c19c464 100644 --- a/backend/internal/entity/dnsprovider/model.go +++ b/backend/internal/entity/dnsprovider/model.go @@ -34,6 +34,11 @@ func (m *Model) LoadByID(id uint) error { return result.Error } +// SetUserID sets the owner of this model +func (m *Model) SetUserID(userID uint) { + m.UserID = userID +} + // Save will save this model to the DB func (m *Model) Save() error { if m.UserID == 0 { diff --git a/backend/internal/entity/host/entity_test.go b/backend/internal/entity/host/entity_test.go index 031ec860f..c49c04e99 100644 --- a/backend/internal/entity/host/entity_test.go +++ b/backend/internal/entity/host/entity_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "npm/internal/model" "npm/internal/status" "npm/internal/test" "npm/internal/types" @@ -189,9 +188,7 @@ func (s *testsuite) TestDelete() { assert.Equal(s.T(), "Unable to delete a new object", err.Error()) m2 := Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, } err2 := m2.Delete() require.NoError(s.T(), err2) @@ -203,11 +200,9 @@ func (s *testsuite) TestGetTemplate() { defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) m := Model{ - Base: model.Base{ - ID: 10, - CreatedAt: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), - UpdatedAt: time.Date(2018, 8, 12, 7, 30, 24, 16, time.UTC).UnixMilli(), - }, + ID: 10, + CreatedAt: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), + UpdatedAt: time.Date(2018, 8, 12, 7, 30, 24, 16, time.UTC).UnixMilli(), UserID: 100, Type: "proxy", NginxTemplateID: 20, diff --git a/backend/internal/entity/lists.go b/backend/internal/entity/lists.go index 186253b7f..7cc6a43a3 100644 --- a/backend/internal/entity/lists.go +++ b/backend/internal/entity/lists.go @@ -23,9 +23,7 @@ func ListQueryBuilder( filters []model.Filter, filterMap map[string]model.FilterMapValue, ) *gorm.DB { - scopes := make([]func(*gorm.DB) *gorm.DB, 0) - scopes = append(scopes, ScopeFilters(filters, filterMap)) - return database.GetDB().Scopes(scopes...) + return database.GetDB().Scopes(ScopeFilters(filters, filterMap)) } // AddOrderToList is used after query above is used for counting diff --git a/backend/internal/entity/nginxtemplate/model.go b/backend/internal/entity/nginxtemplate/model.go index 1d04a06ad..afe1d3953 100644 --- a/backend/internal/entity/nginxtemplate/model.go +++ b/backend/internal/entity/nginxtemplate/model.go @@ -28,6 +28,11 @@ func (m *Model) LoadByID(id uint) error { return result.Error } +// SetUserID sets the owner of this model +func (m *Model) SetUserID(userID uint) { + m.UserID = userID +} + // Save will save this model to the DB func (m *Model) Save() error { if m.UserID == 0 { diff --git a/backend/internal/entity/scopes.go b/backend/internal/entity/scopes.go index 4c1922efe..dbe200ec5 100644 --- a/backend/internal/entity/scopes.go +++ b/backend/internal/entity/scopes.go @@ -96,7 +96,7 @@ func ScopeFilters(filters []model.Filter, filterMap map[string]model.FilterMapVa } func sortToOrderString(sorts []model.Sort) string { - strs := make([]string, 0) + strs := make([]string, 0, len(sorts)) for _, i := range sorts { str := i.Field if i.Direction != "" { @@ -110,17 +110,7 @@ func sortToOrderString(sorts []model.Sort) string { func parseBoolValue(v string) []string { bVal := "0" switch strings.ToLower(v) { - case "yes": - fallthrough - case "true": - fallthrough - case "on": - fallthrough - case "t": - fallthrough - case "1": - fallthrough - case "y": + case "yes", "true", "on", "t", "1", "y": bVal = "1" } return []string{bVal} diff --git a/backend/internal/entity/stream/model.go b/backend/internal/entity/stream/model.go index 206b91ea4..4d58ef9fc 100644 --- a/backend/internal/entity/stream/model.go +++ b/backend/internal/entity/stream/model.go @@ -31,6 +31,11 @@ func (m *Model) LoadByID(id uint) error { return result.Error } +// SetUserID sets the owner of this model +func (m *Model) SetUserID(userID uint) { + m.UserID = userID +} + // Save will save this model to the DB func (m *Model) Save() error { if m.UserID == 0 { diff --git a/backend/internal/entity/user/entity_test.go b/backend/internal/entity/user/entity_test.go index 7045b4d08..a5c19f8ea 100644 --- a/backend/internal/entity/user/entity_test.go +++ b/backend/internal/entity/user/entity_test.go @@ -226,9 +226,7 @@ func (s *testsuite) TestDelete() { assert.Equal(s.T(), "Unable to delete a new object", err.Error()) m2 := Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, Name: "John Doe", } err2 := m2.Delete() @@ -442,9 +440,7 @@ func (s *testsuite) TestSaveCapabilitiesInvalid() { // Empty model returns error m := Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, Capabilities: []string{"doesnotexist", "hosts.manage"}, } err := m.SaveCapabilities() diff --git a/backend/internal/entity/user/methods.go b/backend/internal/entity/user/methods.go index 1002d1855..4c404746c 100644 --- a/backend/internal/entity/user/methods.go +++ b/backend/internal/entity/user/methods.go @@ -89,16 +89,16 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ent // DeleteAll will do just that, and should only be used for testing purposes. func DeleteAll() error { db := database.GetDB() - if result := db.Exec( + result := db.Exec( fmt.Sprintf(`DELETE FROM %s WHERE is_system = ?`, database.QuoteTableName("user")), false, - ); result.Error != nil { + ) + if result.Error != nil { return result.Error } - if result := db.Exec( - fmt.Sprintf(`DELETE FROM %s`, database.QuoteTableName("auth")), - ); result.Error != nil { + result = db.Exec(fmt.Sprintf(`DELETE FROM %s`, database.QuoteTableName("auth"))) + if result.Error != nil { return result.Error } diff --git a/backend/internal/jobqueue/main_test.go b/backend/internal/jobqueue/main_test.go index bebdc7d1c..f6b0dc10e 100644 --- a/backend/internal/jobqueue/main_test.go +++ b/backend/internal/jobqueue/main_test.go @@ -33,17 +33,14 @@ func TestShutdown(t *testing.T) { err := Shutdown() require.Nil(t, err, "Shutdown should not return an error when jobqueue is started") - // nolint: gosimple - select { - case <-ctx.Done(): - switch ctx.Err() { - case context.DeadlineExceeded: - fmt.Println("context timeout exceeded") - case context.Canceled: - fmt.Println("context cancelled by force. whole process is complete") - default: - require.Nil(t, ctx.Err(), "Context done state has unexpected value") - } + <-ctx.Done() + switch ctx.Err() { + case context.DeadlineExceeded: + fmt.Println("context timeout exceeded") + case context.Canceled: + fmt.Println("context cancelled by force. whole process is complete") + default: + require.Nil(t, ctx.Err(), "Context done state has unexpected value") } require.Nil(t, cancel, "Cancel function should be nil after Shutdown") diff --git a/backend/internal/jwt/suite_test.go b/backend/internal/jwt/suite_test.go index b17f5d677..7c1e99b58 100644 --- a/backend/internal/jwt/suite_test.go +++ b/backend/internal/jwt/suite_test.go @@ -5,7 +5,6 @@ import ( "testing" "npm/internal/entity/user" - "npm/internal/model" "npm/internal/test" "github.com/DATA-DOG/go-sqlmock" @@ -183,9 +182,7 @@ func (s *testsuite) TestGetPrivateKey() { // Set currentKeys and try again currentKeys = KeysModel{ - Base: model.Base{ - ID: 10, - }, + ID: 10, PrivateKey: s.privateKeyString, PublicKey: s.publicKeyString, } @@ -210,9 +207,7 @@ func (s *testsuite) TestGetPublicKey() { // Set currentKeys and try again currentKeys = KeysModel{ - Base: model.Base{ - ID: 10, - }, + ID: 10, PrivateKey: s.privateKeyString, PublicKey: s.publicKeyString, } @@ -228,17 +223,13 @@ func (s *testsuite) TestGenerate() { defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) currentKeys = KeysModel{ - Base: model.Base{ - ID: 10, - }, + ID: 10, PrivateKey: s.privateKeyString, PublicKey: s.publicKeyString, } usr := user.Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, } // test 1, user key diff --git a/backend/internal/logger/logger_test.go b/backend/internal/logger/logger_test.go index 9fb765292..0ee8e688e 100644 --- a/backend/internal/logger/logger_test.go +++ b/backend/internal/logger/logger_test.go @@ -168,7 +168,6 @@ func TestConfigure(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { if err := Configure(tt.args.c); (err != nil) != tt.wantErr { t.Errorf("Configure() error = %v, wantErr %v", err, tt.wantErr) diff --git a/backend/internal/migrations/data.go b/backend/internal/migrations/data.go deleted file mode 100644 index 78ef41304..000000000 --- a/backend/internal/migrations/data.go +++ /dev/null @@ -1,347 +0,0 @@ -package migrations - -import ( - "time" - - "npm/internal/entity" - "npm/internal/entity/certificateauthority" - "npm/internal/entity/nginxtemplate" - "npm/internal/entity/setting" - "npm/internal/entity/user" - "npm/internal/model" - - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/datatypes" - "gorm.io/gorm" -) - -const proxyTemplate = `# ------------------------------------------------------------ -{{#each Host.DomainNames}} -# {{this}} -{{/each}} -# ------------------------------------------------------------ - -server { - {{#if Config.Ipv4}} - listen 80; - {{/if}} - {{#if Config.Ipv6}} - listen [::]:80; - {{/if}} - - {{#if Certificate.ID}} - {{#if Config.Ipv4}} - listen 443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; - {{/if}} - {{#if Config.Ipv6}} - listen [::]:443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; - {{/if}} - {{/if}} - - server_name {{#each Host.DomainNames}}{{this}} {{/each}}; - - {{#if Certificate.ID}} - include conf.d/include/ssl-ciphers.conf; - {{#if Certificate.IsAcme}} - ssl_certificate {{Certificate.Folder}}/fullchain.pem; - ssl_certificate_key {{Certificate.Folder}}/privkey.pem; - {{else}} - # Custom SSL - ssl_certificate /data/custom_ssl/npm-{{Certicicate.ID}}/fullchain.pem; - ssl_certificate_key /data/custom_ssl/npm-{{Certificate.ID}}/privkey.pem; - {{/if}} - {{/if}} - - {{#if Host.CachingEnabled}} - include conf.d/include/assets.conf; - {{/if}} - - {{#if Host.BlockExploits}} - include conf.d/include/block-exploits.conf; - {{/if}} - - {{#if Certificate.ID}} - {{#if Host.SSLForced}} - {{#if Host.HSTSEnabled}} - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security "max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload" always; - {{/if}} - # Force SSL - include conf.d/include/force-ssl.conf; - {{/if}} - {{/if}} - - {{#if Host.AllowWebsocketUpgrade}} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - {{/if}} - - access_log /data/logs/host-{{Host.ID}}_access.log proxy; - error_log /data/logs/host-{{Host.ID}}_error.log warn; - - {{Host.AdvancedConfig}} - - # locations ? - - # default location: - location / { - {{#if Host.AccessListID}} - # Authorization - auth_basic "Authorization required"; - auth_basic_user_file /data/access/{{Host.AccessListID}}; - # access_list.passauth ? todo - {{/if}} - - # Access Rules ? todo - - # Access checks must...? todo - - {{#if Certificate.ID}} - {{#if Host.SSLForced}} - {{#if Host.HSTSEnabled}} - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security "max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload" always; - {{/if}} - {{/if}} - {{/if}} - - {{#if Host.AllowWebsocketUpgrade}} - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - {{/if}} - - # Proxy! - add_header X-Served-By $host; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_http_version 1.1; - - {{#if Upstream.ID}} - # upstream - proxy_pass {{Host.ProxyScheme}}://npm_upstream_{{Upstream.ID}}; - {{else}} - # proxy a single host - proxy_pass {{Host.ProxyScheme}}://{{Host.ProxyHost}}:{{Host.ProxyPort}}; - {{/if}} - } - - # Legacy Custom Configuration - include /data/nginx/custom/server_proxy[.]conf; -} -` - -const upstreamTemplate = `# ------------------------------------------------------------ -# Upstream {{Upstream.ID}}: {{Upstream.Name}} -# ------------------------------------------------------------ - -upstream npm_upstream_{{Upstream.ID}} { - - {{#if Upstream.IPHash~}} - ip_hash; - {{~/if}} - - {{#if Upstream.NTLM~}} - ntlm; - {{~/if}} - - {{#if Upstream.Keepalive~}} - keepalive {{Upstream.Keepalive}}; - {{~/if}} - - {{#if Upstream.KeepaliveRequests~}} - keepalive_requests {{Upstream.KeepaliveRequests}}; - {{~/if}} - - {{#if Upstream.KeepaliveTime~}} - keepalive_time {{Upstream.KeepaliveTime}}; - {{~/if}} - - {{#if Upstream.KeepaliveTimeout~}} - keepalive_timeout {{Upstream.KeepaliveTimeout}}; - {{~/if}} - - {{Upstream.AdvancedConfig}} - - {{#each Upstream.Servers~}} - {{#unless IsDeleted~}} - server {{Server}} {{#if Weight}}weight={{Weight}} {{/if}}{{#if MaxConns}}max_conns={{MaxConns}} {{/if}}{{#if MaxFails}}max_fails={{MaxFails}} {{/if}}{{#if FailTimeout}}fail_timeout={{FailTimeout}} {{/if}}{{#if Backup}}backup{{/if}}; - {{/unless}} - {{/each}} -} -` - -// initialData seeds the tables created by initialSchema with the same -// default rows that the old per-engine dbmate migrations inserted: -// capabilities, default settings, built-in certificate authorities, the -// system user, and the built-in nginx templates. -func initialData() *gormigrate.Migration { - return &gormigrate.Migration{ - ID: "20201013035839", - Migrate: func(tx *gorm.DB) error { - now := time.Now().UnixMilli() - base := func() model.Base { - return model.Base{CreatedAt: now, UpdatedAt: now} - } - - capabilities := []entity.Capability{ - {Name: user.CapabilityFullAdmin}, - {Name: user.CapabilityAccessListsView}, - {Name: user.CapabilityAccessListsManage}, - {Name: user.CapabilityAuditLogView}, - {Name: user.CapabilityCertificatesView}, - {Name: user.CapabilityCertificatesManage}, - {Name: user.CapabilityCertificateAuthoritiesView}, - {Name: user.CapabilityCertificateAuthoritiesManage}, - {Name: user.CapabilityDNSProvidersView}, - {Name: user.CapabilityDNSProvidersManage}, - {Name: user.CapabilityHostsView}, - {Name: user.CapabilityHostsManage}, - {Name: user.CapabilityNginxTemplatesView}, - {Name: user.CapabilityNginxTemplatesManage}, - {Name: user.CapabilitySettingsManage}, - {Name: user.CapabilityStreamsView}, - {Name: user.CapabilityStreamsManage}, - {Name: user.CapabilityUsersManage}, - } - if err := tx.Create(&capabilities).Error; err != nil { - return err - } - - settings := []setting.Model{ - { - Base: base(), - Name: "default-site", - Description: "What to show users who hit your Nginx server by default", - Value: datatypes.JSON(`"welcome"`), - }, - { - Base: base(), - Name: "auth-methods", - Description: "Which methods are enabled for authentication", - Value: datatypes.JSON(`["local"]`), - }, - { - Base: base(), - Name: "oauth-auth", - Description: "Configuration for OAuth authentication", - Value: datatypes.JSON(`{}`), - }, - { - Base: base(), - Name: "ldap-auth", - Description: "Configuration for LDAP authentication", - Value: datatypes.JSON(`{"host": "", "dn": "", "sync_by": "uid"}`), - }, - } - if err := tx.Create(&settings).Error; err != nil { - return err - } - - certificateAuthorities := []certificateauthority.Model{ - { - Base: base(), - Name: "ZeroSSL", - AcmeshServer: "zerossl", - IsWildcardSupported: true, - MaxDomains: 10, - IsReadonly: true, - }, - { - Base: base(), - Name: "Let's Encrypt", - AcmeshServer: "https://acme-v02.api.letsencrypt.org/directory", - IsWildcardSupported: true, - MaxDomains: 10, - IsReadonly: true, - }, - { - Base: base(), - Name: "Buypass Go SSL", - AcmeshServer: "https://api.buypass.com/acme/directory", - IsWildcardSupported: false, - MaxDomains: 5, - IsReadonly: true, - }, - { - Base: base(), - Name: "SSL.com", - AcmeshServer: "ssl.com", - IsWildcardSupported: false, - MaxDomains: 10, - IsReadonly: true, - }, - { - Base: base(), - Name: "Let's Encrypt (Testing)", - AcmeshServer: "https://acme-staging-v02.api.letsencrypt.org/directory", - IsWildcardSupported: true, - MaxDomains: 10, - IsReadonly: true, - }, - { - Base: base(), - Name: "Buypass Go SSL (Testing)", - AcmeshServer: "https://api.test4.buypass.no/acme/directory", - IsWildcardSupported: false, - MaxDomains: 5, - IsReadonly: true, - }, - } - if err := tx.Create(&certificateAuthorities).Error; err != nil { - return err - } - - systemUser := user.Model{ - Base: base(), - Name: "System", - Email: "system@localhost", - IsSystem: true, - } - if err := tx.Create(&systemUser).Error; err != nil { - return err - } - - templates := []nginxtemplate.Model{ - { - Base: base(), - UserID: systemUser.ID, - Name: "Default Proxy Template", - Type: "proxy", - Template: proxyTemplate, - }, - { - Base: base(), - UserID: systemUser.ID, - Name: "Default Redirect Template", - Type: "redirect", - Template: "# this is a redirect template", - }, - { - Base: base(), - UserID: systemUser.ID, - Name: "Default Dead Template", - Type: "dead", - Template: "# this is a dead template", - }, - { - Base: base(), - UserID: systemUser.ID, - Name: "Default Stream Template", - Type: "stream", - Template: "# this is a stream template", - }, - { - Base: base(), - UserID: systemUser.ID, - Name: "Default Upstream Template", - Type: "upstream", - Template: upstreamTemplate, - }, - } - return tx.Create(&templates).Error - }, - } -} diff --git a/backend/internal/migrations/log.go b/backend/internal/migrations/log.go new file mode 100644 index 000000000..f782fe02f --- /dev/null +++ b/backend/internal/migrations/log.go @@ -0,0 +1,61 @@ +package migrations + +import ( + "context" + "fmt" + "log/slog" + "slices" + "strings" + + "npm/internal/logger" +) + +// logHandler routes polyschema's slog output through the application +// logger, so migration progress and warnings (e.g. MySQL committing DDL +// implicitly) show up alongside everything else. +type logHandler struct { + attrs []slog.Attr +} + +func newSlogLogger() *slog.Logger { + return slog.New(&logHandler{}) +} + +// Enabled lets every record through; the application logger applies its +// own level filtering. +func (*logHandler) Enabled(context.Context, slog.Level) bool { + return true +} + +func (h *logHandler) Handle(_ context.Context, r slog.Record) error { + var b strings.Builder + b.WriteString(r.Message) + write := func(a slog.Attr) bool { + fmt.Fprintf(&b, " %s=%v", a.Key, a.Value) + return true + } + for _, a := range h.attrs { + write(a) + } + r.Attrs(write) + + msg := b.String() + switch { + case r.Level >= slog.LevelWarn: + logger.Warn("%s", msg) + case r.Level >= slog.LevelInfo: + logger.Info("%s", msg) + default: + logger.Debug("%s", msg) + } + return nil +} + +func (h *logHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &logHandler{attrs: slices.Concat(h.attrs, attrs)} +} + +// WithGroup is unused by polyschema, so groups are flattened. +func (h *logHandler) WithGroup(string) slog.Handler { + return h +} diff --git a/backend/internal/migrations/migrations.go b/backend/internal/migrations/migrations.go index 224b66d3d..353808497 100644 --- a/backend/internal/migrations/migrations.go +++ b/backend/internal/migrations/migrations.go @@ -1,58 +1,49 @@ -// Package migrations owns the application's database schema. It replaces -// the previous dbmate-based setup: instead of hand-written, per-engine SQL -// files, each migration is a small Go function driven by gorm, so a single -// migration works against Postgres, MySQL and SQLite alike. +// Package migrations owns the application's database schema and seed data. +// +// Both live as portable polyschema migration files in embed/migrations/ +// (see https://github.com/jc21/polyschema): one YAML file per change, +// rendered into the right SQL for Postgres, MySQL or SQLite at runtime. +// Applied files are recorded in the historyTable, so on every boot only +// pending files run. +// +// Applied files must never be edited: polyschema checksums them and +// refuses to start if one changes. Schema changes are made by adding a +// new file with a higher version number. package migrations import ( + "context" + "database/sql" + "fmt" + "io/fs" + + "npm/embed" + "npm/internal/config" "npm/internal/database" "npm/internal/logger" - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" + polyschema "github.com/jc21/polyschema/v1" ) -// legacyMigrationsTable is the table dbmate used to track applied -// migrations. Its presence indicates a database that was created before -// the switch to gormigrate. -const legacyMigrationsTable = "schema_migrations" - -// legacyMigrationIDs maps the dbmate migration versions this application -// used to ship to the gormigrate migrations that replace them, in order. -// Existing installs already have this schema and data applied via dbmate, -// so on first boot after upgrading we mark them as already run rather -// than re-running them. -var legacyMigrationIDs = []string{ - "20201013035318", // initial_schema - "20201013035839", // initial_data -} - -// all is the ordered list of every migration this application has ever -// shipped. Entries must never be reordered or edited once released; -// schema changes are made by appending a new migration. -func all() []*gormigrate.Migration { - return []*gormigrate.Migration{ - initialSchema(), - initialData(), - } -} +// historyTable tracks which migration files have been applied. +const historyTable = "migrations" type afterMigrationComplete func() -// Migrate will bring the db up to date +// Migrate applies any pending migrations, then calls followup on success. func Migrate(followup afterMigrationComplete) bool { db := database.GetDB() if db == nil { return false } - if err := bootstrapFromLegacyDBMate(db); err != nil { + sqlDB, err := db.DB() + if err != nil { logger.Error("MigrationError", err) return false } - m := gormigrate.New(db, gormigrate.DefaultOptions, all()) - if err := m.Migrate(); err != nil { + if err := migrate(sqlDB); err != nil { logger.Error("MigrationError", err) return false } @@ -63,45 +54,23 @@ func Migrate(followup afterMigrationComplete) bool { return true } -// bootstrapFromLegacyDBMate detects a database that was migrated by the -// old dbmate setup and pre-marks the gormigrate migrations that replace -// dbmate's initial_schema/initial_data as already applied, so gormigrate -// doesn't try to recreate tables or re-insert seed data that already -// exist. It's a no-op for a brand new database and for one that has -// already been bootstrapped. -func bootstrapFromLegacyDBMate(db *gorm.DB) error { - if !db.Migrator().HasTable(legacyMigrationsTable) { - // Fresh install, nothing to bootstrap. - return nil +// migrate applies every pending file in embed/migrations/ for the +// configured database engine. +func migrate(sqlDB *sql.DB) error { + engine := config.Configuration.DB.GetDriver() + dialect := polyschema.DialectByName(engine) + if dialect == nil { + return fmt.Errorf("migrations: unsupported database driver %q", engine) } - var legacyCount int64 - if err := db.Table(legacyMigrationsTable).Count(&legacyCount).Error; err != nil { - return err - } - if legacyCount == 0 { - return nil - } - - if db.Migrator().HasTable(gormigrate.DefaultOptions.TableName) { - // Already bootstrapped (or running fresh under gormigrate already). - return nil - } - - logger.Info("Existing dbmate-managed database detected, marking bootstrap migrations as applied") - - type migrationRecord struct { - ID string `gorm:"primaryKey;column:id;size:255"` - } - if err := db.Table(gormigrate.DefaultOptions.TableName).AutoMigrate(&migrationRecord{}); err != nil { + files, err := fs.Sub(embed.MigrationFiles, "migrations") + if err != nil { return err } - for _, id := range legacyMigrationIDs { - if err := db.Table(gormigrate.DefaultOptions.TableName).Create(&migrationRecord{ID: id}).Error; err != nil { - return err - } - } - - return nil + m := polyschema.New(sqlDB, dialect, + polyschema.WithTable(historyTable), + polyschema.WithLogger(newSlogLogger()), + ) + return m.Up(context.Background(), files) } diff --git a/backend/internal/migrations/migrations_test.go b/backend/internal/migrations/migrations_test.go index b1dfb9702..f3303dec5 100644 --- a/backend/internal/migrations/migrations_test.go +++ b/backend/internal/migrations/migrations_test.go @@ -1,9 +1,12 @@ package migrations import ( + "io/fs" "path/filepath" "testing" + "npm/embed" + "npm/internal/config" "npm/internal/database" "npm/internal/entity" "npm/internal/entity/certificateauthority" @@ -12,16 +15,18 @@ import ( "npm/internal/entity/user" "github.com/glebarez/sqlite" - "github.com/go-gormigrate/gormigrate/v2" + polyschema "github.com/jc21/polyschema/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" ) func openTestDB(t *testing.T) *gorm.DB { t.Helper() + config.Configuration.DB.Driver = config.DatabaseSqlite dsn := filepath.Join(t.TempDir(), "test.db") - db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: gormlogger.Default.LogMode(gormlogger.Silent)}) require.NoError(t, err) return db } @@ -55,9 +60,9 @@ func TestMigrateFreshDatabase(t *testing.T) { require.NoError(t, db.Model(&nginxtemplate.Model{}).Count(&templateCount).Error) assert.EqualValues(t, 5, templateCount) - var migrationCount int64 - require.NoError(t, db.Table(gormigrate.DefaultOptions.TableName).Count(&migrationCount).Error) - assert.EqualValues(t, len(all()), migrationCount) + var historyCount int64 + require.NoError(t, db.Table(historyTable).Count(&historyCount).Error) + assert.EqualValues(t, 2, historyCount, "one history row per migration file") } func TestMigrateIsIdempotent(t *testing.T) { @@ -76,36 +81,26 @@ func TestMigrateIsIdempotent(t *testing.T) { assert.EqualValues(t, 1, userCount) } -// TestBootstrapFromLegacyDBMate simulates a database that was previously -// migrated by dbmate: it has dbmate's schema_migrations table with the two -// legacy versions recorded, and already contains seeded data. Migrate must -// detect this and mark the equivalent gormigrate migrations as already -// applied instead of re-running them and duplicating the seed data. -func TestBootstrapFromLegacyDBMate(t *testing.T) { - db := openTestDB(t) - database.SetDB(db) - - require.NoError(t, db.Exec(`CREATE TABLE schema_migrations (version TEXT PRIMARY KEY)`).Error) - for _, v := range legacyMigrationIDs { - require.NoError(t, db.Exec(`INSERT INTO schema_migrations (version) VALUES (?)`, v).Error) - } - require.NoError(t, db.Exec(`CREATE TABLE capability (name TEXT PRIMARY KEY)`).Error) - require.NoError(t, db.Exec(`INSERT INTO capability (name) VALUES (?)`, "full-admin").Error) - - require.True(t, Migrate(func() {})) - - var capabilityCount int64 - require.NoError(t, db.Table("capability").Count(&capabilityCount).Error) - assert.EqualValues(t, 1, capabilityCount, "legacy data must not be re-seeded") - - var migrationIDs []string - require.NoError(t, db.Table(gormigrate.DefaultOptions.TableName).Pluck("id", &migrationIDs).Error) - assert.ElementsMatch(t, legacyMigrationIDs, migrationIDs) -} - func TestMigrateNoDB(t *testing.T) { database.SetDB(nil) assert.False(t, Migrate(func() { t.Error("followup should not be called") })) } + +// TestMigrationsRenderForEveryEngine catches a migration file that won't +// run on one of the supported engines, without needing those databases. +func TestMigrationsRenderForEveryEngine(t *testing.T) { + files, err := fs.Sub(embed.MigrationFiles, "migrations") + require.NoError(t, err) + + issues, err := polyschema.CheckFS(files, polyschema.Postgres, polyschema.MySQL, polyschema.SQLite) + require.NoError(t, err) + for _, is := range issues { + if is.Severity == polyschema.Error { + t.Error(is) + } else { + t.Log(is) + } + } +} diff --git a/backend/internal/migrations/schema.go b/backend/internal/migrations/schema.go deleted file mode 100644 index 2a89bd96c..000000000 --- a/backend/internal/migrations/schema.go +++ /dev/null @@ -1,68 +0,0 @@ -package migrations - -import ( - "npm/internal/entity" - "npm/internal/entity/accesslist" - "npm/internal/entity/auth" - "npm/internal/entity/certificate" - "npm/internal/entity/certificateauthority" - "npm/internal/entity/dnsprovider" - "npm/internal/entity/host" - "npm/internal/entity/nginxtemplate" - "npm/internal/entity/setting" - "npm/internal/entity/stream" - "npm/internal/entity/upstream" - "npm/internal/entity/upstreamserver" - "npm/internal/entity/user" - "npm/internal/jwt" - "npm/internal/model" - - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" -) - -// auditLogSchema mirrors the audit_log table. There is no application -// entity for it yet, so its schema is only declared here for AutoMigrate. -type auditLogSchema struct { - model.Base - UserID uint `gorm:"column:user_id"` - ObjectType string `gorm:"column:object_type"` - ObjectID uint `gorm:"column:object_id"` - Action string `gorm:"column:action"` - Meta string `gorm:"column:meta"` -} - -// TableName overrides the table name used by gorm -func (auditLogSchema) TableName() string { - return "audit_log" -} - -// initialSchema creates every table used by the application. It replaces -// the hand-written, per-engine SQL that dbmate used to run: gorm's dialect -// drivers already know how to translate these structs for Postgres, MySQL -// and SQLite, so there's no need to maintain three copies of the same DDL. -func initialSchema() *gormigrate.Migration { - return &gormigrate.Migration{ - ID: "20201013035318", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - &jwt.KeysModel{}, - &user.Model{}, - &entity.Capability{}, - &user.HasCapabilityModel{}, - &auth.Model{}, - &setting.Model{}, - &auditLogSchema{}, - &certificateauthority.Model{}, - &dnsprovider.Model{}, - &certificate.Model{}, - &stream.Model{}, - &nginxtemplate.Model{}, - &upstream.Model{}, - &upstreamserver.Model{}, - &accesslist.Model{}, - &host.Model{}, - ) - }, - } -} diff --git a/backend/internal/nginx/control_test.go b/backend/internal/nginx/control_test.go index dfbd19c67..58e96f209 100644 --- a/backend/internal/nginx/control_test.go +++ b/backend/internal/nginx/control_test.go @@ -4,7 +4,6 @@ import ( "testing" "npm/internal/entity/host" - "npm/internal/model" "npm/internal/test" "github.com/stretchr/testify/assert" @@ -25,9 +24,7 @@ func TestGetHostFilename(t *testing.T) { { "test1", host.Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, }, "", "/data/nginx/hosts/host_10.conf", @@ -35,9 +32,7 @@ func TestGetHostFilename(t *testing.T) { { "test2", host.Model{ - Base: model.Base{ - ID: 10, - }, + ID: 10, }, ".deleted", "/data/nginx/hosts/host_10.conf.deleted", diff --git a/backend/internal/tags/filters.go b/backend/internal/tags/filters.go index bce6319e3..27c8c87d6 100644 --- a/backend/internal/tags/filters.go +++ b/backend/internal/tags/filters.go @@ -50,10 +50,8 @@ func GetFilterMap(m any, globalTablePrefix string) map[string]model.FilterMapVal } // 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) - + // Each field is a https://golang.org/pkg/reflect/#StructField + for field := range t.Fields() { // Get the field tag value filterTag := field.Tag.Get("filter") dbTag := field.Tag.Get("gorm") @@ -61,10 +59,9 @@ func GetFilterMap(m any, globalTablePrefix string) map[string]model.FilterMapVal // Filter -> Schema mapping if filterTag != "" && filterTag != "-" { f := model.FilterMapValue{ - Model: name, + Model: name, + Schema: getFilterTagSchema(filterTag), } - - f.Schema = getFilterTagSchema(filterTag) parts := strings.Split(filterTag, ",") // Filter -> DB Field mapping @@ -104,15 +101,9 @@ func getFilterTagSchema(filterTag string) string { } switch items[1] { - case "number": - fallthrough - case "int": - fallthrough - case "integer": + case "number", "int", "integer": return intFieldSchema(items[0]) - case "bool": - fallthrough - case "boolean": + case "bool", "boolean": return boolFieldSchema(items[0]) case "date": return dateFieldSchema(items[0]) @@ -131,7 +122,7 @@ func getFilterTagSchema(filterTag string) string { // object given and by reading the struct "filter" tags. func GetFilterSchema(m any) string { filterMap := GetFilterMap(m, "") - schemas := make([]string, 0) + schemas := make([]string, 0, len(filterMap)) for _, f := range filterMap { schemas = append(schemas, f.Schema) diff --git a/backend/internal/types/db_date_test.go b/backend/internal/types/db_date_test.go index 1aeafec11..7a3610f22 100644 --- a/backend/internal/types/db_date_test.go +++ b/backend/internal/types/db_date_test.go @@ -25,7 +25,10 @@ func TestDBDate_Value(t *testing.T) { } // Convert the value to int64 - unixTime := value.(int64) + unixTime, ok := value.(int64) + if !ok { + t.Fatalf("Expected int64 value, got %T", value) + } // Convert the unix time back to time.Time actualTime := time.Unix(unixTime, 0) @@ -55,11 +58,11 @@ func TestDBDate_Scan(t *testing.T) { } // Convert the DBDate's time to unix timestamp for comparison - actualUnixTime := dbDate.Time.Unix() + actualUnixSec := dbDate.Time.Unix() // Compare the actual unix time with the expected unix time - if actualUnixTime != unixTime { - t.Errorf("Expected unix time '%v', got '%v'", unixTime, actualUnixTime) + if actualUnixSec != unixTime { + t.Errorf("Expected unix time '%v', got '%v'", unixTime, actualUnixSec) } } @@ -82,12 +85,12 @@ func TestDBDate_UnmarshalJSON(t *testing.T) { } // Convert the DBDate's time to unix timestamp for comparison - actualUnixTime := dbDate.Time.Unix() + actualUnixSec := dbDate.Time.Unix() // Compare the actual unix time with the expected unix time expectedUnixTime := int64(1640995200) - if actualUnixTime != expectedUnixTime { - t.Errorf("Expected unix time '%v', got '%v'", expectedUnixTime, actualUnixTime) + if actualUnixSec != expectedUnixTime { + t.Errorf("Expected unix time '%v', got '%v'", expectedUnixTime, actualUnixSec) } } diff --git a/backend/internal/types/jsonb.go b/backend/internal/types/jsonb.go index 1cd8b3b7b..7b64d631b 100644 --- a/backend/internal/types/jsonb.go +++ b/backend/internal/types/jsonb.go @@ -25,9 +25,9 @@ func (j *JSONB) Scan(src any) error { var srcString string switch v := src.(type) { case string: - srcString = src.(string) + srcString = v case []uint8: - srcString = string(src.([]uint8)) + srcString = string(v) default: return eris.Errorf("Incompatible type for JSONB: %v", v) } diff --git a/backend/internal/types/nullable_db_date_test.go b/backend/internal/types/nullable_db_date_test.go index 613f3fa07..af3b804fd 100644 --- a/backend/internal/types/nullable_db_date_test.go +++ b/backend/internal/types/nullable_db_date_test.go @@ -22,10 +22,10 @@ func TestNullableDBDateValue(t *testing.T) { t.Errorf("Unexpected error: %v", err) } - expectedValue := tme.Unix() + expectedSec := tme.Unix() - if value != expectedValue { - t.Errorf("Incorrect value. Expected: %d, Got: %v", expectedValue, value) + if value != expectedSec { + t.Errorf("Incorrect value. Expected: %d, Got: %v", expectedSec, value) } } @@ -101,10 +101,10 @@ func TestNullableDBDateAsInt64(t *testing.T) { } unixtime := d.AsInt64() - expectedUnixtime := tme.Unix() + expectedSec := tme.Unix() - if unixtime != expectedUnixtime { - t.Errorf("Incorrect unixtime. Expected: %d, Got: %d", expectedUnixtime, unixtime) + if unixtime != expectedSec { + t.Errorf("Incorrect unixtime. Expected: %d, Got: %d", expectedSec, unixtime) } } diff --git a/backend/internal/util/slices.go b/backend/internal/util/slices.go index b02cd3463..962678859 100644 --- a/backend/internal/util/slices.go +++ b/backend/internal/util/slices.go @@ -1,33 +1,24 @@ package util import ( + "slices" "strconv" "strings" ) // SliceContainsItem returns whether the slice given contains the item given func SliceContainsItem(slice []string, item string) bool { - for _, a := range slice { - if a == item { - return true - } - } - return false + return slices.Contains(slice, item) } // SliceContainsInt returns whether the slice given contains the item given func SliceContainsInt(slice []int, item int) bool { - for _, a := range slice { - if a == item { - return true - } - } - return false + return slices.Contains(slice, item) } // ConvertIntSliceToString returns a comma separated string of all items in the slice func ConvertIntSliceToString(slice []int) string { - strs := []string{} + strs := make([]string, 0, len(slice)) for _, item := range slice { strs = append(strs, strconv.Itoa(item)) } diff --git a/backend/internal/validator/hosts_test.go b/backend/internal/validator/hosts_test.go index 946762925..875c74a65 100644 --- a/backend/internal/validator/hosts_test.go +++ b/backend/internal/validator/hosts_test.go @@ -21,7 +21,8 @@ type MockCertificate struct { func (m *MockCertificate) GetByID(id uint) (certificate.Model, error) { args := m.Called(id) - return args.Get(0).(certificate.Model), args.Error(1) + model, _ := args.Get(0).(certificate.Model) + return model, args.Error(1) } type MockUpstream struct { @@ -30,7 +31,8 @@ type MockUpstream struct { func (m *MockUpstream) GetByID(id uint) (upstream.Model, error) { args := m.Called(id) - return args.Get(0).(upstream.Model), args.Error(1) + model, _ := args.Get(0).(upstream.Model) + return model, args.Error(1) } type MockNginxTemplate struct { @@ -39,7 +41,8 @@ type MockNginxTemplate struct { func (m *MockNginxTemplate) GetByID(id uint) (nginxtemplate.Model, error) { args := m.Called(id) - return args.Get(0).(nginxtemplate.Model), args.Error(1) + model, _ := args.Get(0).(nginxtemplate.Model) + return model, args.Error(1) } func TestValidateHost(t *testing.T) { diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh index 44961b054..afc819705 100755 --- a/backend/scripts/lint.sh +++ b/backend/scripts/lint.sh @@ -1,22 +1,32 @@ #!/bin/bash +set -eufo pipefail -BLUE='\E[1;34m' -YELLOW='\E[1;33m' -RESET='\E[0m' -RESULT=0 +export RED='\E[1;31m' +export YELLOW='\E[1;33m' +export RESET='\033[0m' -# go files: incomplete comment check -INCOMPLETE_COMMENTS=$(find . -iname "*.go*" | grep -v " " | xargs grep --colour -H -n -E "^\s*\/\/\s*[A-Z]\w+ \.{3}" 2>/dev/null) -if [[ -n "$INCOMPLETE_COMMENTS" ]]; then - echo -e "${BLUE}❯ ${YELLOW}WARN: Please fix incomplete exported comments:${RESET}" - echo -e "${RED}${INCOMPLETE_COMMENTS}${RESET}" - echo - # RESULT=1 +PROJECT_DIR="$(cd -- "$(dirname -- "$0")/.." && pwd)" +cd "$PROJECT_DIR" + +trap cleanup EXIT +cleanup() { + if [ "$?" -ne 0 ]; then + echo -e "${RED}LINTING FAILED${RESET}" + fi +} + +if ! command -v golangci-lint &>/dev/null; then + echo -e "${YELLOW}Installing golangci-lint ...${RESET}" + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest fi echo -e "${YELLOW}golangci-lint ...${RESET}" -if ! golangci-lint run -E goimports ./...; then - exit 1 +golangci-lint --max-same-issues=0 --max-issues-per-linter=0 run ./... + +if ! command -v modernize &>/dev/null; then + echo -e "${YELLOW}Installing modernize ...${RESET}" + go install golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest fi -exit "$RESULT" +echo -e "${YELLOW}modernize ...${RESET}" +modernize -test "$@" ./... diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh deleted file mode 100755 index 45ee9483e..000000000 --- a/backend/scripts/test.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -eu - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -if ! command -v go-test-coverage &>/dev/null; then - go install github.com/vladopajic/go-test-coverage/v2@latest -fi -if ! command -v tparse &>/dev/null; then - go install github.com/mfridman/tparse@latest -fi - -rm -f "$DIR/coverage.html" - -trap cleanup EXIT -cleanup() { - rm -f "$DIR/coverage.out" -} - -echo "Running go test suite ..." -go test -json -cover ./... -coverprofile="$DIR/coverage.out" | tparse -go tool cover -html="$DIR/coverage.out" -o "$DIR/coverage.html" -go-test-coverage -c "$DIR/.testcoverage.yml" diff --git a/backend/scripts/unit.sh b/backend/scripts/unit.sh new file mode 100755 index 000000000..b3b9cfd8e --- /dev/null +++ b/backend/scripts/unit.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -eu + +export RED='\E[1;31m' +export YELLOW='\E[1;33m' +export RESET='\033[0m' + +cd -- "$(dirname -- "$0")/.." || exit 1 + +if ! command -v go-test-coverage &>/dev/null; then + echo -e "${YELLOW}Installing go-test-coverage ...${RESET}" + go install github.com/vladopajic/go-test-coverage/v2@latest +fi + +if ! command -v tparse &>/dev/null; then + echo -e "${YELLOW}Installing tparse ...${RESET}" + go install github.com/mfridman/tparse@latest +fi + +if ! command -v go-junit-report &>/dev/null; then + echo -e "${YELLOW}Installing go-junit-report ...${RESET}" + go install github.com/jstemmer/go-junit-report/v2@latest +fi + +trap cleanup EXIT +cleanup() { + if [ "$?" -ne 0 ]; then + echo -e "${RED}UNIT TESTING FAILED - check output and consider minimum coverage requirements${RESET}" + fi + rm -f coverage.out +} + +mkdir -p ../test/results/html-reports ../test/results/junit +go test -json -cover -coverprofile="./coverage.out" ./... | tparse || true +go tool cover -html="./coverage.out" -o "../test/results/html-reports/backend-coverage.html" +go test -v -covermode=atomic ./... 2>&1 | go-junit-report >../test/results/junit/backend.xml + +# this enforces minimum coverage requirements +go-test-coverage -c .testcoverage.yml