mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-06 21:00:10 +00:00
Compare commits
40 Commits
5dd81c071f
...
v3
Author | SHA1 | Date | |
---|---|---|---|
|
72e4a78fbf | ||
|
f3182c1258 | ||
|
9d3c06dbe4 | ||
|
152b7666d8 | ||
|
4e6d65645f | ||
|
8434b9fce4 | ||
|
f4bd65dd2c | ||
|
a856c4d6e1 | ||
|
2f334b5f9f | ||
|
2145df0dfb | ||
|
331c761a1c | ||
|
03b3b6379b | ||
|
050c087a51 | ||
|
61c41b8ec3 | ||
|
47d7896301 | ||
|
d048d1fc98 | ||
|
3774a40498 | ||
|
208037946f | ||
|
f23299f793 | ||
|
aa15052eea | ||
|
c556b8acef | ||
|
4d60876394 | ||
|
6e820a36ac | ||
|
a277a5d167 | ||
|
8434a2d1fa | ||
|
bee6a83f33 | ||
|
b37ef94c22 | ||
|
3f6ac9a021 | ||
|
1d5f390f9b | ||
|
d2048e540d | ||
|
d121de808c | ||
|
4d3d37eaed | ||
|
0cd2e07fd3 | ||
|
0a18a565c7 | ||
|
c4db4a2647 | ||
|
e78dd069f1 | ||
|
9a2e5c92d5 | ||
|
833dd23dce | ||
|
514520ce1a | ||
|
21e3bce95d |
18
Jenkinsfile
vendored
18
Jenkinsfile
vendored
@@ -188,6 +188,11 @@ pipeline {
|
|||||||
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
|
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
|
||||||
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
|
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
|
||||||
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
|
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
|
||||||
|
sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db.log 2>&1'
|
||||||
|
sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
|
||||||
|
sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
|
||||||
|
sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
|
||||||
|
|
||||||
junit 'test/results/junit/*'
|
junit 'test/results/junit/*'
|
||||||
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
|
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
|
||||||
}
|
}
|
||||||
@@ -245,26 +250,19 @@ pipeline {
|
|||||||
sh './scripts/ci/build-cleanup'
|
sh './scripts/ci/build-cleanup'
|
||||||
echo 'Reverting ownership'
|
echo 'Reverting ownership'
|
||||||
sh 'docker run --rm -v $(pwd):/data jc21/gotools:latest chown -R "$(id -u):$(id -g)" /data'
|
sh 'docker run --rm -v $(pwd):/data jc21/gotools:latest chown -R "$(id -u):$(id -g)" /data'
|
||||||
}
|
printResult()
|
||||||
success {
|
|
||||||
juxtapose event: 'success'
|
|
||||||
sh 'figlet "SUCCESS"'
|
|
||||||
}
|
}
|
||||||
failure {
|
failure {
|
||||||
|
archiveArtifacts(artifacts: 'debug/**/*', allowEmptyArchive: true)
|
||||||
dir(path: 'test') {
|
dir(path: 'test') {
|
||||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml'
|
archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml'
|
||||||
}
|
}
|
||||||
archiveArtifacts(artifacts: 'debug/*', allowEmptyArchive: true)
|
|
||||||
juxtapose event: 'failure'
|
|
||||||
sh 'figlet "FAILURE"'
|
|
||||||
}
|
}
|
||||||
unstable {
|
unstable {
|
||||||
|
archiveArtifacts(artifacts: 'debug/**/*', allowEmptyArchive: true)
|
||||||
dir(path: 'test') {
|
dir(path: 'test') {
|
||||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml'
|
archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml'
|
||||||
}
|
}
|
||||||
archiveArtifacts(artifacts: 'debug/*', allowEmptyArchive: true)
|
|
||||||
juxtapose event: 'unstable'
|
|
||||||
sh 'figlet "UNSTABLE"'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,56 +1,166 @@
|
|||||||
|
---
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
# Prevents against memory leaks in production caused by not closing
|
||||||
- errcheck
|
# file handle
|
||||||
- gosimple
|
- bodyclose
|
||||||
- govet
|
# Detects cloned code. DRY is good programming practice. Can cause issues
|
||||||
- gosec
|
# with testing code where simplicity is preferred over duplication.
|
||||||
- goconst
|
# Disabled for test code.
|
||||||
- gocritic
|
# - dupl
|
||||||
- gocyclo
|
# Detects unchecked errors in go programs. These unchecked errors can be
|
||||||
- gofmt
|
# critical bugs in some cases.
|
||||||
- goimports
|
- errcheck
|
||||||
- ineffassign
|
# Simplifies go code.
|
||||||
- misspell
|
- gosimple
|
||||||
- nakedret
|
# Controls Go package import order and makes it always deterministic.
|
||||||
- prealloc
|
- gci
|
||||||
#- revive
|
# Reports suspicious constructs, maintained by goteam. e.g. Printf unused
|
||||||
- staticcheck
|
# params not caught at compile time.
|
||||||
- typecheck
|
- govet
|
||||||
- unused
|
# Detect security issues with gocode. Use of secrets in code or obsolete
|
||||||
- unconvert
|
# security algorithms. It's imaged heuristic methods are used in finding
|
||||||
- unparam
|
# 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
|
||||||
|
- 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 commonly misspelled english words in comments
|
||||||
|
- misspell
|
||||||
|
# Detect naked returns on non-trivial functions, and conform with
|
||||||
|
# Go CodeReviewComments
|
||||||
|
- 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
|
||||||
|
# Remove unnecessary type conversions
|
||||||
|
- unconvert
|
||||||
|
# Remove unnecessary(unused) function parameters
|
||||||
|
- unparam
|
||||||
linters-settings:
|
linters-settings:
|
||||||
goconst:
|
errcheck:
|
||||||
# minimal length of string constant
|
exclude-functions:
|
||||||
# default: 3
|
- fmt.Fprint
|
||||||
min-len: 2
|
- fmt.Fprintf
|
||||||
# minimum number of occurrences of string constant
|
gci:
|
||||||
# default: 3
|
sections:
|
||||||
min-occurences: 2
|
- standard # Standard section: captures all standard packages.
|
||||||
misspell:
|
- localmodule # Local module section: contains all local packages.
|
||||||
locale: UK
|
# - prefix(gogs.jc21.com) # Prefixed gerrit.lan packages (jumgo).
|
||||||
ignore-words:
|
- default # Everything else (github.com, golang.org, etc).
|
||||||
- color
|
- 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
|
||||||
|
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:
|
issues:
|
||||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
# Maximum count of issues with the same text. Set to 0 to disable. Default
|
||||||
# We have chosen an arbitrary value that works based on practical usage.
|
# is 3. We have chosen an arbitrary value that works based on practical usage.
|
||||||
max-same: 20
|
max-same: 20
|
||||||
# See cmdline flag documentation for more info about default excludes --exclude-use-default
|
# See cmdline flag documentation for more info about default excludes
|
||||||
# Nothing is excluded by default
|
# --exclude-use-default. Nothing is excluded by default
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
# Exclude some linters from running on tests files. # TODO: Add examples why this is good
|
# Exclude some linters from running on tests files.
|
||||||
|
# TODO: Add examples why this is good
|
||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
# Tests should be simple? Add example why this is good?
|
# Tests should be simple? Add example why this is good?
|
||||||
- gocyclo
|
- gocyclo
|
||||||
# Error checking adds verbosity and complexity for minimal value
|
# Error checking adds verbosity and complexity for minimal value
|
||||||
- errcheck
|
- errcheck
|
||||||
# Table test encourage duplication in defining the table tests.
|
# Table test encourage duplication in defining the table tests.
|
||||||
- dupl
|
- dupl
|
||||||
# Hard coded example tokens, SQL injection and other bad practices may
|
# Hard coded example tokens, SQL injection and other bad practices may
|
||||||
# want to be tested
|
# want to be tested
|
||||||
- gosec
|
- gosec
|
||||||
|
# Test data can long
|
||||||
|
# - lll
|
||||||
|
run:
|
||||||
|
go: '1.23'
|
||||||
|
@@ -18,4 +18,4 @@ threshold:
|
|||||||
# package: 30
|
# package: 30
|
||||||
# (optional; default 0)
|
# (optional; default 0)
|
||||||
# The minimum total coverage project should have
|
# The minimum total coverage project should have
|
||||||
total: 34
|
total: 30
|
||||||
|
@@ -15,6 +15,9 @@ import (
|
|||||||
"npm/internal/jobqueue"
|
"npm/internal/jobqueue"
|
||||||
"npm/internal/jwt"
|
"npm/internal/jwt"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
// properly respect available cpu cores
|
||||||
|
_ "go.uber.org/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commit string
|
var commit string
|
||||||
|
@@ -10,6 +10,24 @@
|
|||||||
"$ref": "file://./paths/get.json"
|
"$ref": "file://./paths/get.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "file://./paths/auth/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "file://./paths/auth/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "file://./paths/auth/refresh/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/sse": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "file://./paths/auth/sse/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/certificates": {
|
"/certificates": {
|
||||||
"get": {
|
"get": {
|
||||||
"$ref": "file://./paths/certificates/get.json"
|
"$ref": "file://./paths/certificates/get.json"
|
||||||
@@ -155,19 +173,6 @@
|
|||||||
"$ref": "file://./paths/streams/streamID/delete.json"
|
"$ref": "file://./paths/streams/streamID/delete.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/tokens": {
|
|
||||||
"get": {
|
|
||||||
"$ref": "file://./paths/tokens/get.json"
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"$ref": "file://./paths/tokens/post.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/tokens/sse": {
|
|
||||||
"post": {
|
|
||||||
"$ref": "file://./paths/tokens/sse/post.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/upstreams": {
|
"/upstreams": {
|
||||||
"get": {
|
"get": {
|
||||||
"$ref": "file://./paths/upstreams/get.json"
|
"$ref": "file://./paths/upstreams/get.json"
|
||||||
@@ -219,6 +224,9 @@
|
|||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"AuthConfigObject": {
|
||||||
|
"$ref": "file://./components/AuthConfigObject.json"
|
||||||
|
},
|
||||||
"CertificateAuthorityList": {
|
"CertificateAuthorityList": {
|
||||||
"$ref": "file://./components/CertificateAuthorityList.json"
|
"$ref": "file://./components/CertificateAuthorityList.json"
|
||||||
},
|
},
|
||||||
|
13
backend/embed/api_docs/components/AuthConfigObject.json
Normal file
13
backend/embed/api_docs/components/AuthConfigObject.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"description": "AuthConfigObject",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"local",
|
||||||
|
"ldap",
|
||||||
|
"oauth"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"pattern": "^(local|ldap|oauth)$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,6 @@
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"name",
|
"name",
|
||||||
"nickname",
|
|
||||||
"email",
|
"email",
|
||||||
"is_disabled"
|
"is_disabled"
|
||||||
],
|
],
|
||||||
@@ -29,12 +28,7 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 2,
|
"minLength": 2,
|
||||||
"maxLength": 100
|
"maxLength": 50
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 2,
|
|
||||||
"maxLength": 100
|
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -59,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"pattern": "^(local|ldap|oauth)$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
28
backend/embed/api_docs/paths/auth/get.json
Normal file
28
backend/embed/api_docs/paths/auth/get.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"operationId": "getAuthConfig",
|
||||||
|
"summary": "Returns auth configuration",
|
||||||
|
"tags": ["Auth"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "200 response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["result"],
|
||||||
|
"properties": {
|
||||||
|
"result": {
|
||||||
|
"$ref": "#/components/schemas/AuthConfigObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": "todo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"operationId": "requestToken",
|
"operationId": "requestToken",
|
||||||
"summary": "Request a new access token from credentials",
|
"summary": "Request a new access token from credentials",
|
||||||
"tags": ["Tokens"],
|
"tags": ["Auth"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"description": "Credentials Payload",
|
"description": "Credentials Payload",
|
||||||
"required": true,
|
"required": true,
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"operationId": "refreshToken",
|
"operationId": "refreshToken",
|
||||||
"summary": "Refresh your access token",
|
"summary": "Refresh your access token",
|
||||||
"tags": ["Tokens"],
|
"tags": ["Auth"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "200 response",
|
"description": "200 response",
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"operationId": "requestSSEToken",
|
"operationId": "requestSSEToken",
|
||||||
"summary": "Request a new SSE token",
|
"summary": "Request a new SSE token",
|
||||||
"tags": ["Tokens"],
|
"tags": ["Auth"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "200 response",
|
"description": "200 response",
|
@@ -28,7 +28,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "The sorting of the list",
|
"description": "The sorting of the list",
|
||||||
"example": "name,nickname.desc,email.asc"
|
"example": "name,email.asc"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -57,10 +57,6 @@
|
|||||||
"field": "name",
|
"field": "name",
|
||||||
"direction": "ASC"
|
"direction": "ASC"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"field": "nickname",
|
|
||||||
"direction": "DESC"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"field": "email",
|
"field": "email",
|
||||||
"direction": "ASC"
|
"direction": "ASC"
|
||||||
@@ -70,7 +66,6 @@
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010090000,
|
"created_at": 1578010090000,
|
||||||
"updated_at": 1578010095000,
|
"updated_at": 1578010095000,
|
||||||
@@ -81,7 +76,6 @@
|
|||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"nickname": "John",
|
|
||||||
"email": "johdoe@example.com",
|
"email": "johdoe@example.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010105000,
|
"updated_at": 1578010105000,
|
||||||
@@ -95,7 +89,6 @@
|
|||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "Jane Doe",
|
"name": "Jane Doe",
|
||||||
"nickname": "Jane",
|
|
||||||
"email": "janedoe@example.com",
|
"email": "janedoe@example.com",
|
||||||
"created_at": 1578010110000,
|
"created_at": 1578010110000,
|
||||||
"updated_at": 1578010115000,
|
"updated_at": 1578010115000,
|
||||||
|
@@ -31,7 +31,6 @@
|
|||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010100000,
|
"updated_at": 1578010100000,
|
||||||
|
@@ -43,7 +43,6 @@
|
|||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010105000,
|
"updated_at": 1578010105000,
|
||||||
|
@@ -52,7 +52,6 @@
|
|||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010110000,
|
"updated_at": 1578010110000,
|
||||||
|
@@ -17,7 +17,6 @@ CREATE TABLE IF NOT EXISTS `user`
|
|||||||
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
||||||
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
`name` VARCHAR(50) NOT NULL,
|
`name` VARCHAR(50) NOT NULL,
|
||||||
`nickname` VARCHAR(50) NOT NULL,
|
|
||||||
`email` VARCHAR(255) NOT NULL,
|
`email` VARCHAR(255) NOT NULL,
|
||||||
`is_system` BOOLEAN NOT NULL DEFAULT FALSE,
|
`is_system` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
`is_disabled` BOOLEAN NOT NULL DEFAULT FALSE
|
`is_disabled` BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
@@ -45,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `auth`
|
|||||||
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
`user_id` INT NOT NULL,
|
`user_id` INT NOT NULL,
|
||||||
`type` VARCHAR(50) NOT NULL,
|
`type` VARCHAR(50) NOT NULL,
|
||||||
|
`identity` VARCHAR(255) NOT NULL,
|
||||||
`secret` VARCHAR(255) NOT NULL,
|
`secret` VARCHAR(255) NOT NULL,
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
|
||||||
UNIQUE (`user_id`, `type`)
|
UNIQUE (`user_id`, `type`)
|
||||||
|
@@ -37,6 +37,27 @@ INSERT INTO `setting` (
|
|||||||
"default-site",
|
"default-site",
|
||||||
"What to show users who hit your Nginx server by default",
|
"What to show users who hit your Nginx server by default",
|
||||||
'"welcome"' -- remember this is json
|
'"welcome"' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
"auth-methods",
|
||||||
|
"Which methods are enabled for authentication",
|
||||||
|
'["local"]' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
"oauth-auth",
|
||||||
|
"Configuration for OAuth authentication",
|
||||||
|
'{}' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
"ldap-auth",
|
||||||
|
"Configuration for LDAP authentication",
|
||||||
|
'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Default Certificate Authorities
|
-- Default Certificate Authorities
|
||||||
@@ -104,14 +125,12 @@ INSERT INTO `user` (
|
|||||||
`created_at`,
|
`created_at`,
|
||||||
`updated_at`,
|
`updated_at`,
|
||||||
`name`,
|
`name`,
|
||||||
`nickname`,
|
|
||||||
`email`,
|
`email`,
|
||||||
`is_system`
|
`is_system`
|
||||||
) VALUES (
|
) VALUES (
|
||||||
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
"System",
|
"System",
|
||||||
"System",
|
|
||||||
"system@localhost",
|
"system@localhost",
|
||||||
TRUE
|
TRUE
|
||||||
);
|
);
|
||||||
|
@@ -15,7 +15,6 @@ CREATE TABLE "user" (
|
|||||||
"updated_at" BIGINT NOT NULL DEFAULT 0,
|
"updated_at" BIGINT NOT NULL DEFAULT 0,
|
||||||
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
"name" VARCHAR(50) NOT NULL,
|
"name" VARCHAR(50) NOT NULL,
|
||||||
"nickname" VARCHAR(50) NOT NULL,
|
|
||||||
"email" VARCHAR(255) NOT NULL,
|
"email" VARCHAR(255) NOT NULL,
|
||||||
"is_system" BOOLEAN NOT NULL DEFAULT FALSE,
|
"is_system" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
"is_disabled" BOOLEAN NOT NULL DEFAULT FALSE
|
"is_disabled" BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
@@ -39,6 +38,7 @@ CREATE TABLE "auth" (
|
|||||||
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
"user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
|
"user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
|
||||||
"type" VARCHAR(50) NOT NULL,
|
"type" VARCHAR(50) NOT NULL,
|
||||||
|
"identity" VARCHAR(255) NOT NULL,
|
||||||
"secret" VARCHAR(255) NOT NULL,
|
"secret" VARCHAR(255) NOT NULL,
|
||||||
UNIQUE ("user_id", "type")
|
UNIQUE ("user_id", "type")
|
||||||
);
|
);
|
||||||
|
@@ -37,6 +37,27 @@ INSERT INTO "setting" (
|
|||||||
'default-site',
|
'default-site',
|
||||||
'What to show users who hit your Nginx server by default',
|
'What to show users who hit your Nginx server by default',
|
||||||
'"welcome"' -- remember this is json
|
'"welcome"' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
'auth-methods',
|
||||||
|
'Which methods are enabled for authentication',
|
||||||
|
'["local"]' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
'oauth-auth',
|
||||||
|
'Configuration for OAuth authentication',
|
||||||
|
'{}' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
'ldap-auth',
|
||||||
|
'Configuration for LDAP authentication',
|
||||||
|
'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Default Certificate Authorities
|
-- Default Certificate Authorities
|
||||||
@@ -104,14 +125,12 @@ INSERT INTO "user" (
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"name",
|
"name",
|
||||||
"nickname",
|
|
||||||
"email",
|
"email",
|
||||||
"is_system"
|
"is_system"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
'System',
|
'System',
|
||||||
'System',
|
|
||||||
'system@localhost',
|
'system@localhost',
|
||||||
TRUE
|
TRUE
|
||||||
);
|
);
|
||||||
|
@@ -17,7 +17,6 @@ CREATE TABLE IF NOT EXISTS `user`
|
|||||||
`updated_at` INTEGER NOT NULL DEFAULT 0,
|
`updated_at` INTEGER NOT NULL DEFAULT 0,
|
||||||
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
||||||
`name` TEXT NOT NULL,
|
`name` TEXT NOT NULL,
|
||||||
`nickname` TEXT NOT NULL,
|
|
||||||
`email` TEXT NOT NULL,
|
`email` TEXT NOT NULL,
|
||||||
`is_system` INTEGER NOT NULL DEFAULT 0,
|
`is_system` INTEGER NOT NULL DEFAULT 0,
|
||||||
`is_disabled` INTEGER NOT NULL DEFAULT 0
|
`is_disabled` INTEGER NOT NULL DEFAULT 0
|
||||||
@@ -45,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `auth`
|
|||||||
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
||||||
`user_id` INTEGER NOT NULL,
|
`user_id` INTEGER NOT NULL,
|
||||||
`type` TEXT NOT NULL,
|
`type` TEXT NOT NULL,
|
||||||
|
`identity` TEXT NOT NULL,
|
||||||
`secret` TEXT NOT NULL,
|
`secret` TEXT NOT NULL,
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
||||||
UNIQUE (`user_id`, `type`)
|
UNIQUE (`user_id`, `type`)
|
||||||
|
@@ -36,6 +36,27 @@ INSERT INTO `setting` (
|
|||||||
"default-site",
|
"default-site",
|
||||||
"What to show users who hit your Nginx server by default",
|
"What to show users who hit your Nginx server by default",
|
||||||
'"welcome"' -- remember this is json
|
'"welcome"' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unixepoch() * 1000,
|
||||||
|
unixepoch() * 1000,
|
||||||
|
"auth-methods",
|
||||||
|
"Which methods are enabled for authentication",
|
||||||
|
'["local"]' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unixepoch() * 1000,
|
||||||
|
unixepoch() * 1000,
|
||||||
|
"oauth-auth",
|
||||||
|
"Configuration for OAuth authentication",
|
||||||
|
'{}' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unixepoch() * 1000,
|
||||||
|
unixepoch() * 1000,
|
||||||
|
"ldap-auth",
|
||||||
|
"Configuration for LDAP authentication",
|
||||||
|
'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Default Certificate Authorities
|
-- Default Certificate Authorities
|
||||||
@@ -103,14 +124,12 @@ INSERT INTO `user` (
|
|||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
name,
|
name,
|
||||||
nickname,
|
|
||||||
email,
|
email,
|
||||||
is_system
|
is_system
|
||||||
) VALUES (
|
) VALUES (
|
||||||
unixepoch() * 1000,
|
unixepoch() * 1000,
|
||||||
unixepoch() * 1000,
|
unixepoch() * 1000,
|
||||||
"System",
|
"System",
|
||||||
"System",
|
|
||||||
"system@localhost",
|
"system@localhost",
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
module npm
|
module npm
|
||||||
|
|
||||||
go 1.23
|
go 1.24
|
||||||
|
|
||||||
|
toolchain go1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
@@ -9,78 +11,86 @@ require (
|
|||||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
|
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e
|
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e
|
||||||
github.com/fatih/color v1.17.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.2.2
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.1
|
github.com/go-chi/jwtauth/v5 v5.3.3
|
||||||
github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760
|
github.com/jc21/go-sse v1.7.0
|
||||||
|
github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/qri-io/jsonschema v0.2.1
|
github.com/qri-io/jsonschema v0.2.1
|
||||||
github.com/rotisserie/eris v0.5.4
|
github.com/rotisserie/eris v0.5.4
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/vrischmann/envconfig v1.3.0
|
github.com/vrischmann/envconfig v1.4.1
|
||||||
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.uber.org/goleak v1.3.0
|
go.uber.org/goleak v1.3.0
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.39.0
|
||||||
gorm.io/datatypes v1.2.1
|
golang.org/x/oauth2 v0.30.0
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/datatypes v1.2.6
|
||||||
gorm.io/driver/postgres v1.5.9
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/gorm v1.25.11
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.30.0
|
||||||
gorm.io/plugin/soft_delete v1.2.1
|
gorm.io/plugin/soft_delete v1.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c // indirect
|
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c // indirect
|
||||||
github.com/lestrrat-go/jwx/v2 v2.0.20 // indirect
|
github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/pdebug/v3 v3.0.1 // indirect
|
github.com/lestrrat-go/pdebug/v3 v3.0.1 // indirect
|
||||||
github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b // indirect
|
github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/qri-io/jsonpointer v0.1.1 // indirect
|
github.com/qri-io/jsonpointer v0.1.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.26.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/uint128 v1.2.0 // indirect
|
lukechampine.com/uint128 v1.3.0 // indirect
|
||||||
modernc.org/cc/v3 v3.40.0 // indirect
|
modernc.org/cc/v3 v3.41.0 // indirect
|
||||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
modernc.org/ccgo/v3 v3.17.0 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/opt v0.1.3 // indirect
|
modernc.org/opt v0.1.3 // indirect
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
modernc.org/sqlite v1.28.0 // indirect
|
||||||
modernc.org/strutil v1.1.3 // indirect
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
modernc.org/token v1.0.1 // indirect
|
modernc.org/token v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/amacneil/dbmate/v2 => github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1
|
replace github.com/amacneil/dbmate/v2 => github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1
|
||||||
|
220
backend/go.sum
220
backend/go.sum
@@ -1,7 +1,11 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
||||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
@@ -12,31 +16,34 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e h1:2R8DvYLNr5DL25eWwpOdPno1eIbTNjJC0d7v8ti5cus=
|
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e h1:2R8DvYLNr5DL25eWwpOdPno1eIbTNjJC0d7v8ti5cus=
|
||||||
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e/go.mod h1:YjikoytuRI4q+GRd3xrOrKJN+Ayi2dwRomHLDDeMHfs=
|
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e/go.mod h1:YjikoytuRI4q+GRd3xrOrKJN+Ayi2dwRomHLDDeMHfs=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo=
|
||||||
|
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
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.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
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 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
@@ -45,22 +52,36 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
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-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
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/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1 h1:WEZwsDG5eXdgh0NfA9SpuShOP6rJCah22ihvZsaoimM=
|
github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1 h1:WEZwsDG5eXdgh0NfA9SpuShOP6rJCah22ihvZsaoimM=
|
||||||
github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1/go.mod h1:XAPcHsokw7nyvFbuN9FdcYb8JSEUTaJDFYOirnNxEvc=
|
github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1/go.mod h1:XAPcHsokw7nyvFbuN9FdcYb8JSEUTaJDFYOirnNxEvc=
|
||||||
github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec h1:KKntwkZlM2w/88QiDyAeZ4th8grqtituzMW8qyapYzc=
|
github.com/jc21/go-sse v1.7.0 h1:Lavb7FGssS2UdJMK1P3r4rQJgg2JZx7BINi8pd/QzKg=
|
||||||
github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
|
github.com/jc21/go-sse v1.7.0/go.mod h1:dbA0LtDgvSEhlAXz+meN1KAcdvcCLiF0aVdhjhZjbGI=
|
||||||
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k=
|
github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96 h1:LzBXyNdALQHC7DUy8PE7IFTihmnSRop4Ps2cBIr/WP8=
|
||||||
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
|
github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96/go.mod h1:Vno1sw0yqqImXnYy3q6ueIG+h+Vwwfbkfw9DTHCrHfY=
|
||||||
|
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=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||||
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
@@ -76,42 +97,39 @@ 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kyoh86/richgo v0.3.8/go.mod h1:2C8POkF1H04iTOG2Tp1yyZhspCME9nN3cir3VXJ02II=
|
|
||||||
github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs=
|
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c h1:pGh5EFIfczeDHwgMHgfwjhZzL+8/E3uZF6T7vER/W8c=
|
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c h1:pGh5EFIfczeDHwgMHgfwjhZzL+8/E3uZF6T7vER/W8c=
|
||||||
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c/go.mod h1:xw2Gm4Mg+ST9s8fHR1VkUIyOJMJnSloRZlPQB+wyVpY=
|
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c/go.mod h1:xw2Gm4Mg+ST9s8fHR1VkUIyOJMJnSloRZlPQB+wyVpY=
|
||||||
github.com/lestrrat-go/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc=
|
github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
|
||||||
github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4=
|
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
|
||||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/lestrrat-go/pdebug/v3 v3.0.1 h1:3G5sX/aw/TbMTtVc9U7IHBWRZtMvwvBziF1e4HoQtv8=
|
github.com/lestrrat-go/pdebug/v3 v3.0.1 h1:3G5sX/aw/TbMTtVc9U7IHBWRZtMvwvBziF1e4HoQtv8=
|
||||||
github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
|
github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
|
||||||
github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b h1:YUFRoeHK/mvRjBR0bBRDC7ZGygYchoQ8j1xMENlObro=
|
github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb h1:DDg5u5lk2v8O8qxs8ecQkMUBj3tLW6wkSLzxxOyi1Ig=
|
||||||
github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b/go.mod h1:s2U6PowV3/Jobkx/S9d0XiPwOzs6niW3DIouw+7nZC8=
|
github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb/go.mod h1:i+E8Uf04vf2QjOWyJdGY75vmG+4rxiZW2kIj1lTB5mo=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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-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.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
|
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||||
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
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 h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
@@ -119,12 +137,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
|
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
|
||||||
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
|
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
|
||||||
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
|
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
|
||||||
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
|
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
|
||||||
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
@@ -136,92 +154,108 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr
|
|||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc=
|
github.com/vrischmann/envconfig v1.4.1 h1:fucz2HsoAkJCLgIngWdWqLNxNjdWD14zfrLF6EQPdY4=
|
||||||
|
github.com/vrischmann/envconfig v1.4.1/go.mod h1:cX3p+/PEssil6fWwzIS7kf8iFpli3giuxXGHxckucYc=
|
||||||
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w=
|
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w=
|
||||||
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4=
|
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4=
|
||||||
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38=
|
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38=
|
||||||
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/datatypes v1.2.1 h1:r+g0bk4LPCW2v4+Ls7aeNgGme7JYdNDQ2VtvlNUfBh0=
|
gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck=
|
||||||
gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs=
|
gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
|
||||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||||
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
|
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||||
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
|
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||||
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
|
||||||
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
|
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=
|
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
|
||||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA=
|
||||||
|
modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
|
||||||
|
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/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
|
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/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
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 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
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/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||||
|
@@ -3,92 +3,238 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
c "npm/internal/api/context"
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
"npm/internal/entity/auth"
|
"npm/internal/entity/auth"
|
||||||
|
"npm/internal/entity/setting"
|
||||||
"npm/internal/entity/user"
|
"npm/internal/entity/user"
|
||||||
"npm/internal/errors"
|
"npm/internal/errors"
|
||||||
|
njwt "npm/internal/jwt"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type setAuthModel struct {
|
// tokenPayload is the structure we expect from a incoming login request
|
||||||
// The json tags are required, as the change password form decodes into this object
|
type tokenPayload struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Secret string `json:"secret"`
|
Identity string `json:"identity"`
|
||||||
CurrentSecret string `json:"current_secret"`
|
Secret string `json:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAuth sets a auth method. This can be used for "me" and `2` for example
|
// GetAuthConfig is anonymous and returns the types of authentication
|
||||||
// Route: POST /users/:userID/auth
|
// enabled for this site
|
||||||
func SetAuth() func(http.ResponseWriter, *http.Request) {
|
func GetAuthConfig() func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
val, err := setting.GetAuthMethods()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, nil)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewToken Also known as a Login, requesting a new token with credentials
|
||||||
|
// Route: POST /auth
|
||||||
|
func NewToken() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Read the bytes from the body
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
|
|
||||||
var newAuth setAuthModel
|
var payload tokenPayload
|
||||||
err := json.Unmarshal(bodyBytes, &newAuth)
|
err := json.Unmarshal(bodyBytes, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, isSelf, userIDErr := getUserIDFromRequest(r)
|
// Check that this auth type is enabled
|
||||||
if userIDErr != nil {
|
if authMethods, err := setting.GetAuthMethods(); err == gorm.ErrRecordNotFound {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
h.ResultResponseJSON(w, r, http.StatusOK, nil)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
return
|
||||||
|
} else if !slices.Contains(authMethods, payload.Type) {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidAuthType.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load user
|
switch payload.Type {
|
||||||
thisUser, thisUserErr := user.GetByID(userID)
|
case "ldap":
|
||||||
if thisUserErr != nil {
|
newTokenLDAP(w, r, payload)
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
|
case "local":
|
||||||
return
|
newTokenLocal(w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenLocal(w http.ResponseWriter, r *http.Request, payload tokenPayload) {
|
||||||
|
// Find user by email
|
||||||
|
userObj, userErr := user.GetByEmail(payload.Identity)
|
||||||
|
if userErr != nil {
|
||||||
|
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), userErr.Error())
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Auth
|
||||||
|
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
|
||||||
|
if authErr != nil {
|
||||||
|
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Auth
|
||||||
|
validateErr := authObj.ValidateSecret(payload.Secret)
|
||||||
|
if validateErr != nil {
|
||||||
|
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
|
||||||
|
// Sleep for 1 second to prevent brute force password guessing
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := njwt.Generate(&userObj, false); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenLDAP(w http.ResponseWriter, r *http.Request, payload tokenPayload) {
|
||||||
|
// Get LDAP settings
|
||||||
|
ldapSettings, err := setting.GetLDAPSettings()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("LDAP settings not found", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets try to authenticate with LDAP
|
||||||
|
ldapUser, err := auth.LDAPAuthenticate(payload.Identity, payload.Secret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("LDAP Auth Error", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Auth by identity
|
||||||
|
authObj, authErr := auth.GetByIdenityType(ldapUser.Username, payload.Type)
|
||||||
|
if authErr == gorm.ErrRecordNotFound {
|
||||||
|
// Auth is not found for this identity. We can create it
|
||||||
|
if !ldapSettings.AutoCreateUser {
|
||||||
|
// LDAP Login was successful, but user does not have an auth record
|
||||||
|
// and auto create is disabled. Showing account disabled error
|
||||||
|
// for the time being
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find user by email
|
||||||
|
foundUser, err := user.GetByEmail(ldapUser.Email)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// User not found, create user
|
||||||
|
foundUser, err = user.CreateFromLDAPUser(ldapUser)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("user.CreateFromLDAPUser", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Created user from LDAP: %s, %s", ldapUser.Username, foundUser.Email)
|
||||||
|
} else if err != nil {
|
||||||
|
logger.Error("user.GetByEmail", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create auth record and attach to this user
|
||||||
|
authObj = auth.Model{
|
||||||
|
UserID: foundUser.ID,
|
||||||
|
Type: auth.TypeLDAP,
|
||||||
|
Identity: ldapUser.Username,
|
||||||
|
}
|
||||||
|
if err := authObj.Save(); err != nil {
|
||||||
|
logger.Error("auth.Save", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Created LDAP auth for user: %s, %s", ldapUser.Username, foundUser.Email)
|
||||||
|
} else if authErr != nil {
|
||||||
|
logger.Error("auth.GetByIdenityType", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, authErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userObj, userErr := user.GetByID(authObj.UserID)
|
||||||
|
if userErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := njwt.Generate(&userObj, false); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken an existing token by given them a new one with the same claims
|
||||||
|
// Route: POST /auth/refresh
|
||||||
|
func RefreshToken() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: Use your own methods to verify an existing user is
|
||||||
|
// able to refresh their token and then give them a new one
|
||||||
|
userObj, _ := user.GetByEmail("jc@jc21.com")
|
||||||
|
if response, err := njwt.Generate(&userObj, false); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSSEToken will generate and return a very short lived token for
|
||||||
|
// use by the /sse/* endpoint. It requires an app token to generate this
|
||||||
|
// Route: POST /auth/sse
|
||||||
|
func NewSSEToken() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := r.Context().Value(c.UserIDCtxKey).(uint)
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
userObj, userErr := user.GetByID(userID)
|
||||||
|
if userErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := njwt.Generate(&userObj, true); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
if thisUser.IsSystem {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing auth for user
|
|
||||||
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
|
|
||||||
if userAuthErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSelf {
|
|
||||||
// confirm that the current_secret given is valid for the one stored in the database
|
|
||||||
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
|
|
||||||
if validateErr != nil {
|
|
||||||
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
|
|
||||||
// Sleep for 1 second to prevent brute force password guessing
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newAuth.Type == auth.TypePassword {
|
|
||||||
err := userAuth.SetPassword(newAuth.Secret)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("SetPasswordError", err)
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = userAuth.Save(); err != nil {
|
|
||||||
logger.Error("AuthSaveError", err)
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userAuth.Secret = ""
|
|
||||||
|
|
||||||
// todo: add to audit-log
|
|
||||||
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,6 +11,8 @@ import (
|
|||||||
"npm/internal/api/middleware"
|
"npm/internal/api/middleware"
|
||||||
"npm/internal/entity/certificateauthority"
|
"npm/internal/entity/certificateauthority"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCertificateAuthorities will return a list of Certificate Authorities
|
// GetCertificateAuthorities will return a list of Certificate Authorities
|
||||||
@@ -46,7 +47,7 @@ func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := certificateauthority.GetByID(caID)
|
item, err := certificateauthority.GetByID(caID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
@@ -100,7 +101,7 @@ func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
ca, err := certificateauthority.GetByID(caID)
|
ca, err := certificateauthority.GetByID(caID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
@@ -140,7 +141,7 @@ func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := certificateauthority.GetByID(caID)
|
item, err := certificateauthority.GetByID(caID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,6 +13,8 @@ import (
|
|||||||
"npm/internal/entity/host"
|
"npm/internal/entity/host"
|
||||||
"npm/internal/jobqueue"
|
"npm/internal/jobqueue"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCertificates will return a list of Certificates
|
// GetCertificates will return a list of Certificates
|
||||||
@@ -138,7 +139,7 @@ func getCertificateFromRequest(w http.ResponseWriter, r *http.Request) *certific
|
|||||||
|
|
||||||
certificateObject, err := certificate.GetByID(certificateID)
|
certificateObject, err := certificate.GetByID(certificateID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
return &certificateObject
|
return &certificateObject
|
||||||
@@ -150,7 +151,7 @@ func getCertificateFromRequest(w http.ResponseWriter, r *http.Request) *certific
|
|||||||
|
|
||||||
// fillObjectFromBody has some reusable code for all endpoints that
|
// fillObjectFromBody has some reusable code for all endpoints that
|
||||||
// have a certificate id in the url. it will write errors to the output.
|
// have a certificate id in the url. it will write errors to the output.
|
||||||
func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema string, o interface{}) bool {
|
func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema string, o any) bool {
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
|
|
||||||
if validationSchema != "" {
|
if validationSchema != "" {
|
||||||
@@ -175,10 +176,10 @@ func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureCertificate(c certificate.Model) {
|
func configureCertificate(cert certificate.Model) {
|
||||||
err := jobqueue.AddJob(jobqueue.Job{
|
err := jobqueue.AddJob(jobqueue.Job{
|
||||||
Name: "RequestCertificate",
|
Name: "RequestCertificate",
|
||||||
Action: c.Request,
|
Action: cert.Request,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("ConfigureCertificateError", err)
|
logger.Error("ConfigureCertificateError", err)
|
||||||
|
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
"npm/internal/config"
|
"npm/internal/config"
|
||||||
)
|
)
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,6 +11,8 @@ import (
|
|||||||
"npm/internal/dnsproviders"
|
"npm/internal/dnsproviders"
|
||||||
"npm/internal/entity/dnsprovider"
|
"npm/internal/entity/dnsprovider"
|
||||||
"npm/internal/errors"
|
"npm/internal/errors"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDNSProviders will return a list of DNS Providers
|
// GetDNSProviders will return a list of DNS Providers
|
||||||
@@ -46,7 +47,7 @@ func GetDNSProvider() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := dnsprovider.GetByID(providerID)
|
item, err := dnsprovider.GetByID(providerID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
@@ -95,7 +96,7 @@ func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := dnsprovider.GetByID(providerID)
|
item, err := dnsprovider.GetByID(providerID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
@@ -130,7 +131,7 @@ func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := dnsprovider.GetByID(providerID)
|
item, err := dnsprovider.GetByID(providerID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||||
|
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"npm/internal/acme"
|
"npm/internal/acme"
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
"npm/internal/config"
|
"npm/internal/config"
|
||||||
|
@@ -21,11 +21,22 @@ func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
|
|||||||
return pageInfo, err
|
return pageInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageInfo.Sort = middleware.GetSortFromContext(r)
|
|
||||||
|
|
||||||
return pageInfo, nil
|
return pageInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getQueryVarString(r *http.Request, varName string, required bool, defaultValue string) (string, error) {
|
||||||
|
queryValues := r.URL.Query()
|
||||||
|
varValue := queryValues.Get(varName)
|
||||||
|
|
||||||
|
if varValue == "" && required {
|
||||||
|
return "", eris.Errorf("%v was not supplied in the request", varName)
|
||||||
|
} else if varValue == "" {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return varValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) {
|
func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) {
|
||||||
queryValues := r.URL.Query()
|
queryValues := r.URL.Query()
|
||||||
varValue := queryValues.Get(varName)
|
varValue := queryValues.Get(varName)
|
||||||
@@ -45,7 +56,7 @@ func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getURLParamInt(r *http.Request, varName string) (uint, error) {
|
func getURLParamInt(r *http.Request, varName string) (uint, error) {
|
||||||
var defaultValue uint = 0
|
var defaultValue uint
|
||||||
|
|
||||||
required := true
|
required := true
|
||||||
paramStr := chi.URLParam(r, varName)
|
paramStr := chi.URLParam(r, varName)
|
||||||
|
@@ -3,9 +3,10 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"npm/internal/model"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"npm/internal/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,6 +13,8 @@ import (
|
|||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
"npm/internal/nginx"
|
"npm/internal/nginx"
|
||||||
"npm/internal/validator"
|
"npm/internal/validator"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetHosts will return a list of Hosts
|
// GetHosts will return a list of Hosts
|
||||||
@@ -48,7 +49,7 @@ func GetHost() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := host.GetByID(hostID)
|
item, err := host.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
@@ -111,7 +112,7 @@ func UpdateHost() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
hostObject, err := host.GetByID(hostID)
|
hostObject, err := host.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
@@ -156,7 +157,7 @@ func DeleteHost() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := host.GetByID(hostID)
|
item, err := host.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||||
@@ -181,7 +182,7 @@ func GetHostNginxConfig(format string) func(http.ResponseWriter, *http.Request)
|
|||||||
|
|
||||||
item, err := host.GetByID(hostID)
|
item, err := host.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// Get the config from disk
|
// Get the config from disk
|
||||||
@@ -191,7 +192,7 @@ func GetHostNginxConfig(format string) func(http.ResponseWriter, *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if format == "text" {
|
if format == "text" {
|
||||||
h.ResultResponseText(w, r, http.StatusOK, content)
|
h.ResultResponseText(w, http.StatusOK, content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, content)
|
h.ResultResponseJSON(w, r, http.StatusOK, content)
|
||||||
@@ -201,11 +202,11 @@ func GetHostNginxConfig(format string) func(http.ResponseWriter, *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureHost(h host.Model) {
|
func configureHost(hst host.Model) {
|
||||||
err := jobqueue.AddJob(jobqueue.Job{
|
err := jobqueue.AddJob(jobqueue.Job{
|
||||||
Name: "NginxConfigureHost",
|
Name: "NginxConfigureHost",
|
||||||
Action: func() error {
|
Action: func() error {
|
||||||
return nginx.ConfigureHost(h)
|
return nginx.ConfigureHost(hst)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
"npm/internal/api/middleware"
|
"npm/internal/api/middleware"
|
||||||
"npm/internal/entity/nginxtemplate"
|
"npm/internal/entity/nginxtemplate"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetNginxTemplates will return a list of Nginx Templates
|
// GetNginxTemplates will return a list of Nginx Templates
|
||||||
@@ -44,7 +45,7 @@ func GetNginxTemplate() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := nginxtemplate.GetByID(templateID)
|
item, err := nginxtemplate.GetByID(templateID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
@@ -95,7 +96,7 @@ func UpdateNginxTemplate() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
nginxTemplate, err := nginxtemplate.GetByID(templateID)
|
nginxTemplate, err := nginxtemplate.GetByID(templateID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
@@ -130,7 +131,7 @@ func DeleteNginxTemplate() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := nginxtemplate.GetByID(templateID)
|
item, err := nginxtemplate.GetByID(templateID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||||
|
156
backend/internal/api/handler/oauth.go
Normal file
156
backend/internal/api/handler/oauth.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
h "npm/internal/api/http"
|
||||||
|
"npm/internal/entity/auth"
|
||||||
|
"npm/internal/entity/setting"
|
||||||
|
"npm/internal/entity/user"
|
||||||
|
"npm/internal/errors"
|
||||||
|
njwt "npm/internal/jwt"
|
||||||
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getRequestIPAddress will use X-FORWARDED-FOR header 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)
|
||||||
|
}
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthLogin ...
|
||||||
|
// Route: GET /oauth/login
|
||||||
|
func OAuthLogin() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !setting.AuthMethodEnabled(auth.TypeOAuth) {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectBase, _ := getQueryVarString(r, "redirect_base", false, "")
|
||||||
|
url, err := auth.OAuthLogin(redirectBase, getRequestIPAddress(r))
|
||||||
|
if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthRedirect ...
|
||||||
|
// Route: GET /oauth/redirect
|
||||||
|
func OAuthRedirect() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !setting.AuthMethodEnabled(auth.TypeOAuth) {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := getQueryVarString(r, "code", true, "")
|
||||||
|
if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ou, err := auth.OAuthReturn(r.Context(), code, getRequestIPAddress(r))
|
||||||
|
if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ou.Identifier == "" {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, "User found, but OAuth identifier seems misconfigured", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, err := newTokenOAuth(ou)
|
||||||
|
if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode jwt to json
|
||||||
|
j, _ := json.Marshal(jwt)
|
||||||
|
|
||||||
|
// Redirect to frontend with success
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/?token_response=%s", url.QueryEscape(string(j))), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTokenOAuth takes a OAuthUser and creates a new token,
|
||||||
|
// optionally creating a new user if one does not exist
|
||||||
|
func newTokenOAuth(ou *auth.OAuthUser) (*njwt.GeneratedResponse, error) {
|
||||||
|
// Get OAuth settings
|
||||||
|
oAuthSettings, err := setting.GetOAuthSettings()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("OAuth settings not found", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Auth by identity
|
||||||
|
authObj, authErr := auth.GetByIdenityType(ou.GetID(), auth.TypeOAuth)
|
||||||
|
if authErr == gorm.ErrRecordNotFound {
|
||||||
|
// Auth is not found for this identity. We can create it
|
||||||
|
if !oAuthSettings.AutoCreateUser {
|
||||||
|
// user does not have an auth record
|
||||||
|
// and auto create is disabled. Showing account disabled error
|
||||||
|
// for the time being
|
||||||
|
return nil, errors.ErrUserDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find user by email
|
||||||
|
foundUser, err := user.GetByEmail(ou.GetEmail())
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// User not found, create user
|
||||||
|
foundUser, err = user.CreateFromOAuthUser(ou)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("user.CreateFromOAuthUser", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Info("Created user from OAuth: %s, %s", ou.GetID(), foundUser.Email)
|
||||||
|
} else if err != nil {
|
||||||
|
logger.Error("user.GetByEmail", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create auth record and attach to this user
|
||||||
|
authObj = auth.Model{
|
||||||
|
UserID: foundUser.ID,
|
||||||
|
Type: auth.TypeOAuth,
|
||||||
|
Identity: ou.GetID(),
|
||||||
|
}
|
||||||
|
if err := authObj.Save(); err != nil {
|
||||||
|
logger.Error("auth.Save", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Info("Created OAuth auth for user: %s, %s", ou.GetID(), foundUser.Email)
|
||||||
|
} else if authErr != nil {
|
||||||
|
logger.Error("auth.GetByIdenityType", err)
|
||||||
|
return nil, authErr
|
||||||
|
}
|
||||||
|
|
||||||
|
userObj, userErr := user.GetByID(authObj.UserID)
|
||||||
|
if userErr != nil {
|
||||||
|
return nil, userErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
return nil, errors.ErrUserDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt, err := njwt.Generate(&userObj, false)
|
||||||
|
return &jwt, err
|
||||||
|
}
|
@@ -12,7 +12,7 @@ import (
|
|||||||
"npm/internal/config"
|
"npm/internal/config"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
jsref "github.com/jc21/jsref"
|
"github.com/jc21/jsref"
|
||||||
"github.com/jc21/jsref/provider"
|
"github.com/jc21/jsref/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ var (
|
|||||||
// Schema simply reads the swagger schema from disk and returns is raw
|
// Schema simply reads the swagger schema from disk and returns is raw
|
||||||
// Route: GET /schema
|
// Route: GET /schema
|
||||||
func Schema() func(http.ResponseWriter, *http.Request) {
|
func Schema() func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprint(w, string(getSchema()))
|
fmt.Fprint(w, string(getSchema()))
|
||||||
@@ -42,8 +42,8 @@ func getSchema() []byte {
|
|||||||
swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version))
|
swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version))
|
||||||
|
|
||||||
// Dereference the JSON Schema:
|
// Dereference the JSON Schema:
|
||||||
var schema interface{}
|
var sch any
|
||||||
if err := json.Unmarshal(swaggerSchema, &schema); err != nil {
|
if err := json.Unmarshal(swaggerSchema, &sch); err != nil {
|
||||||
logger.Error("SwaggerUnmarshalError", err)
|
logger.Error("SwaggerUnmarshalError", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func getSchema() []byte {
|
|||||||
logger.Error("SchemaProviderError", err)
|
logger.Error("SchemaProviderError", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...)
|
result, err := resolver.Resolve(sch, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("SwaggerResolveError", err)
|
logger.Error("SwaggerResolveError", err)
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,6 +11,7 @@ import (
|
|||||||
"npm/internal/entity/setting"
|
"npm/internal/entity/setting"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSettings will return a list of Settings
|
// GetSettings will return a list of Settings
|
||||||
@@ -41,7 +41,7 @@ func GetSetting() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := setting.GetByName(name)
|
item, err := setting.GetByName(name)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
@@ -64,6 +64,12 @@ func CreateSetting() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the setting already exists
|
||||||
|
if _, err := setting.GetByName(newSetting.Name); err == nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Setting with name '%s' already exists", newSetting.Name), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = newSetting.Save(); err != nil {
|
if err = newSetting.Save(); err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
|
||||||
return
|
return
|
||||||
@@ -75,23 +81,23 @@ func CreateSetting() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
// UpdateSetting updates a setting
|
// UpdateSetting updates a setting
|
||||||
// Route: PUT /settings/{name}
|
// Route: PUT /settings/{name}
|
||||||
|
// TODO: Add validation for the setting value, for system settings they should be validated against the setting name and type
|
||||||
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
settingName := chi.URLParam(r, "name")
|
settingName := chi.URLParam(r, "name")
|
||||||
|
|
||||||
setting, err := setting.GetByName(settingName)
|
setting, err := setting.GetByName(settingName)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
err := json.Unmarshal(bodyBytes, &setting)
|
if err := json.Unmarshal(bodyBytes, &setting); err != nil {
|
||||||
if err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = setting.Save(); err != nil {
|
if err := setting.Save(); err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
"npm/internal/api/middleware"
|
"npm/internal/api/middleware"
|
||||||
"npm/internal/entity/stream"
|
"npm/internal/entity/stream"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetStreams will return a list of Streams
|
// GetStreams will return a list of Streams
|
||||||
@@ -44,7 +45,7 @@ func GetStream() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := stream.GetByID(hostID)
|
item, err := stream.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
h.ResultResponseJSON(w, r, http.StatusOK, item)
|
||||||
@@ -93,7 +94,7 @@ func UpdateStream() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
host, err := stream.GetByID(hostID)
|
host, err := stream.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
@@ -128,7 +129,7 @@ func DeleteStream() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := stream.GetByID(hostID)
|
item, err := stream.GetByID(hostID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
|
||||||
|
@@ -1,116 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
h "npm/internal/api/http"
|
|
||||||
"npm/internal/errors"
|
|
||||||
"npm/internal/logger"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
|
||||||
"npm/internal/entity/auth"
|
|
||||||
"npm/internal/entity/user"
|
|
||||||
njwt "npm/internal/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tokenPayload is the structure we expect from a incoming login request
|
|
||||||
type tokenPayload struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Identity string `json:"identity"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewToken Also known as a Login, requesting a new token with credentials
|
|
||||||
// Route: POST /tokens
|
|
||||||
func NewToken() func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Read the bytes from the body
|
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
|
||||||
|
|
||||||
var payload tokenPayload
|
|
||||||
err := json.Unmarshal(bodyBytes, &payload)
|
|
||||||
if err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find user
|
|
||||||
userObj, userErr := user.GetByEmail(payload.Identity)
|
|
||||||
if userErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userObj.IsDisabled {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Auth
|
|
||||||
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
|
|
||||||
if authErr != nil {
|
|
||||||
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify Auth
|
|
||||||
validateErr := authObj.ValidateSecret(payload.Secret)
|
|
||||||
if validateErr != nil {
|
|
||||||
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
|
|
||||||
// Sleep for 1 second to prevent brute force password guessing
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response, err := njwt.Generate(&userObj, false); err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshToken an existing token by given them a new one with the same claims
|
|
||||||
// Route: GET /tokens
|
|
||||||
func RefreshToken() func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// TODO: Use your own methods to verify an existing user is
|
|
||||||
// able to refresh their token and then give them a new one
|
|
||||||
userObj, _ := user.GetByEmail("jc@jc21.com")
|
|
||||||
if response, err := njwt.Generate(&userObj, false); err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSSEToken will generate and return a very short lived token for
|
|
||||||
// use by the /sse/* endpoint. It requires an app token to generate this
|
|
||||||
// Route: POST /tokens/sse
|
|
||||||
func NewSSEToken() func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Context().Value(c.UserIDCtxKey).(uint)
|
|
||||||
|
|
||||||
// Find user
|
|
||||||
userObj, userErr := user.GetByID(userID)
|
|
||||||
if userErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userObj.IsDisabled {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response, err := njwt.Generate(&userObj, true); err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +1,6 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,6 +14,8 @@ import (
|
|||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
"npm/internal/nginx"
|
"npm/internal/nginx"
|
||||||
"npm/internal/validator"
|
"npm/internal/validator"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUpstreams will return a list of Upstreams
|
// GetUpstreams will return a list of Upstreams
|
||||||
@@ -49,7 +50,7 @@ func GetUpstream() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := upstream.GetByID(upstreamID)
|
item, err := upstream.GetByID(upstreamID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
@@ -94,7 +95,7 @@ func CreateUpstream() func(http.ResponseWriter, *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateHost updates a host
|
// UpdateUpstream updates a stream
|
||||||
// Route: PUT /upstreams/{upstreamID}
|
// Route: PUT /upstreams/{upstreamID}
|
||||||
func UpdateUpstream() func(http.ResponseWriter, *http.Request) {
|
func UpdateUpstream() func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -149,7 +150,7 @@ func DeleteUpstream() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := upstream.GetByID(upstreamID)
|
item, err := upstream.GetByID(upstreamID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// Ensure that this upstream isn't in use by a host
|
// Ensure that this upstream isn't in use by a host
|
||||||
@@ -166,7 +167,7 @@ func DeleteUpstream() func(http.ResponseWriter, *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostNginxConfig will return a Host's nginx config from disk
|
// GetUpstreamNginxConfig will return a Host's nginx config from disk
|
||||||
// Route: GET /upstreams/{upstreamID}/nginx-config
|
// Route: GET /upstreams/{upstreamID}/nginx-config
|
||||||
// Route: GET /upstreams/{upstreamID}/nginx-config.txt
|
// Route: GET /upstreams/{upstreamID}/nginx-config.txt
|
||||||
func GetUpstreamNginxConfig(format string) func(http.ResponseWriter, *http.Request) {
|
func GetUpstreamNginxConfig(format string) func(http.ResponseWriter, *http.Request) {
|
||||||
@@ -180,7 +181,7 @@ func GetUpstreamNginxConfig(format string) func(http.ResponseWriter, *http.Reque
|
|||||||
|
|
||||||
item, err := upstream.GetByID(upstreamID)
|
item, err := upstream.GetByID(upstreamID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// Get the config from disk
|
// Get the config from disk
|
||||||
@@ -190,7 +191,7 @@ func GetUpstreamNginxConfig(format string) func(http.ResponseWriter, *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if format == "text" {
|
if format == "text" {
|
||||||
h.ResultResponseText(w, r, http.StatusOK, content)
|
h.ResultResponseText(w, http.StatusOK, content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, content)
|
h.ResultResponseJSON(w, r, http.StatusOK, content)
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
c "npm/internal/api/context"
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
@@ -15,8 +15,16 @@ import (
|
|||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type setAuthModel struct {
|
||||||
|
// The json tags are required, as the change password form decodes into this object
|
||||||
|
Type string `json:"type"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
CurrentSecret string `json:"current_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsers returns all users
|
// GetUsers returns all users
|
||||||
// Route: GET /users
|
// Route: GET /users
|
||||||
func GetUsers() func(http.ResponseWriter, *http.Request) {
|
func GetUsers() func(http.ResponseWriter, *http.Request) {
|
||||||
@@ -48,7 +56,7 @@ func GetUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := user.GetByID(userID)
|
item, err := user.GetByID(userID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
@@ -72,7 +80,7 @@ func UpdateUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
userObject, err := user.GetByID(userID)
|
userObject, err := user.GetByID(userID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
// nolint: errcheck,gosec
|
// nolint: errcheck,gosec
|
||||||
@@ -136,7 +144,7 @@ func DeleteUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
item, err := user.GetByID(userID)
|
item, err := user.GetByID(userID)
|
||||||
switch err {
|
switch err {
|
||||||
case sql.ErrNoRows:
|
case gorm.ErrRecordNotFound:
|
||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
if err := item.Delete(); err != nil {
|
if err := item.Delete(); err != nil {
|
||||||
@@ -188,7 +196,7 @@ func CreateUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
// newUser has been saved, now save their auth
|
// newUser has been saved, now save their auth
|
||||||
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
|
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
|
||||||
newUser.Auth.UserID = newUser.ID
|
newUser.Auth.UserID = newUser.ID
|
||||||
if newUser.Auth.Type == auth.TypePassword {
|
if newUser.Auth.Type == auth.TypeLocal {
|
||||||
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
|
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("SetPasswordError", err)
|
logger.Error("SetPasswordError", err)
|
||||||
@@ -247,3 +255,79 @@ func getUserIDFromRequest(r *http.Request) (uint, bool, error) {
|
|||||||
}
|
}
|
||||||
return userID, self, nil
|
return userID, self, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAuth sets a auth method. This can be used for "me" and `2` for example
|
||||||
|
// Route: POST /users/:userID/auth
|
||||||
|
func SetAuth() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
|
|
||||||
|
var newAuth setAuthModel
|
||||||
|
err := json.Unmarshal(bodyBytes, &newAuth)
|
||||||
|
if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, isSelf, userIDErr := getUserIDFromRequest(r)
|
||||||
|
if userIDErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user
|
||||||
|
thisUser, thisUserErr := user.GetByID(userID)
|
||||||
|
if thisUserErr == gorm.ErrRecordNotFound {
|
||||||
|
h.NotFound(w, r)
|
||||||
|
return
|
||||||
|
} else if thisUserErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisUser.IsSystem {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing auth for user
|
||||||
|
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
|
||||||
|
if userAuthErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSelf {
|
||||||
|
// confirm that the current_secret given is valid for the one stored in the database
|
||||||
|
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
|
||||||
|
if validateErr != nil {
|
||||||
|
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
|
||||||
|
// Sleep for 1 second to prevent brute force password guessing
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newAuth.Type == auth.TypeLocal {
|
||||||
|
err := userAuth.SetPassword(newAuth.Secret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("SetPasswordError", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = userAuth.Save(); err != nil {
|
||||||
|
logger.Error("AuthSaveError", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuth.Secret = ""
|
||||||
|
|
||||||
|
// todo: add to audit-log
|
||||||
|
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -21,19 +21,19 @@ var (
|
|||||||
|
|
||||||
// Response interface for standard API results
|
// Response interface for standard API results
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Result interface{} `json:"result"`
|
Result any `json:"result"`
|
||||||
Error interface{} `json:"error,omitempty"`
|
Error any `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorResponse interface for errors returned via the API
|
// ErrorResponse interface for errors returned via the API
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Code interface{} `json:"code"`
|
Code any `json:"code"`
|
||||||
Message interface{} `json:"message"`
|
Message any `json:"message"`
|
||||||
Invalid interface{} `json:"invalid,omitempty"`
|
Invalid any `json:"invalid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultResponseJSON will write the result as json to the http output
|
// ResultResponseJSON will write the result as json to the http output
|
||||||
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) {
|
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result any) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errs []jsonsc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResultErrorJSON will format the result as a standard error object and send it for output
|
// ResultErrorJSON will format the result as a standard error object and send it for output
|
||||||
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) {
|
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended any) {
|
||||||
errorResponse := ErrorResponse{
|
errorResponse := ErrorResponse{
|
||||||
Code: status,
|
Code: status,
|
||||||
Message: message,
|
Message: message,
|
||||||
@@ -98,7 +98,7 @@ func NotFound(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResultResponseText will write the result as text to the http output
|
// ResultResponseText will write the result as text to the http output
|
||||||
func ResultResponseText(w http.ResponseWriter, r *http.Request, status int, content string) {
|
func ResultResponseText(w http.ResponseWriter, status int, content string) {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
fmt.Fprint(w, content)
|
fmt.Fprint(w, content)
|
||||||
|
@@ -4,9 +4,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"npm/internal/entity/user"
|
"npm/internal/entity/user"
|
||||||
"npm/internal/model"
|
"npm/internal/model"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/qri-io/jsonschema"
|
"github.com/qri-io/jsonschema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -20,7 +21,7 @@ func TestResultResponseJSON(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
status int
|
status int
|
||||||
given interface{}
|
given any
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -33,12 +34,11 @@ func TestResultResponseJSON(t *testing.T) {
|
|||||||
name: "detailed response",
|
name: "detailed response",
|
||||||
status: http.StatusBadRequest,
|
status: http.StatusBadRequest,
|
||||||
given: user.Model{
|
given: user.Model{
|
||||||
ModelBase: model.ModelBase{ID: 10},
|
Base: model.Base{ID: 10},
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
Nickname: "Jonny",
|
|
||||||
},
|
},
|
||||||
want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"nickname\":\"Jonny\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}",
|
want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error response",
|
name: "error response",
|
||||||
@@ -118,7 +118,7 @@ func TestResultErrorJSON(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
status int
|
status int
|
||||||
message string
|
message string
|
||||||
extended interface{}
|
extended any
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -180,9 +180,8 @@ func TestResultResponseText(t *testing.T) {
|
|||||||
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
t.Run("basic test", func(t *testing.T) {
|
t.Run("basic test", func(t *testing.T) {
|
||||||
r := httptest.NewRequest(http.MethodGet, "/anything", nil)
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
ResultResponseText(w, r, http.StatusOK, "omg this works")
|
ResultResponseText(w, http.StatusOK, "omg this works")
|
||||||
res := w.Result()
|
res := w.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
|
@@ -15,7 +15,7 @@ func TestAccessControl(t *testing.T) {
|
|||||||
// goleak is used to detect goroutine leaks
|
// goleak is used to detect goroutine leaks
|
||||||
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
c "npm/internal/api/context"
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
@@ -11,7 +12,6 @@ import (
|
|||||||
"npm/internal/entity/user"
|
"npm/internal/entity/user"
|
||||||
njwt "npm/internal/jwt"
|
njwt "npm/internal/jwt"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
"npm/internal/util"
|
|
||||||
|
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
)
|
)
|
||||||
@@ -35,7 +35,7 @@ func DecodeAuth() func(http.Handler) http.Handler {
|
|||||||
// Enforce is a authentication middleware to enforce access from the
|
// Enforce is a authentication middleware to enforce access from the
|
||||||
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
|
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
|
||||||
// response for any unverified tokens and passes the good ones through.
|
// response for any unverified tokens and passes the good ones through.
|
||||||
func Enforce(permission string) func(http.Handler) http.Handler {
|
func Enforce(permissions ...string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -56,7 +56,7 @@ func Enforce(permission string) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if permissions exist for this user
|
// Check if permissions exist for this user
|
||||||
if permission != "" {
|
if len(permissions) > 0 {
|
||||||
// Since the permission that we require is not on the token, we have to get it from the DB
|
// Since the permission that we require is not on the token, we have to get it from the DB
|
||||||
// So we don't go crazy with hits, we will use a memory cache
|
// So we don't go crazy with hits, we will use a memory cache
|
||||||
cacheKey := fmt.Sprintf("userCapabilties.%v", userID)
|
cacheKey := fmt.Sprintf("userCapabilties.%v", userID)
|
||||||
@@ -75,9 +75,16 @@ func Enforce(permission string) func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
// Now check that they have the permission in their admin capabilities
|
// Now check that they have the permission in their admin capabilities
|
||||||
// full-admin can do anything
|
// full-admin can do anything
|
||||||
if !util.SliceContainsItem(userCapabilities, user.CapabilityFullAdmin) && !util.SliceContainsItem(userCapabilities, permission) {
|
hasOnePermission := false
|
||||||
|
for _, permission := range permissions {
|
||||||
|
if slices.Contains(userCapabilities, user.CapabilityFullAdmin) || slices.Contains(userCapabilities, permission) {
|
||||||
|
hasOnePermission = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasOnePermission {
|
||||||
// Access denied
|
// Access denied
|
||||||
logger.Debug("User has: %+v but needs %s", userCapabilities, permission)
|
logger.Debug("Enforce Failed: User has %v but needs %v", userCapabilities, permissions)
|
||||||
h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", nil)
|
h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,6 @@ func AuthCacheInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthCacheSet will store the item in memory for the expiration time
|
// AuthCacheSet will store the item in memory for the expiration time
|
||||||
func AuthCacheSet(k string, x interface{}) {
|
func AuthCacheSet(k string, x any) {
|
||||||
AuthCache.Set(k, x, cache.DefaultExpiration)
|
AuthCache.Set(k, x, cache.DefaultExpiration)
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,7 @@ func TestBodyContext(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
// Create a test handler that checks the context for the body data
|
// Create a test handler that checks the context for the body data
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
assert.Equal(t, body, bodyData)
|
||||||
})
|
})
|
||||||
|
@@ -15,7 +15,7 @@ func TestCors(t *testing.T) {
|
|||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.Cors(r))
|
r.Use(middleware.Cors(r))
|
||||||
|
|
||||||
r.Get("/test", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/test", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Write([]byte("test"))
|
w.Write([]byte("test"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ func TestOptions(t *testing.T) {
|
|||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.Options(r))
|
r.Use(middleware.Options(r))
|
||||||
|
|
||||||
r.Get("/test", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/test", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Write([]byte("test"))
|
w.Write([]byte("test"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
@@ -9,15 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// EnforceSetup will error if the config setup doesn't match what is required
|
// EnforceSetup will error if the config setup doesn't match what is required
|
||||||
func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler {
|
func EnforceSetup() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if config.IsSetup != shouldBeSetup {
|
if !config.IsSetup {
|
||||||
state := "during"
|
h.ResultErrorJSON(w, r, http.StatusForbidden, "Not available during setup phase", nil)
|
||||||
if config.IsSetup {
|
|
||||||
state = "after"
|
|
||||||
}
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,11 +5,11 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"go.uber.org/goleak"
|
|
||||||
|
|
||||||
"npm/internal/api/middleware"
|
"npm/internal/api/middleware"
|
||||||
"npm/internal/config"
|
"npm/internal/config"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnforceSetup(t *testing.T) {
|
func TestEnforceSetup(t *testing.T) {
|
||||||
@@ -17,34 +17,19 @@ func TestEnforceSetup(t *testing.T) {
|
|||||||
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
shouldBeSetup bool
|
isSetup bool
|
||||||
isSetup bool
|
expectedCode int
|
||||||
expectedCode int
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "should allow request when setup is expected and is setup",
|
name: "should allow request when setup is expected and is setup",
|
||||||
shouldBeSetup: true,
|
isSetup: true,
|
||||||
isSetup: true,
|
expectedCode: http.StatusOK,
|
||||||
expectedCode: http.StatusOK,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "should error when setup is expected but not setup",
|
name: "should error when setup is expected but not setup",
|
||||||
shouldBeSetup: true,
|
isSetup: false,
|
||||||
isSetup: false,
|
expectedCode: http.StatusForbidden,
|
||||||
expectedCode: http.StatusForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should allow request when setup is not expected and not setup",
|
|
||||||
shouldBeSetup: false,
|
|
||||||
isSetup: false,
|
|
||||||
expectedCode: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should error when setup is not expected but is setup",
|
|
||||||
shouldBeSetup: false,
|
|
||||||
isSetup: true,
|
|
||||||
expectedCode: http.StatusForbidden,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +37,7 @@ func TestEnforceSetup(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
config.IsSetup = tt.isSetup
|
config.IsSetup = tt.isSetup
|
||||||
|
|
||||||
handler := middleware.EnforceSetup(tt.shouldBeSetup)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := middleware.EnforceSetup()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ func TestExpansion(t *testing.T) {
|
|||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||||
expand := middleware.GetExpandFromContext(r)
|
expand := middleware.GetExpandFromContext(r)
|
||||||
assert.Equal(t, []string{"item1", "item2"}, expand)
|
assert.Equal(t, []string{"item1", "item2"}, expand)
|
||||||
})
|
})
|
||||||
@@ -39,7 +39,7 @@ func TestExpansion(t *testing.T) {
|
|||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||||
expand := middleware.GetExpandFromContext(r)
|
expand := middleware.GetExpandFromContext(r)
|
||||||
assert.Nil(t, expand)
|
assert.Nil(t, expand)
|
||||||
})
|
})
|
||||||
|
@@ -21,21 +21,21 @@ import (
|
|||||||
// and the sort parameter is valid as well.
|
// and the sort parameter is valid as well.
|
||||||
// After we have determined what the Filters are to be, they are saved on the Context
|
// After we have determined what the Filters are to be, they are saved on the Context
|
||||||
// to be used later in other endpoints.
|
// to be used later in other endpoints.
|
||||||
func ListQuery(obj interface{}) func(http.Handler) http.Handler {
|
func ListQuery(obj any) func(http.Handler) http.Handler {
|
||||||
schemaData := tags.GetFilterSchema(obj)
|
schemaData := tags.GetFilterSchema(obj)
|
||||||
filterMap := tags.GetFilterMap(obj)
|
filterMap := tags.GetFilterMap(obj, "")
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
ctx, statusCode, errMsg, errors := listQueryFilters(r, ctx, schemaData)
|
ctx, statusCode, errMsg, errors := listQueryFilters(ctx, r, schemaData)
|
||||||
if statusCode > 0 {
|
if statusCode > 0 {
|
||||||
h.ResultErrorJSON(w, r, statusCode, errMsg, errors)
|
h.ResultErrorJSON(w, r, statusCode, errMsg, errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, statusCode, errMsg = listQuerySort(r, filterMap, ctx)
|
ctx, statusCode, errMsg = listQuerySort(ctx, r, filterMap)
|
||||||
if statusCode > 0 {
|
if statusCode > 0 {
|
||||||
h.ResultErrorJSON(w, r, statusCode, errMsg, nil)
|
h.ResultErrorJSON(w, r, statusCode, errMsg, nil)
|
||||||
return
|
return
|
||||||
@@ -47,9 +47,9 @@ func ListQuery(obj interface{}) func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listQuerySort(
|
func listQuerySort(
|
||||||
|
ctx context.Context,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
filterMap map[string]model.FilterMapValue,
|
filterMap map[string]model.FilterMapValue,
|
||||||
ctx context.Context,
|
|
||||||
) (context.Context, int, string) {
|
) (context.Context, int, string) {
|
||||||
var sortFields []model.Sort
|
var sortFields []model.Sort
|
||||||
|
|
||||||
@@ -99,10 +99,10 @@ func listQuerySort(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listQueryFilters(
|
func listQueryFilters(
|
||||||
r *http.Request,
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
r *http.Request,
|
||||||
schemaData string,
|
schemaData string,
|
||||||
) (context.Context, int, string, interface{}) {
|
) (context.Context, int, string, any) {
|
||||||
reservedFilterKeys := []string{
|
reservedFilterKeys := []string{
|
||||||
"limit",
|
"limit",
|
||||||
"offset",
|
"offset",
|
||||||
|
@@ -53,7 +53,7 @@ func TestListQuery(t *testing.T) {
|
|||||||
ctx = context.WithValue(ctx, c.FiltersCtxKey, tags.GetFilterSchema(testObj))
|
ctx = context.WithValue(ctx, c.FiltersCtxKey, tags.GetFilterSchema(testObj))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler := middleware.ListQuery(testObj)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := middleware.ListQuery(testObj)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
16
backend/internal/api/middleware/log.go
Normal file
16
backend/internal/api/middleware/log.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"npm/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log will print out route information to the logger
|
||||||
|
// only when debug is enabled
|
||||||
|
func Log(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger.Debug("Request: %s %s", r.Method, r.URL.Path)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
@@ -33,7 +33,6 @@ func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte)
|
|||||||
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
|
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Get content from context
|
// Get content from context
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
|
|
||||||
|
@@ -26,14 +26,14 @@ func SSEAuth(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims != nil {
|
if claims == nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := uint(claims["uid"].(float64))
|
userID := uint(claims["uid"].(float64))
|
||||||
_, enabled, _ := user.IsEnabled(userID)
|
_, enabled, _ := user.IsEnabled(userID)
|
||||||
if token == nil || !enabled {
|
if !enabled {
|
||||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@ import (
|
|||||||
// NewRouter returns a new router object
|
// NewRouter returns a new router object
|
||||||
func NewRouter() http.Handler {
|
func NewRouter() http.Handler {
|
||||||
// Cors
|
// Cors
|
||||||
cors := cors.New(cors.Options{
|
corss := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
|
||||||
@@ -42,7 +42,7 @@ func NewRouter() http.Handler {
|
|||||||
middleware.AccessControl,
|
middleware.AccessControl,
|
||||||
middleware.Cors(r),
|
middleware.Cors(r),
|
||||||
middleware.Options(r),
|
middleware.Options(r),
|
||||||
cors.Handler,
|
corss.Handler,
|
||||||
chiMiddleware.RealIP,
|
chiMiddleware.RealIP,
|
||||||
chiMiddleware.Recoverer,
|
chiMiddleware.Recoverer,
|
||||||
chiMiddleware.Throttle(5),
|
chiMiddleware.Throttle(5),
|
||||||
@@ -50,6 +50,7 @@ func NewRouter() http.Handler {
|
|||||||
middleware.Expansion,
|
middleware.Expansion,
|
||||||
middleware.DecodeAuth(),
|
middleware.DecodeAuth(),
|
||||||
middleware.BodyContext(),
|
middleware.BodyContext(),
|
||||||
|
middleware.Log,
|
||||||
)
|
)
|
||||||
|
|
||||||
return applyRoutes(r)
|
return applyRoutes(r)
|
||||||
@@ -61,51 +62,59 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
r.NotFound(handler.NotFound())
|
r.NotFound(handler.NotFound())
|
||||||
r.MethodNotAllowed(handler.NotAllowed())
|
r.MethodNotAllowed(handler.NotAllowed())
|
||||||
|
|
||||||
|
// OAuth endpoints aren't technically API endpoints
|
||||||
|
r.With(middleware.EnforceSetup()).Route("/oauth", func(r chi.Router) {
|
||||||
|
r.Get("/login", handler.OAuthLogin())
|
||||||
|
r.Get("/redirect", handler.OAuthRedirect())
|
||||||
|
})
|
||||||
|
|
||||||
// SSE - requires a sse token as the `jwt` get parameter
|
// SSE - requires a sse token as the `jwt` get parameter
|
||||||
// Exists inside /api but it's here so that we can skip the Timeout middleware
|
// Exists inside /api but it's here so that we can skip the Timeout middleware
|
||||||
// that applies to other endpoints.
|
// that applies to other endpoints.
|
||||||
r.With(middleware.EnforceSetup(true), middleware.SSEAuth).
|
r.With(middleware.EnforceSetup(), middleware.SSEAuth).
|
||||||
Mount("/api/sse", serverevents.Get())
|
Mount("/api/sse", serverevents.Get())
|
||||||
|
|
||||||
// API
|
// API
|
||||||
r.With(chiMiddleware.Timeout(30*time.Second)).Route("/api", func(r chi.Router) {
|
r.With(chiMiddleware.Timeout(30*time.Second)).Route("/api", func(r chi.Router) {
|
||||||
r.Get("/", handler.Health())
|
r.Get("/", handler.Health())
|
||||||
r.Get("/schema", handler.Schema())
|
r.Get("/schema", handler.Schema())
|
||||||
r.With(middleware.EnforceSetup(true), middleware.Enforce("")).
|
r.With(middleware.EnforceSetup(), middleware.Enforce()).
|
||||||
Get("/config", handler.Config())
|
Get("/config", handler.Config())
|
||||||
|
|
||||||
// Tokens
|
// Auth
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/tokens", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/auth", func(r chi.Router) {
|
||||||
|
r.Get("/", handler.GetAuthConfig())
|
||||||
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
|
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
|
||||||
Post("/", handler.NewToken())
|
Post("/", handler.NewToken())
|
||||||
r.With(middleware.Enforce("")).
|
r.With(middleware.Enforce()).
|
||||||
Get("/", handler.RefreshToken())
|
Post("/refresh", handler.RefreshToken())
|
||||||
r.With(middleware.Enforce("")).
|
r.With(middleware.Enforce()).
|
||||||
Post("/sse", handler.NewSSEToken())
|
Post("/sse", handler.NewSSEToken())
|
||||||
})
|
})
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
r.Route("/users", func(r chi.Router) {
|
r.Route("/users", func(r chi.Router) {
|
||||||
// Create - can be done in Setup stage as well
|
// Create - can be done in Setup stage as well
|
||||||
r.With(middleware.Enforce(user.CapabilityUsersManage), middleware.EnforceRequestSchema(schema.CreateUser())).
|
r.With(
|
||||||
Post("/", handler.CreateUser())
|
middleware.Enforce(user.CapabilityUsersManage),
|
||||||
|
middleware.EnforceRequestSchema(schema.CreateUser()),
|
||||||
|
).Post("/", handler.CreateUser())
|
||||||
|
|
||||||
// Requires Setup stage to be completed
|
// Requires Setup stage to be completed
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/", func(r chi.Router) {
|
||||||
// Get yourself, requires a login but no other permissions
|
// Get yourself, requires a login but no other permissions
|
||||||
r.With(middleware.Enforce("")).
|
r.With(middleware.Enforce()).
|
||||||
Get("/{userID:me}", handler.GetUser())
|
Get("/{userID:me}", handler.GetUser())
|
||||||
|
|
||||||
// Update yourself, requires a login but no other permissions
|
// Update yourself, requires a login but no other permissions
|
||||||
r.With(middleware.Enforce(""), middleware.EnforceRequestSchema(schema.UpdateUser())).
|
r.With(
|
||||||
Put("/{userID:me}", handler.UpdateUser())
|
middleware.Enforce(),
|
||||||
|
middleware.EnforceRequestSchema(schema.UpdateUser()),
|
||||||
|
).Put("/{userID:me}", handler.UpdateUser())
|
||||||
|
|
||||||
r.With(middleware.Enforce(user.CapabilityUsersManage)).Route("/", func(r chi.Router) {
|
r.With(middleware.Enforce(user.CapabilityUsersManage)).Route("/", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(middleware.ListQuery(user.Model{})).Get("/", handler.GetUsers())
|
||||||
middleware.Enforce(user.CapabilityUsersManage),
|
|
||||||
middleware.ListQuery(user.Model{}),
|
|
||||||
).Get("/", handler.GetUsers())
|
|
||||||
|
|
||||||
// Specific Item
|
// Specific Item
|
||||||
r.Get("/{userID:[0-9]+}", handler.GetUser())
|
r.Get("/{userID:[0-9]+}", handler.GetUser())
|
||||||
@@ -117,10 +126,14 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Auth - sets passwords
|
// Auth - sets passwords
|
||||||
r.With(middleware.Enforce(""), middleware.EnforceRequestSchema(schema.SetAuth())).
|
r.With(
|
||||||
Post("/{userID:me}/auth", handler.SetAuth())
|
middleware.Enforce(),
|
||||||
r.With(middleware.Enforce(user.CapabilityUsersManage), middleware.EnforceRequestSchema(schema.SetAuth())).
|
middleware.EnforceRequestSchema(schema.SetAuth()),
|
||||||
Post("/{userID:[0-9]+}/auth", handler.SetAuth())
|
).Post("/{userID:me}/auth", handler.SetAuth())
|
||||||
|
r.With(
|
||||||
|
middleware.Enforce(user.CapabilityUsersManage),
|
||||||
|
middleware.EnforceRequestSchema(schema.SetAuth()),
|
||||||
|
).Post("/{userID:[0-9]+}/auth", handler.SetAuth())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,7 +146,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilitySettingsManage)).Route("/settings", func(r chi.Router) {
|
r.With(middleware.EnforceSetup(), middleware.Enforce(user.CapabilitySettingsManage)).Route("/settings", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.ListQuery(setting.Model{}),
|
middleware.ListQuery(setting.Model{}),
|
||||||
@@ -147,7 +160,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Access Lists
|
// Access Lists
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/access-lists", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/access-lists", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityAccessListsView),
|
middleware.Enforce(user.CapabilityAccessListsView),
|
||||||
@@ -171,7 +184,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// DNS Providers
|
// DNS Providers
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/dns-providers", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/dns-providers", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityDNSProvidersView),
|
middleware.Enforce(user.CapabilityDNSProvidersView),
|
||||||
@@ -201,7 +214,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Certificate Authorities
|
// Certificate Authorities
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/certificate-authorities", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/certificate-authorities", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityCertificateAuthoritiesView),
|
middleware.Enforce(user.CapabilityCertificateAuthoritiesView),
|
||||||
@@ -231,7 +244,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Certificates
|
// Certificates
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/certificates", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/certificates", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityCertificatesView),
|
middleware.Enforce(user.CapabilityCertificatesView),
|
||||||
@@ -258,7 +271,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Hosts
|
// Hosts
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/hosts", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/hosts", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityHostsView),
|
middleware.Enforce(user.CapabilityHostsView),
|
||||||
@@ -284,7 +297,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Nginx Templates
|
// Nginx Templates
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/nginx-templates", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/nginx-templates", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityNginxTemplatesView),
|
middleware.Enforce(user.CapabilityNginxTemplatesView),
|
||||||
@@ -308,7 +321,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Streams
|
// Streams
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/streams", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/streams", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityStreamsView),
|
middleware.Enforce(user.CapabilityStreamsView),
|
||||||
@@ -332,7 +345,7 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Upstreams
|
// Upstreams
|
||||||
r.With(middleware.EnforceSetup(true)).Route("/upstreams", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/upstreams", func(r chi.Router) {
|
||||||
// List
|
// List
|
||||||
r.With(
|
r.With(
|
||||||
middleware.Enforce(user.CapabilityHostsView),
|
middleware.Enforce(user.CapabilityHostsView),
|
||||||
|
@@ -64,6 +64,9 @@ const anyType = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ func CreateDNSProvider() string {
|
|||||||
|
|
||||||
allSchemasWrapped := make([]string, 0)
|
allSchemasWrapped := make([]string, 0)
|
||||||
for providerName, provider := range allProviders {
|
for providerName, provider := range allProviders {
|
||||||
schema, err := provider.GetJsonSchema()
|
schema, err := provider.GetJSONSchema()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("ProviderSchemaError", eris.Wrapf(err, "Invalid Provider Schema for %s: %v", provider.Title, err))
|
logger.Error("ProviderSchemaError", eris.Wrapf(err, "Invalid Provider Schema for %s: %v", provider.Title, err))
|
||||||
} else {
|
} else {
|
||||||
|
@@ -16,7 +16,6 @@ func CreateUser() string {
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": %s,
|
"name": %s,
|
||||||
"nickname": %s,
|
|
||||||
"email": %s,
|
"email": %s,
|
||||||
"is_disabled": {
|
"is_disabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -30,7 +29,7 @@ func CreateUser() string {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"pattern": "^local$"
|
||||||
},
|
},
|
||||||
"secret": %s
|
"secret": %s
|
||||||
}
|
}
|
||||||
@@ -38,5 +37,5 @@ func CreateUser() string {
|
|||||||
"capabilities": %s
|
"capabilities": %s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), stringMinMax(8, 255), capabilties())
|
`, stringMinMax(2, 50), stringMinMax(5, 150), stringMinMax(8, 255), capabilties())
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ func GetToken() string {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"enum": ["local", "ldap"]
|
||||||
},
|
},
|
||||||
"identity": %s,
|
"identity": %s,
|
||||||
"secret": %s
|
"secret": %s
|
||||||
|
133
backend/internal/api/schema/schema_test.go
Normal file
133
backend/internal/api/schema/schema_test.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"npm/internal/entity/certificate"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSchemas(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
schema string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "CreateCertificate",
|
||||||
|
schema: CreateCertificate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateCertificate TypeHTTP",
|
||||||
|
schema: UpdateCertificate(certificate.TypeHTTP),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateCertificate TypeDNS",
|
||||||
|
schema: UpdateCertificate(certificate.TypeDNS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateCertificate TypeCustom",
|
||||||
|
schema: UpdateCertificate(certificate.TypeCustom),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateCertificate TypeMkcert",
|
||||||
|
schema: UpdateCertificate(certificate.TypeMkcert),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateCertificate default",
|
||||||
|
schema: UpdateCertificate(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateAccessList",
|
||||||
|
schema: CreateAccessList(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateCertificateAuthority",
|
||||||
|
schema: CreateCertificateAuthority(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateDNSProvider",
|
||||||
|
schema: CreateDNSProvider(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateHost",
|
||||||
|
schema: CreateHost(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateNginxTemplate",
|
||||||
|
schema: CreateNginxTemplate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateSetting",
|
||||||
|
schema: CreateSetting(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateStream",
|
||||||
|
schema: CreateStream(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateUpstream",
|
||||||
|
schema: CreateUpstream(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CreateUser",
|
||||||
|
schema: CreateUser(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GetToken",
|
||||||
|
schema: GetToken(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SetAuth",
|
||||||
|
schema: SetAuth(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateAccessList",
|
||||||
|
schema: UpdateAccessList(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateCertificateAuthority",
|
||||||
|
schema: UpdateCertificateAuthority(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateDNSProvider",
|
||||||
|
schema: UpdateDNSProvider(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateHost",
|
||||||
|
schema: UpdateHost(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateNginxTemplate",
|
||||||
|
schema: UpdateNginxTemplate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateSetting",
|
||||||
|
schema: UpdateSetting(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateStream",
|
||||||
|
schema: UpdateStream(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateUpstream",
|
||||||
|
schema: UpdateUpstream(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UpdateUser",
|
||||||
|
schema: UpdateUser(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
byt := []byte(tt.schema)
|
||||||
|
var prettyJSON bytes.Buffer
|
||||||
|
err := json.Indent(&prettyJSON, byt, "", " ")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, len(prettyJSON.String()), 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,7 @@ package schema
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// SetAuth is the schema for incoming data validation
|
// SetAuth is the schema for incoming data validation
|
||||||
|
// Only local auth is supported for setting a password
|
||||||
func SetAuth() string {
|
func SetAuth() string {
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
@@ -15,7 +16,7 @@ func SetAuth() string {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"pattern": "^local$"
|
||||||
},
|
},
|
||||||
"secret": %s,
|
"secret": %s,
|
||||||
"current_secret": %s
|
"current_secret": %s
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
// UpdateHostTemplate is the schema for incoming data validation
|
// UpdateNginxTemplate is the schema for incoming data validation
|
||||||
func UpdateNginxTemplate() string {
|
func UpdateNginxTemplate() string {
|
||||||
return `
|
return `
|
||||||
{
|
{
|
||||||
|
@@ -11,7 +11,6 @@ func UpdateUser() string {
|
|||||||
"minProperties": 1,
|
"minProperties": 1,
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": %s,
|
"name": %s,
|
||||||
"nickname": %s,
|
|
||||||
"email": %s,
|
"email": %s,
|
||||||
"is_disabled": {
|
"is_disabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -19,5 +18,5 @@ func UpdateUser() string {
|
|||||||
"capabilities": %s
|
"capabilities": %s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), capabilties())
|
`, stringMinMax(2, 50), stringMinMax(5, 150), capabilties())
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,7 @@ func InitArgs(version, commit *string) {
|
|||||||
|
|
||||||
if appArguments.Version {
|
if appArguments.Version {
|
||||||
fmt.Printf("v%s (%s)\n", *version, *commit)
|
fmt.Printf("v%s (%s)\n", *version, *commit)
|
||||||
|
// nolint: revive
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ func CreateDataFolders() {
|
|||||||
path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder)
|
path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder)
|
||||||
}
|
}
|
||||||
logger.Debug("Creating folder: %s", path)
|
logger.Debug("Creating folder: %s", path)
|
||||||
|
// nolint: gosec
|
||||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||||
logger.Error("CreateDataFolderError", err)
|
logger.Error("CreateDataFolderError", err)
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -46,8 +46,8 @@ func SetDB(db *gorm.DB) {
|
|||||||
func connect() (*gorm.DB, error) {
|
func connect() (*gorm.DB, error) {
|
||||||
var d gorm.Dialector
|
var d gorm.Dialector
|
||||||
dsn := config.Configuration.DB.GetGormConnectURL()
|
dsn := config.Configuration.DB.GetGormConnectURL()
|
||||||
switch strings.ToLower(config.Configuration.DB.Driver) {
|
|
||||||
|
|
||||||
|
switch strings.ToLower(config.Configuration.DB.Driver) {
|
||||||
case config.DatabaseSqlite:
|
case config.DatabaseSqlite:
|
||||||
// autocreate(dsn)
|
// autocreate(dsn)
|
||||||
d = sqlite.Open(dsn)
|
d = sqlite.Open(dsn)
|
||||||
|
@@ -2,8 +2,9 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"npm/internal/config"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"npm/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -18,11 +19,12 @@ const (
|
|||||||
// is for special cases where we run raw sql
|
// is for special cases where we run raw sql
|
||||||
func QuoteTableName(tbl string) string {
|
func QuoteTableName(tbl string) string {
|
||||||
switch strings.ToLower(config.Configuration.DB.Driver) {
|
switch strings.ToLower(config.Configuration.DB.Driver) {
|
||||||
case config.DatabasePostgres:
|
case config.DatabaseMysql:
|
||||||
return fmt.Sprintf(`"%s"`, tbl)
|
// backticks for mysql
|
||||||
default:
|
|
||||||
// This is the same for Mysql and Sqlite
|
|
||||||
return fmt.Sprintf("`%s`", tbl)
|
return fmt.Sprintf("`%s`", tbl)
|
||||||
|
default:
|
||||||
|
// double quotes for everything else
|
||||||
|
return fmt.Sprintf(`"%s"`, tbl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,6 +9,8 @@ import (
|
|||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
|
|
||||||
"github.com/amacneil/dbmate/v2/pkg/dbmate"
|
"github.com/amacneil/dbmate/v2/pkg/dbmate"
|
||||||
|
|
||||||
|
// Drivers:
|
||||||
_ "github.com/amacneil/dbmate/v2/pkg/driver/mysql"
|
_ "github.com/amacneil/dbmate/v2/pkg/driver/mysql"
|
||||||
_ "github.com/amacneil/dbmate/v2/pkg/driver/postgres"
|
_ "github.com/amacneil/dbmate/v2/pkg/driver/postgres"
|
||||||
_ "github.com/amacneil/dbmate/v2/pkg/driver/sqlite"
|
_ "github.com/amacneil/dbmate/v2/pkg/driver/sqlite"
|
||||||
|
@@ -2,6 +2,7 @@ package dnsproviders
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"npm/internal/errors"
|
"npm/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,8 +32,8 @@ type Provider struct {
|
|||||||
Properties map[string]providerField `json:"properties"`
|
Properties map[string]providerField `json:"properties"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJsonSchema encodes this object as JSON string
|
// GetJSONSchema encodes this object as JSON string
|
||||||
func (p *Provider) GetJsonSchema() (string, error) {
|
func (p *Provider) GetJSONSchema() (string, error) {
|
||||||
b, err := json.Marshal(p)
|
b, err := json.Marshal(p)
|
||||||
return string(b), err
|
return string(b), err
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
package dnsproviders
|
package dnsproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"npm/internal/util"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"npm/internal/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.uber.org/goleak"
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
@@ -13,7 +14,7 @@ func TestAcmeDNSProvider(t *testing.T) {
|
|||||||
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
provider := getDNSAcmeDNS()
|
provider := getDNSAcmeDNS()
|
||||||
json, err := provider.GetJsonSchema()
|
json, err := provider.GetJSONSchema()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, `{
|
assert.Equal(t, `{
|
||||||
"title": "dns_acmedns",
|
"title": "dns_acmedns",
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
package dnsproviders
|
package dnsproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"npm/internal/util"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"npm/internal/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.uber.org/goleak"
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
@@ -14,7 +15,7 @@ func TestAdProvider(t *testing.T) {
|
|||||||
|
|
||||||
provider := getDNSAd()
|
provider := getDNSAd()
|
||||||
provider.ConvertToUpdatable()
|
provider.ConvertToUpdatable()
|
||||||
json, err := provider.GetJsonSchema()
|
json, err := provider.GetJSONSchema()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, `{
|
assert.Equal(t, `{
|
||||||
"title": "dns_ad",
|
"title": "dns_ad",
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
package dnsproviders
|
package dnsproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"npm/internal/util"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"npm/internal/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.uber.org/goleak"
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
@@ -13,7 +14,7 @@ func TestAliProvider(t *testing.T) {
|
|||||||
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
provider := getDNSAli()
|
provider := getDNSAli()
|
||||||
json, err := provider.GetJsonSchema()
|
json, err := provider.GetJSONSchema()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, `{
|
assert.Equal(t, `{
|
||||||
"title": "dns_ali",
|
"title": "dns_ali",
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
Meta types.JSONB `json:"meta" gorm:"column:meta"`
|
Meta types.JSONB `json:"meta" gorm:"column:meta"`
|
||||||
|
@@ -40,7 +40,7 @@ func (s *testsuite) SetupTest() {
|
|||||||
}).AddRow(
|
}).AddRow(
|
||||||
10,
|
10,
|
||||||
100,
|
100,
|
||||||
TypePassword,
|
TypeLocal,
|
||||||
"abc123",
|
"abc123",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ func TestExampleTestSuite(t *testing.T) {
|
|||||||
func assertModel(t *testing.T, m Model) {
|
func assertModel(t *testing.T, m Model) {
|
||||||
assert.Equal(t, uint(10), m.ID)
|
assert.Equal(t, uint(10), m.ID)
|
||||||
assert.Equal(t, uint(100), m.UserID)
|
assert.Equal(t, uint(100), m.UserID)
|
||||||
assert.Equal(t, TypePassword, m.Type)
|
assert.Equal(t, TypeLocal, m.Type)
|
||||||
assert.Equal(t, "abc123", m.Secret)
|
assert.Equal(t, "abc123", m.Secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +83,25 @@ func (s *testsuite) TestGetByUserIDType() {
|
|||||||
|
|
||||||
s.mock.
|
s.mock.
|
||||||
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)).
|
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)).
|
||||||
WithArgs(100, TypePassword, 0, 1).
|
WithArgs(100, TypeLocal, 0, 1).
|
||||||
WillReturnRows(s.singleRow)
|
WillReturnRows(s.singleRow)
|
||||||
|
|
||||||
m, err := GetByUserIDType(100, TypePassword)
|
m, err := GetByUserIDType(100, TypeLocal)
|
||||||
|
require.NoError(s.T(), err)
|
||||||
|
require.NoError(s.T(), s.mock.ExpectationsWereMet())
|
||||||
|
assertModel(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testsuite) TestGetByIdenityType() {
|
||||||
|
// goleak is used to detect goroutine leaks
|
||||||
|
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
|
s.mock.
|
||||||
|
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE identity = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)).
|
||||||
|
WithArgs("johndoe", TypeLocal, 0, 1).
|
||||||
|
WillReturnRows(s.singleRow)
|
||||||
|
|
||||||
|
m, err := GetByIdenityType("johndoe", TypeLocal)
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
require.NoError(s.T(), s.mock.ExpectationsWereMet())
|
require.NoError(s.T(), s.mock.ExpectationsWereMet())
|
||||||
assertModel(s.T(), m)
|
assertModel(s.T(), m)
|
||||||
@@ -97,13 +112,14 @@ func (s *testsuite) TestSave() {
|
|||||||
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
s.mock.ExpectBegin()
|
s.mock.ExpectBegin()
|
||||||
s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "auth" ("created_at","updated_at","is_deleted","user_id","type","secret") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id"`)).
|
s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "auth" ("created_at","updated_at","is_deleted","user_id","type","identity","secret") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`)).
|
||||||
WithArgs(
|
WithArgs(
|
||||||
sqlmock.AnyArg(),
|
sqlmock.AnyArg(),
|
||||||
sqlmock.AnyArg(),
|
sqlmock.AnyArg(),
|
||||||
0,
|
0,
|
||||||
100,
|
100,
|
||||||
TypePassword,
|
TypeLocal,
|
||||||
|
"",
|
||||||
"abc123",
|
"abc123",
|
||||||
).
|
).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11"))
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11"))
|
||||||
@@ -112,7 +128,7 @@ func (s *testsuite) TestSave() {
|
|||||||
// New model
|
// New model
|
||||||
m := Model{
|
m := Model{
|
||||||
UserID: 100,
|
UserID: 100,
|
||||||
Type: TypePassword,
|
Type: TypeLocal,
|
||||||
Secret: "abc123",
|
Secret: "abc123",
|
||||||
}
|
}
|
||||||
err := m.Save()
|
err := m.Save()
|
||||||
@@ -123,13 +139,11 @@ func (s *testsuite) TestSave() {
|
|||||||
func (s *testsuite) TestSetPassword() {
|
func (s *testsuite) TestSetPassword() {
|
||||||
// goleak is used to detect goroutine leaks
|
// goleak is used to detect goroutine leaks
|
||||||
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
m := Model{UserID: 100}
|
m := Model{UserID: 100}
|
||||||
err := m.SetPassword("abc123")
|
err := m.SetPassword("abc123")
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
assert.Equal(s.T(), TypePassword, m.Type)
|
assert.Equal(s.T(), TypeLocal, m.Type)
|
||||||
assert.Greater(s.T(), len(m.Secret), 15)
|
assert.Greater(s.T(), len(m.Secret), 15)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testsuite) TestValidateSecret() {
|
func (s *testsuite) TestValidateSecret() {
|
||||||
@@ -143,10 +157,10 @@ func (s *testsuite) TestValidateSecret() {
|
|||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
err = m.ValidateSecret("this is not the password")
|
err = m.ValidateSecret("this is not the password")
|
||||||
assert.NotNil(s.T(), err)
|
assert.NotNil(s.T(), err)
|
||||||
assert.Equal(s.T(), "Invalid Password", err.Error())
|
assert.Equal(s.T(), "Invalid Credentials", err.Error())
|
||||||
|
|
||||||
m.Type = "not a valid type"
|
m.Type = "not a valid type"
|
||||||
err = m.ValidateSecret("abc123")
|
err = m.ValidateSecret("abc123")
|
||||||
assert.NotNil(s.T(), err)
|
assert.NotNil(s.T(), err)
|
||||||
assert.Equal(s.T(), "Could not validate Secret, auth type is not a Password", err.Error())
|
assert.Equal(s.T(), "Could not validate Secret, auth type is not Local", err.Error())
|
||||||
}
|
}
|
||||||
|
96
backend/internal/entity/auth/ldap.go
Normal file
96
backend/internal/entity/auth/ldap.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"npm/internal/entity/setting"
|
||||||
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
ldap3 "github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/rotisserie/eris"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LDAPUser is the LDAP User
|
||||||
|
type LDAPUser struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAPAuthenticate will use ldap to authenticate with user/pass
|
||||||
|
func LDAPAuthenticate(identity, password string) (*LDAPUser, error) {
|
||||||
|
ldapSettings, err := setting.GetLDAPSettings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dn := strings.Replace(ldapSettings.UserDN, "{{USERNAME}}", identity, 1)
|
||||||
|
conn, err := ldapConnect(ldapSettings.Host, dn, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// nolint: errcheck, gosec
|
||||||
|
defer conn.Close()
|
||||||
|
return ldapSearchUser(conn, ldapSettings, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt ldap connection
|
||||||
|
func ldapConnect(host, dn, password string) (*ldap3.Conn, error) {
|
||||||
|
var conn *ldap3.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if conn, err = ldap3.DialURL(fmt.Sprintf("ldap://%s", host)); err != nil {
|
||||||
|
logger.Error("LdapError", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LDAP Logging in with: %s", dn)
|
||||||
|
if err := conn.Bind(dn, password); err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "Invalid Credentials") {
|
||||||
|
logger.Error("LDAPAuthError", err)
|
||||||
|
}
|
||||||
|
// nolint: gosec, errcheck
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LDAP Login Successful")
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ldapSearchUser(l *ldap3.Conn, ldapSettings setting.LDAPSettings, username string) (*LDAPUser, error) {
|
||||||
|
// Search for the given username
|
||||||
|
searchRequest := ldap3.NewSearchRequest(
|
||||||
|
ldapSettings.BaseDN,
|
||||||
|
ldap3.ScopeWholeSubtree,
|
||||||
|
ldap3.NeverDerefAliases,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
strings.Replace(ldapSettings.SelfFilter, "{{USERNAME}}", username, 1),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
sr, err := l.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("LdapError", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sr.Entries) < 1 {
|
||||||
|
return nil, eris.New("No user found in LDAP search")
|
||||||
|
} else if len(sr.Entries) > 1 {
|
||||||
|
j, _ := json.Marshal(sr)
|
||||||
|
logger.Debug("LDAP Search Results: %s", j)
|
||||||
|
return nil, eris.Errorf("Too many LDAP results returned in LDAP search: %d", len(sr.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LDAPUser{
|
||||||
|
Username: strings.ToLower(username),
|
||||||
|
Name: sr.Entries[0].GetAttributeValue(ldapSettings.NameProperty),
|
||||||
|
Email: strings.ToLower(sr.Entries[0].GetAttributeValue(ldapSettings.EmailProperty)),
|
||||||
|
}, nil
|
||||||
|
}
|
@@ -11,7 +11,7 @@ func GetByID(id int) (Model, error) {
|
|||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByUserIDType finds a user by email
|
// GetByUserIDType finds a user by id and type
|
||||||
func GetByUserIDType(userID uint, authType string) (Model, error) {
|
func GetByUserIDType(userID uint, authType string) (Model, error) {
|
||||||
var auth Model
|
var auth Model
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
@@ -21,3 +21,14 @@ func GetByUserIDType(userID uint, authType string) (Model, error) {
|
|||||||
First(&auth)
|
First(&auth)
|
||||||
return auth, result.Error
|
return auth, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByIdenityType finds a user by identity and type
|
||||||
|
func GetByIdenityType(identity string, authType string) (Model, error) {
|
||||||
|
var auth Model
|
||||||
|
db := database.GetDB()
|
||||||
|
result := db.
|
||||||
|
Where("identity = ?", identity).
|
||||||
|
Where("type = ?", authType).
|
||||||
|
First(&auth)
|
||||||
|
return auth, result.Error
|
||||||
|
}
|
||||||
|
@@ -8,17 +8,20 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Auth types
|
||||||
const (
|
const (
|
||||||
// TypePassword is the Password Type
|
TypeLocal = "local"
|
||||||
TypePassword = "password"
|
TypeLDAP = "ldap"
|
||||||
|
TypeOAuth = "oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id"`
|
UserID uint `json:"user_id" gorm:"column:user_id"`
|
||||||
Type string `json:"type" gorm:"column:type;default:password"`
|
Type string `json:"type" gorm:"column:type;default:local"`
|
||||||
Secret string `json:"secret,omitempty" gorm:"column:secret"`
|
Identity string `json:"identity,omitempty" gorm:"column:identity"`
|
||||||
|
Secret string `json:"secret,omitempty" gorm:"column:secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName overrides the table name used by gorm
|
// TableName overrides the table name used by gorm
|
||||||
@@ -36,7 +39,6 @@ func (m *Model) LoadByID(id int) error {
|
|||||||
// Save will save this model to the DB
|
// Save will save this model to the DB
|
||||||
func (m *Model) Save() error {
|
func (m *Model) Save() error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
// todo: touch? not sure that save does this or not?
|
|
||||||
result := db.Save(m)
|
result := db.Save(m)
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
@@ -48,7 +50,7 @@ func (m *Model) SetPassword(password string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Type = TypePassword
|
m.Type = TypeLocal
|
||||||
m.Secret = string(hash)
|
m.Secret = string(hash)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -56,13 +58,13 @@ func (m *Model) SetPassword(password string) error {
|
|||||||
|
|
||||||
// ValidateSecret will check if a given secret matches the encrypted secret
|
// ValidateSecret will check if a given secret matches the encrypted secret
|
||||||
func (m *Model) ValidateSecret(secret string) error {
|
func (m *Model) ValidateSecret(secret string) error {
|
||||||
if m.Type != TypePassword {
|
if m.Type != TypeLocal {
|
||||||
return eris.New("Could not validate Secret, auth type is not a Password")
|
return eris.New("Could not validate Secret, auth type is not Local")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
|
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return eris.New("Invalid Password")
|
return eris.New("Invalid Credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
218
backend/internal/entity/auth/oauth.go
Normal file
218
backend/internal/entity/auth/oauth.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"npm/internal/entity/setting"
|
||||||
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
cache "github.com/patrickmn/go-cache"
|
||||||
|
"github.com/rotisserie/eris"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints
|
||||||
|
var (
|
||||||
|
OAuthCache *cache.Cache
|
||||||
|
settingGetOAuthSettings = setting.GetOAuthSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthCacheInit will create a new Memory Cache
|
||||||
|
func OAuthCacheInit() {
|
||||||
|
if OAuthCache == nil {
|
||||||
|
logger.Debug("Creating a new OAuthCache")
|
||||||
|
OAuthCache = cache.New(5*time.Minute, 5*time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthUser is the OAuth User
|
||||||
|
type OAuthUser struct {
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Resource map[string]any `json:"resource"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceField will attempt to get a field from the resource
|
||||||
|
func (m *OAuthUser) GetResourceField(field string) string {
|
||||||
|
if m.Resource != nil {
|
||||||
|
if value, ok := m.Resource[field]; ok {
|
||||||
|
return value.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID attempts to get an ID from the resource
|
||||||
|
func (m *OAuthUser) GetID() string {
|
||||||
|
if m.Identifier != "" {
|
||||||
|
return m.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []string{
|
||||||
|
"uid",
|
||||||
|
"user_id",
|
||||||
|
"username",
|
||||||
|
"preferred_username",
|
||||||
|
"email",
|
||||||
|
"mail",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
if val := m.GetResourceField(field); val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName attempts to get a name from the resource
|
||||||
|
// using different fields
|
||||||
|
func (m *OAuthUser) GetName() string {
|
||||||
|
fields := []string{
|
||||||
|
"nickname",
|
||||||
|
"given_name",
|
||||||
|
"name",
|
||||||
|
"preferred_username",
|
||||||
|
"username",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
if name := m.GetResourceField(field); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback:
|
||||||
|
return m.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmail will return an email address even if it can't be known in the
|
||||||
|
// Resource
|
||||||
|
func (m *OAuthUser) GetEmail() string {
|
||||||
|
// See if there's an email field first
|
||||||
|
if email := m.GetResourceField("email"); email != "" {
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the identifier if it looks like an email
|
||||||
|
if m.Identifier != "" {
|
||||||
|
if strings.Contains(m.Identifier, "@") {
|
||||||
|
return m.Identifier
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s@oauth", m.Identifier)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOAuth2Config() (*oauth2.Config, *setting.OAuthSettings, error) {
|
||||||
|
oauthSettings, err := settingGetOAuthSettings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauthSettings.ClientID == "" || oauthSettings.ClientSecret == "" || oauthSettings.AuthURL == "" || oauthSettings.TokenURL == "" {
|
||||||
|
return nil, nil, eris.New("oauth-settings-incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oauth2.Config{
|
||||||
|
ClientID: oauthSettings.ClientID,
|
||||||
|
ClientSecret: oauthSettings.ClientSecret,
|
||||||
|
Scopes: oauthSettings.Scopes,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: oauthSettings.AuthURL,
|
||||||
|
TokenURL: oauthSettings.TokenURL,
|
||||||
|
},
|
||||||
|
}, &oauthSettings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthLogin is hit by the client to generate a URL to redirect to
|
||||||
|
// and start the oauth process
|
||||||
|
func OAuthLogin(redirectBase, ipAddress string) (string, error) {
|
||||||
|
OAuthCacheInit()
|
||||||
|
|
||||||
|
conf, _, err := getOAuth2Config()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := oauth2.GenerateVerifier()
|
||||||
|
OAuthCache.Set(getCacheKey(ipAddress), verifier, cache.DefaultExpiration)
|
||||||
|
|
||||||
|
// todo: state should be unique to the incoming IP address of the requester, I guess
|
||||||
|
url := conf.AuthCodeURL("state", oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
|
||||||
|
|
||||||
|
if redirectBase != "" {
|
||||||
|
url = url + "&redirect_uri=" + redirectBase + "/oauth/redirect"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("URL: %s", url)
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthReturn ...
|
||||||
|
func OAuthReturn(ctx context.Context, code, ipAddress string) (*OAuthUser, error) {
|
||||||
|
// Just in case...
|
||||||
|
OAuthCacheInit()
|
||||||
|
|
||||||
|
conf, oauthSettings, err := getOAuth2Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier, found := OAuthCache.Get(getCacheKey(ipAddress))
|
||||||
|
if !found {
|
||||||
|
return nil, eris.New("oauth-verifier-not-found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the authorization code that is pushed to the redirect
|
||||||
|
// URL. Exchange will do the handshake to retrieve the
|
||||||
|
// initial access token. The HTTP Client returned by
|
||||||
|
// conf.Client will refresh the token as necessary.
|
||||||
|
tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier.(string)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this stage, the token is the JWT as given by the oauth server.
|
||||||
|
// we need to use that to get more info about this user,
|
||||||
|
// and then we'll create our own jwt for use later.
|
||||||
|
|
||||||
|
client := conf.Client(ctx, tok)
|
||||||
|
resp, err := client.Get(oauthSettings.ResourceURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: errcheck, gosec
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ou := OAuthUser{
|
||||||
|
Token: tok.AccessToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal the body into a interface
|
||||||
|
if err := json.Unmarshal(body, &ou.Resource); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get the identifier from the resource
|
||||||
|
if oauthSettings.Identifier != "" {
|
||||||
|
ou.Identifier = ou.GetResourceField(oauthSettings.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ou, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCacheKey(ipAddress string) string {
|
||||||
|
return fmt.Sprintf("oauth-%s", ipAddress)
|
||||||
|
}
|
430
backend/internal/entity/auth/oauth_test.go
Normal file
430
backend/internal/entity/auth/oauth_test.go
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"npm/internal/entity/setting"
|
||||||
|
|
||||||
|
cache "github.com/patrickmn/go-cache"
|
||||||
|
"github.com/rotisserie/eris"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetOAuth2Config(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mockSettings setting.OAuthSettings
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid settings",
|
||||||
|
mockSettings: setting.OAuthSettings{
|
||||||
|
ClientID: "valid-client-id",
|
||||||
|
ClientSecret: "valid-client-secret",
|
||||||
|
AuthURL: "https://auth.url",
|
||||||
|
TokenURL: "https://token.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing ClientID",
|
||||||
|
mockSettings: setting.OAuthSettings{
|
||||||
|
ClientSecret: "valid-client-secret",
|
||||||
|
AuthURL: "https://auth.url",
|
||||||
|
TokenURL: "https://token.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
},
|
||||||
|
expectedError: eris.New("oauth-settings-incorrect"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing ClientSecret",
|
||||||
|
mockSettings: setting.OAuthSettings{
|
||||||
|
ClientID: "valid-client-id",
|
||||||
|
AuthURL: "https://auth.url",
|
||||||
|
TokenURL: "https://token.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
},
|
||||||
|
expectedError: eris.New("oauth-settings-incorrect"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing AuthURL",
|
||||||
|
mockSettings: setting.OAuthSettings{
|
||||||
|
ClientID: "valid-client-id",
|
||||||
|
ClientSecret: "valid-client-secret",
|
||||||
|
TokenURL: "https://token.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
},
|
||||||
|
expectedError: eris.New("oauth-settings-incorrect"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing TokenURL",
|
||||||
|
mockSettings: setting.OAuthSettings{
|
||||||
|
ClientID: "valid-client-id",
|
||||||
|
ClientSecret: "valid-client-secret",
|
||||||
|
AuthURL: "https://auth.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
},
|
||||||
|
expectedError: eris.New("oauth-settings-incorrect"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Mock the GetOAuthSettings function
|
||||||
|
settingGetOAuthSettings = func() (setting.OAuthSettings, error) {
|
||||||
|
return tt.mockSettings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config, settings, err := getOAuth2Config()
|
||||||
|
|
||||||
|
if tt.expectedError != nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, tt.expectedError.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, config)
|
||||||
|
assert.NotNil(t, settings)
|
||||||
|
assert.Equal(t, tt.mockSettings.ClientID, config.ClientID)
|
||||||
|
assert.Equal(t, tt.mockSettings.ClientSecret, config.ClientSecret)
|
||||||
|
assert.Equal(t, tt.mockSettings.AuthURL, config.Endpoint.AuthURL)
|
||||||
|
assert.Equal(t, tt.mockSettings.TokenURL, config.Endpoint.TokenURL)
|
||||||
|
assert.Equal(t, tt.mockSettings.Scopes, config.Scopes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEmail(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
oauthUser OAuthUser
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Email in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"email": "user@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Identifier is email",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Identifier: "user@example.com",
|
||||||
|
},
|
||||||
|
expected: "user@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Identifier is not email",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Identifier: "user123",
|
||||||
|
},
|
||||||
|
expected: "user123@oauth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No email or identifier",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{},
|
||||||
|
},
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
email := tt.oauthUser.GetEmail()
|
||||||
|
assert.Equal(t, tt.expected, email)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
oauthUser OAuthUser
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Nickname in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"nickname": "user_nick",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user_nick",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Given name in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"given_name": "User Given",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "User Given",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Name in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"name": "User Name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "User Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Preferred username in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"preferred_username": "preferred_user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "preferred_user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Username in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"username": "user123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No name fields in resource, fallback to identifier",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Identifier: "fallback_identifier",
|
||||||
|
Resource: map[string]any{},
|
||||||
|
},
|
||||||
|
expected: "fallback_identifier",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No name fields and no identifier",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{},
|
||||||
|
},
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All fields",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Identifier: "fallback_identifier",
|
||||||
|
Resource: map[string]any{
|
||||||
|
"nickname": "user_nick",
|
||||||
|
"given_name": "User Given",
|
||||||
|
"name": "User Name",
|
||||||
|
"preferred_username": "preferred_user",
|
||||||
|
"username": "user123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user_nick",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
name := tt.oauthUser.GetName()
|
||||||
|
assert.Equal(t, tt.expected, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetID(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
oauthUser OAuthUser
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Identifier is set",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Identifier: "user123",
|
||||||
|
},
|
||||||
|
expected: "user123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UID in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"uid": "uid123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "uid123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "User ID in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"user_id": "user_id123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user_id123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Username in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"username": "username123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "username123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Preferred username in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"preferred_username": "preferred_user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "preferred_user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Email in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"email": "user@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mail in resource",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{
|
||||||
|
"mail": "mail@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "mail@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No identifier or resource fields",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Resource: map[string]any{},
|
||||||
|
},
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All fields",
|
||||||
|
oauthUser: OAuthUser{
|
||||||
|
Identifier: "user123",
|
||||||
|
Resource: map[string]any{
|
||||||
|
"uid": "uid123",
|
||||||
|
"user_id": "user_id123",
|
||||||
|
"username": "username123",
|
||||||
|
"preferred_username": "preferred_user",
|
||||||
|
"mail": "mail@example.com",
|
||||||
|
"email": "email@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "user123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
id := tt.oauthUser.GetID()
|
||||||
|
assert.Equal(t, tt.expected, id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthLogin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
redirectBase string
|
||||||
|
ipAddress string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid redirect base",
|
||||||
|
redirectBase: "https://redirect.base",
|
||||||
|
ipAddress: "127.0.0.1",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty redirect base",
|
||||||
|
redirectBase: "",
|
||||||
|
ipAddress: "127.0.0.1",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Mock the GetOAuthSettings function
|
||||||
|
settingGetOAuthSettings = func() (setting.OAuthSettings, error) {
|
||||||
|
return setting.OAuthSettings{
|
||||||
|
ClientID: "valid-client-id",
|
||||||
|
ClientSecret: "valid-client-secret",
|
||||||
|
AuthURL: "https://auth.url",
|
||||||
|
TokenURL: "https://token.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := OAuthLogin(tt.redirectBase, tt.ipAddress)
|
||||||
|
|
||||||
|
if tt.expectedError != nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, tt.expectedError.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthReturn(t *testing.T) {
|
||||||
|
var errNotFound = eris.New("oauth-verifier-not-found")
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
code string
|
||||||
|
ipAddress string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid code",
|
||||||
|
code: "invalid-code",
|
||||||
|
ipAddress: "127.0.0.100",
|
||||||
|
expectedError: errNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Mock the GetOAuthSettings function
|
||||||
|
settingGetOAuthSettings = func() (setting.OAuthSettings, error) {
|
||||||
|
return setting.OAuthSettings{
|
||||||
|
ClientID: "valid-client-id",
|
||||||
|
ClientSecret: "valid-client-secret",
|
||||||
|
AuthURL: "https://auth.url",
|
||||||
|
TokenURL: "https://token.url",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
ResourceURL: "https://resource.url",
|
||||||
|
Identifier: "id",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise the cache and set a verifier
|
||||||
|
OAuthCacheInit()
|
||||||
|
if tt.expectedError != errNotFound {
|
||||||
|
OAuthCache.Set(getCacheKey(tt.ipAddress), "valid-verifier", cache.DefaultExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
user, err := OAuthReturn(ctx, tt.code, tt.ipAddress)
|
||||||
|
|
||||||
|
if tt.expectedError != nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, tt.expectedError.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -44,7 +44,7 @@ const (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Type string `json:"type" gorm:"column:type" filter:"type,string"`
|
Type string `json:"type" gorm:"column:type" filter:"type,string"`
|
||||||
CertificateAuthorityID types.NullableDBUint `json:"certificate_authority_id" gorm:"column:certificate_authority_id" filter:"certificate_authority_id,integer"`
|
CertificateAuthorityID types.NullableDBUint `json:"certificate_authority_id" gorm:"column:certificate_authority_id" filter:"certificate_authority_id,integer"`
|
||||||
@@ -244,6 +244,7 @@ func (m *Model) Request() error {
|
|||||||
certKeyFile, certFullchainFile, certFolder := m.GetCertificateLocations()
|
certKeyFile, certFullchainFile, certFolder := m.GetCertificateLocations()
|
||||||
|
|
||||||
// ensure certFolder is created
|
// ensure certFolder is created
|
||||||
|
// nolint: gosec
|
||||||
if err := os.MkdirAll(certFolder, os.ModePerm); err != nil {
|
if err := os.MkdirAll(certFolder, os.ModePerm); err != nil {
|
||||||
logger.Error("CreateFolderError", err)
|
logger.Error("CreateFolderError", err)
|
||||||
return err
|
return err
|
||||||
|
@@ -122,12 +122,12 @@ func (s *testsuite) TestList() {
|
|||||||
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
s.mock.
|
s.mock.
|
||||||
ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "certificate_authority" WHERE name LIKE $1 AND "certificate_authority"."is_deleted" = $2`)).
|
ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "certificate_authority" WHERE "certificate_authority"."name" LIKE $1 AND "certificate_authority"."is_deleted" = $2`)).
|
||||||
WithArgs("%test%", 0).
|
WithArgs("%test%", 0).
|
||||||
WillReturnRows(s.listCountRows)
|
WillReturnRows(s.listCountRows)
|
||||||
|
|
||||||
s.mock.
|
s.mock.
|
||||||
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "certificate_authority" WHERE name LIKE $1 AND "certificate_authority"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)).
|
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "certificate_authority" WHERE "certificate_authority"."name" LIKE $1 AND "certificate_authority"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)).
|
||||||
WithArgs("%test%", 0, 8).
|
WithArgs("%test%", 0, 8).
|
||||||
WillReturnRows(s.listRows)
|
WillReturnRows(s.listRows)
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ func (s *testsuite) TestDelete() {
|
|||||||
assert.Equal(s.T(), "Unable to delete a new object", err.Error())
|
assert.Equal(s.T(), "Unable to delete a new object", err.Error())
|
||||||
|
|
||||||
m2 := Model{
|
m2 := Model{
|
||||||
ModelBase: model.ModelBase{
|
Base: model.Base{
|
||||||
ID: 10,
|
ID: 10,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
AcmeshServer string `json:"acmesh_server" gorm:"column:acmesh_server" filter:"acmesh_server,string"`
|
AcmeshServer string `json:"acmesh_server" gorm:"column:acmesh_server" filter:"acmesh_server,string"`
|
||||||
CABundle string `json:"ca_bundle" gorm:"column:ca_bundle" filter:"ca_bundle,string"`
|
CABundle string `json:"ca_bundle" gorm:"column:ca_bundle" filter:"ca_bundle,string"`
|
||||||
|
@@ -204,12 +204,12 @@ func (s *testsuite) TestList() {
|
|||||||
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
s.mock.
|
s.mock.
|
||||||
ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "dns_provider" WHERE acmesh_name LIKE $1 AND "dns_provider"."is_deleted" = $2`)).
|
ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "dns_provider" WHERE "dns_provider"."acmesh_name" LIKE $1 AND "dns_provider"."is_deleted" = $2`)).
|
||||||
WithArgs("dns%", 0).
|
WithArgs("dns%", 0).
|
||||||
WillReturnRows(s.listCountRows)
|
WillReturnRows(s.listCountRows)
|
||||||
|
|
||||||
s.mock.
|
s.mock.
|
||||||
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "dns_provider" WHERE acmesh_name LIKE $1 AND "dns_provider"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)).
|
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "dns_provider" WHERE "dns_provider"."acmesh_name" LIKE $1 AND "dns_provider"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)).
|
||||||
WithArgs("dns%", 0, 8).
|
WithArgs("dns%", 0, 8).
|
||||||
WillReturnRows(s.listRows)
|
WillReturnRows(s.listRows)
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ func (s *testsuite) TestDelete() {
|
|||||||
assert.Equal(s.T(), "Unable to delete a new object", err.Error())
|
assert.Equal(s.T(), "Unable to delete a new object", err.Error())
|
||||||
|
|
||||||
m2 := Model{
|
m2 := Model{
|
||||||
ModelBase: model.ModelBase{
|
Base: model.Base{
|
||||||
ID: 10,
|
ID: 10,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
AcmeshName string `json:"acmesh_name" gorm:"column:acmesh_name" filter:"acmesh_name,string"`
|
AcmeshName string `json:"acmesh_name" gorm:"column:acmesh_name" filter:"acmesh_name,string"`
|
||||||
@@ -70,8 +70,8 @@ func (m *Model) GetAcmeShEnvVars() ([]string, error) {
|
|||||||
return envs, nil
|
return envs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnvsFromMeta(meta interface{}) []string {
|
func getEnvsFromMeta(meta any) []string {
|
||||||
if rec, ok := meta.(map[string]interface{}); ok {
|
if rec, ok := meta.(map[string]any); ok {
|
||||||
envs := make([]string, 0)
|
envs := make([]string, 0)
|
||||||
for key, val := range rec {
|
for key, val := range rec {
|
||||||
if f, ok := val.(string); ok {
|
if f, ok := val.(string); ok {
|
||||||
@@ -81,8 +81,8 @@ func getEnvsFromMeta(meta interface{}) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return envs
|
return envs
|
||||||
} else {
|
|
||||||
logger.Debug("getEnvsFromMeta: meta is not an map of strings")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug("getEnvsFromMeta: meta is not an map of strings")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -6,22 +6,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetFilterMap returns the filter map
|
// GetFilterMap returns the filter map
|
||||||
func GetFilterMap(m interface{}, includeBaseEntity bool) map[string]model.FilterMapValue {
|
// _ was called `includeBaseEntity`
|
||||||
filterMap := tags.GetFilterMap(m)
|
func GetFilterMap(m any, _ bool) map[string]model.FilterMapValue {
|
||||||
if includeBaseEntity {
|
filterMap := tags.GetFilterMap(m, "")
|
||||||
return mergeFilterMaps(tags.GetFilterMap(model.ModelBase{}), filterMap)
|
|
||||||
}
|
// TODO: this is done in GetFilterMap isn't it?
|
||||||
|
// if includeBaseEntity {
|
||||||
|
// return mergeFilterMaps(tags.GetFilterMap(model.Base{}, ""), filterMap)
|
||||||
|
// }
|
||||||
|
|
||||||
return filterMap
|
return filterMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeFilterMaps(m1 map[string]model.FilterMapValue, m2 map[string]model.FilterMapValue) map[string]model.FilterMapValue {
|
|
||||||
merged := make(map[string]model.FilterMapValue, 0)
|
|
||||||
for k, v := range m1 {
|
|
||||||
merged[k] = v
|
|
||||||
}
|
|
||||||
for key, value := range m2 {
|
|
||||||
merged[key] = value
|
|
||||||
}
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
@@ -189,7 +189,7 @@ func (s *testsuite) TestDelete() {
|
|||||||
assert.Equal(s.T(), "Unable to delete a new object", err.Error())
|
assert.Equal(s.T(), "Unable to delete a new object", err.Error())
|
||||||
|
|
||||||
m2 := Model{
|
m2 := Model{
|
||||||
ModelBase: model.ModelBase{
|
Base: model.Base{
|
||||||
ID: 10,
|
ID: 10,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ func (s *testsuite) TestGetTemplate() {
|
|||||||
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
|
||||||
|
|
||||||
m := Model{
|
m := Model{
|
||||||
ModelBase: model.ModelBase{
|
Base: model.Base{
|
||||||
ID: 10,
|
ID: 10,
|
||||||
CreatedAt: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
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(),
|
UpdatedAt: time.Date(2018, 8, 12, 7, 30, 24, 16, time.UTC).UnixMilli(),
|
||||||
|
@@ -27,7 +27,7 @@ const (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Type string `json:"type" gorm:"column:type" filter:"type,string"`
|
Type string `json:"type" gorm:"column:type" filter:"type,string"`
|
||||||
NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"`
|
NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"`
|
||||||
|
@@ -14,12 +14,12 @@ type ListResponse struct {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
Sort []model.Sort `json:"sort"`
|
Sort []model.Sort `json:"sort"`
|
||||||
Filter []model.Filter `json:"filter,omitempty"`
|
Filter []model.Filter `json:"filter,omitempty"`
|
||||||
Items interface{} `json:"items,omitempty"`
|
Items any `json:"items,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListQueryBuilder is used to setup queries for lists
|
// ListQueryBuilder is used to setup queries for lists
|
||||||
func ListQueryBuilder(
|
func ListQueryBuilder(
|
||||||
pageInfo *model.PageInfo,
|
_ *model.PageInfo,
|
||||||
filters []model.Filter,
|
filters []model.Filter,
|
||||||
filterMap map[string]model.FilterMapValue,
|
filterMap map[string]model.FilterMapValue,
|
||||||
) *gorm.DB {
|
) *gorm.DB {
|
||||||
|
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
Type string `json:"type" gorm:"column:type" filter:"type,string"`
|
Type string `json:"type" gorm:"column:type" filter:"type,string"`
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ScopeOffsetLimit ...
|
||||||
func ScopeOffsetLimit(pageInfo *model.PageInfo) func(db *gorm.DB) *gorm.DB {
|
func ScopeOffsetLimit(pageInfo *model.PageInfo) func(db *gorm.DB) *gorm.DB {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
if pageInfo.Offset > 0 || pageInfo.Limit > 0 {
|
if pageInfo.Offset > 0 || pageInfo.Limit > 0 {
|
||||||
@@ -19,6 +20,7 @@ func ScopeOffsetLimit(pageInfo *model.PageInfo) func(db *gorm.DB) *gorm.DB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScopeOrderBy ...
|
||||||
func ScopeOrderBy(sort []model.Sort, defaultSort model.Sort) func(db *gorm.DB) *gorm.DB {
|
func ScopeOrderBy(sort []model.Sort, defaultSort model.Sort) func(db *gorm.DB) *gorm.DB {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
if sort != nil {
|
if sort != nil {
|
||||||
@@ -36,6 +38,7 @@ func ScopeOrderBy(sort []model.Sort, defaultSort model.Sort) func(db *gorm.DB) *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScopeFilters ...
|
||||||
func ScopeFilters(filters []model.Filter, filterMap map[string]model.FilterMapValue) func(db *gorm.DB) *gorm.DB {
|
func ScopeFilters(filters []model.Filter, filterMap map[string]model.FilterMapValue) func(db *gorm.DB) *gorm.DB {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
like := database.GetCaseInsensitiveLike()
|
like := database.GetCaseInsensitiveLike()
|
||||||
|
33
backend/internal/entity/scopes_test.go
Normal file
33
backend/internal/entity/scopes_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseBoolValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{"yes", []string{"1"}},
|
||||||
|
{"true", []string{"1"}},
|
||||||
|
{"on", []string{"1"}},
|
||||||
|
{"t", []string{"1"}},
|
||||||
|
{"1", []string{"1"}},
|
||||||
|
{"y", []string{"1"}},
|
||||||
|
{"no", []string{"0"}},
|
||||||
|
{"false", []string{"0"}},
|
||||||
|
{"off", []string{"0"}},
|
||||||
|
{"f", []string{"0"}},
|
||||||
|
{"0", []string{"0"}},
|
||||||
|
{"n", []string{"0"}},
|
||||||
|
{"random", []string{"0"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := parseBoolValue(test.input)
|
||||||
|
assert.Equal(t, test.expected, result, "Input: %s", test.input)
|
||||||
|
}
|
||||||
|
}
|
32
backend/internal/entity/setting/auth_methods.go
Normal file
32
backend/internal/entity/setting/auth_methods.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAuthMethods returns the authentication methods enabled for this site
|
||||||
|
func GetAuthMethods() ([]string, error) {
|
||||||
|
var m Model
|
||||||
|
if err := m.LoadByName("auth-methods"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var r []string
|
||||||
|
if err := json.Unmarshal([]byte(m.Value.String()), &r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMethodEnabled checks that the auth method given is
|
||||||
|
// enabled in the db setting
|
||||||
|
func AuthMethodEnabled(method string) bool {
|
||||||
|
r, err := GetAuthMethods()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Contains(r, method)
|
||||||
|
}
|
32
backend/internal/entity/setting/ldap.go
Normal file
32
backend/internal/entity/setting/ldap.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LDAPSettings are the settings for LDAP that come from
|
||||||
|
// the `ldap-auth` setting value
|
||||||
|
type LDAPSettings struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
BaseDN string `json:"base_dn"`
|
||||||
|
UserDN string `json:"user_dn"`
|
||||||
|
EmailProperty string `json:"email_property"`
|
||||||
|
NameProperty string `json:"name_property"`
|
||||||
|
SelfFilter string `json:"self_filter"`
|
||||||
|
AutoCreateUser bool `json:"auto_create_user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLDAPSettings will return the LDAP settings
|
||||||
|
func GetLDAPSettings() (LDAPSettings, error) {
|
||||||
|
var l LDAPSettings
|
||||||
|
var m Model
|
||||||
|
if err := m.LoadByName("ldap-auth"); err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
Description string `json:"description" gorm:"column:description" filter:"description,string"`
|
Description string `json:"description" gorm:"column:description" filter:"description,string"`
|
||||||
Value datatypes.JSON `json:"value" gorm:"column:value"`
|
Value datatypes.JSON `json:"value" gorm:"column:value"`
|
||||||
|
42
backend/internal/entity/setting/oauth.go
Normal file
42
backend/internal/entity/setting/oauth.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthSettings are the settings for OAuth that come from
|
||||||
|
// the `oauth-auth` setting value
|
||||||
|
type OAuthSettings struct {
|
||||||
|
AutoCreateUser bool `json:"auto_create_user"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
AuthURL string `json:"authorization_url"`
|
||||||
|
TokenURL string `json:"token_url"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
LogoutURL string `json:"logout_url"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
ResourceURL string `json:"resource_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOAuthSettings will return the OAuth settings
|
||||||
|
func GetOAuthSettings() (OAuthSettings, error) {
|
||||||
|
var o OAuthSettings
|
||||||
|
var m Model
|
||||||
|
if err := m.LoadByName("oauth-auth"); err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(m.Value.String()), &o); err != nil {
|
||||||
|
return o, err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ApplyDefaults()
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyDefaults will ensure there are defaults set
|
||||||
|
func (m *OAuthSettings) ApplyDefaults() {
|
||||||
|
if m.Identifier == "" {
|
||||||
|
m.Identifier = "email"
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
ExpiresOn types.DBDate `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"`
|
ExpiresOn types.DBDate `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"`
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Provider string `json:"provider" gorm:"column:provider" filter:"provider,string"`
|
Provider string `json:"provider" gorm:"column:provider" filter:"provider,string"`
|
||||||
|
@@ -17,7 +17,7 @@ import (
|
|||||||
// Model is the model
|
// Model is the model
|
||||||
// See: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
|
// See: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.Base
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"`
|
NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"`
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user