Add support for multiple different users, rewrote API, breaking API changes, UI changes

* Require 1.9.6 for upgrade, add function to get userID from request

* Automatically add user when successfully authenticated with headers / oauth, disallow modifing own user permissions

* Dont show user/pw page when using header authentication

* Only display redacted versions of API keys #228, fixed deployment password

* Added animation for deleting API key

* Only create salt once

* Disable elements on upload UI if insufficient permissions

* BREAKING: User field must be email for OAUTH2, added warning in setup when changing database

* BREAKING: Added option to restrict to only registered users

* Fixed crash due to concurrent map iteration

* Replace /uploadComplete with API call, BREAKING API is now in headers

* BREAKING: require true|false instead of only checking for true

* BREAKING API: Renamed apiKeyToModify parameter to targetKey
This commit is contained in:
Marc Bulling
2025-02-04 09:22:55 +01:00
committed by GitHub
parent e0c0dcc55c
commit d88f649209
82 changed files with 8340 additions and 3056 deletions
+268
View File
@@ -0,0 +1,268 @@
package main
import (
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"os"
"strings"
)
const fileRouting = "../../internal/webserver/api/routing.go"
const fileOutput = "../../internal/webserver/api/routingParsing.go"
// Function to find all declared types referenced in the RequestParser field
func findDeclaredTypes(filePath string) ([]*ast.TypeSpec, error) {
// Open the source file containing the struct
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// Parse the Go source code
fs := token.NewFileSet()
node, err := parser.ParseFile(fs, filePath, file, parser.ParseComments)
if err != nil {
return nil, err
}
// Map to store the found types
var declaredTypes []*ast.TypeSpec
// Traverse the AST to find the struct definitions (type declarations)
for _, decl := range node.Decls {
if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.TYPE {
for _, spec := range genDecl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if strings.HasPrefix(typeSpec.Name.String(), "param") {
declaredTypes = append(declaredTypes, typeSpec)
}
}
}
}
}
return declaredTypes, nil
}
func hasTags(fields []*ast.Field) bool {
for _, field := range fields {
if field.Tag != nil {
// Extract the header tag by accessing the field.Tag.Value
tag := field.Tag.Value
if tag != "" {
// Remove backticks
tag = tag[1 : len(tag)-1]
// Check if the tag has the "header" key and extract its value
tagParts := strings.Split(tag, " ")
for _, part := range tagParts {
if strings.HasPrefix(part, "header:") {
return true
}
}
}
}
}
return false
}
func hasRequiredTag(tags []string) bool {
// Check if the tag contains "required:true"
for _, tag := range tags {
if strings.HasPrefix(tag, "required") {
return true
}
}
return false
}
func headerExists(headerName string, required, isString bool) string {
return fmt.Sprintf("\n"+`
// RequestParser header value %s, required: %v
exists, err = checkHeaderExists(r, %s, %v, %v)
if err != nil {
return err
}
p.foundHeaders[%s] = exists`, headerName, required, headerName, required, isString, headerName)
}
func generateParseRequestMethod(typeName string, fields []*ast.Field) string {
// Start generating the ParseRequest method
if !hasTags(fields) {
return fmt.Sprintf(`
// ParseRequest parses the header file. As %s has no fields with the
// tag header, this method does nothing, except calling ProcessParameter()
func (p *%s) ParseRequest(r *http.Request) error {
return p.ProcessParameter(r)
}
%s`, typeName, typeName, writeNewInstanceCode(typeName))
}
method := fmt.Sprintf(`// ParseRequest reads r and saves the passed header values in the %s struct
// In the end, ProcessParameter() is called
func (p *%s) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)`, typeName, typeName)
// Iterate over the fields and generate parsing logic for those with a header tag
for _, field := range fields {
if field.Tag != nil {
// Extract the header tag by accessing the field.Tag.Value
tag := field.Tag.Value
if tag != "" {
// Remove backticks
tag = tag[1 : len(tag)-1]
// Check if the tag has the "header" key and extract its value
tagParts := strings.Split(tag, " ")
required := hasRequiredTag(tagParts)
for _, part := range tagParts {
if strings.HasPrefix(part, "header:") {
// Extract header name after 'header:'
headerName := strings.TrimPrefix(part, "header:")
fieldType := field.Type.(*ast.Ident).Name
// Use appropriate parsing function based on the field type
switch fieldType {
case "string":
method += headerExists(headerName, required, true)
method += fmt.Sprintf(`
if (exists) {
p.%s = r.Header.Get(%s)
}
`, field.Names[0].Name, headerName)
case "bool":
method += headerExists(headerName, required, false)
method += fmt.Sprintf(`
if (exists) {
p.%s, err = parseHeaderBool(r, %s)
if err != nil {
return fmt.Errorf("invalid value in header %s supplied")
}
}
`, field.Names[0].Name, headerName, strings.Replace(headerName, "\"", "", -1))
case "int":
method += headerExists(headerName, required, false)
method += fmt.Sprintf(`
if (exists) {
p.%s, err = parseHeaderInt(r, %s)
if err != nil {
return fmt.Errorf("invalid value in header %s supplied")
}
}
`, field.Names[0].Name, headerName, strings.Replace(headerName, "\"", "", -1))
case "int64":
method += headerExists(headerName, required, false)
method += fmt.Sprintf(`
if (exists) {
p.%s, err = parseHeaderInt64(r, %s)
if err != nil {
return fmt.Errorf("invalid value in header %s supplied")
}
}
`, field.Names[0].Name, headerName, strings.Replace(headerName, "\"", "", -1))
default:
panic("unsupported field type")
}
}
}
}
}
}
method += "\nreturn p.ProcessParameter(r)\n}\n"
method += writeNewInstanceCode(typeName)
return method
}
func writeNewInstanceCode(name string) string {
return fmt.Sprintf(`
// New returns a new instance of %s struct
func (p *%s) New() requestParser {
return &%s{}
}`, name, name, name)
}
func writeAndFormatCode(generatedCode string, filePath string) error {
// Write the generated code to the specified file
err := os.WriteFile(filePath, []byte(generatedCode), 0644)
if err != nil {
return fmt.Errorf("failed to write file: %v", err)
}
// Read the file to format
fileContent, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file: %v", err)
}
// Format the content using go fmt
formattedContent, err := format.Source(fileContent)
if err != nil {
return fmt.Errorf("failed to format file: %v", err)
}
// Write the formatted content back to the file
err = os.WriteFile(filePath, formattedContent, 0644)
if err != nil {
return fmt.Errorf("failed to write formatted file: %v", err)
}
return nil
}
func main() {
// Find declared types in the routings.go file
types, err := findDeclaredTypes(fileRouting)
if err != nil {
log.Fatalf("Error finding types: %v", err)
}
var output strings.Builder
output.WriteString(`// Code generated by updateApiRouting.go - DO NOT EDIT.
package api
import (
"fmt"
"net/http"
)
// Do not modify: This is an automatically generated file created by updateApiRouting.go
// It contains the code that is used to parse the headers submitted in an API request
`)
// Process each struct type
for _, typeSpec := range types {
// Find the struct definition and its fields
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
// Generate the ParseRequest method for the struct
method := generateParseRequestMethod(typeSpec.Name.Name, structType.Fields.List)
output.WriteString(method + "\n\n")
}
err = writeAndFormatCode(output.String(), fileOutput)
if err != nil {
log.Fatalf("Error writing file: %v", err)
}
fmt.Println("Updated API parsing")
}
+3 -2
View File
@@ -58,9 +58,10 @@ func parseProtectedUrls() []string {
}
func writeConstantFile(urls []string) {
var output = `package setup
var output = `// Code generated by updateProtectedUrls.go - DO NOT EDIT.
package setup
// Do not modify: This is an automatically generated File.
// Do not modify: This is an automatically generated file created by updateProtectedUrls.go
// It contains all URLs that need to be protected when using an external authentication.
// protectedUrls contains a list of URLs that need to be protected if authentication is disabled.
+7 -796
View File
@@ -1,902 +1,113 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.mills.io/prologic/bitcask v1.0.2/go.mod h1:ppXpR3haeYrijyJDleAkSGH3p90w6sIHxEA/7UHMxH4=
git.sr.ht/~shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:7YhY1ru/6vTScuHp4NpcCVCUIyfTdPK7+h4NaJohCCk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.44.233/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.45.24/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.48.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.48.2/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.49.22/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.51.7/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.51.25/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.53.19/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.54.11/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/coreos/go-oidc/v3 v3.7.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/johannesboyne/gofakes3 v0.0.0-20210415062230-4b6b67a85d38/go.mod h1:Zj9d90chLFOXPNj/m+HfCAFx1s8zSue9HiqC/hbHLS0=
github.com/johannesboyne/gofakes3 v0.0.0-20230914150226-f005f5cc03aa/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g=
github.com/johannesboyne/gofakes3 v0.0.0-20240217095638-c55a48f17be6/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g=
github.com/johannesboyne/gofakes3 v0.0.0-20240513200200-99de01ee122d/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g=
github.com/johannesboyne/gofakes3 v0.0.0-20241026070602-0da3aa9c32ca/go.mod h1:t6osVdP++3g4v2awHz4+HFccij23BbdT1rX3W7IijqQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/plar/go-adaptive-radix-tree v1.0.4/go.mod h1:Ot8d28EII3i7Lv4PSvBlF8ejiD/CtRYDuPsySJbSaK8=
github.com/plar/go-adaptive-radix-tree v1.0.5/go.mod h1:15VOUO7R9MhJL8HOJdpydR0rvanrtRE6fA6XSa/tqWE=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs=
github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:n+VKSARF5y/tS9XFSP7vWDfS+GUC5vs/YT7M5XDTUEM=
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tdewolff/minify/v2 v2.12.5 h1:s2KDBt/D/3ayE3gcqQF8VIgTmYgkx+btuLvVAeePzZM=
github.com/tdewolff/minify/v2 v2.12.5/go.mod h1:i8QXtVyL7Ddwc4I5gqzvgBqKlTMgMNTbiXaPO4Iqg+A=
github.com/tdewolff/minify/v2 v2.20.7/go.mod h1:bj2NpP3zoUhsPzE4oM4JYwuUyVCU/uMaCYZ6/riEjIo=
github.com/tdewolff/minify/v2 v2.20.34 h1:XueI6sQtgS7du45fyBCNkNfPQ9SINaYavMFNOxp37SA=
github.com/tdewolff/minify/v2 v2.20.34/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU=
github.com/tdewolff/parse/v2 v2.6.5 h1:lYvWBk55GkqKl0JJenGpmrgu/cPHQQ6/Mm1hBGswoGQ=
github.com/tdewolff/parse/v2 v2.6.5/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
github.com/tdewolff/parse/v2 v2.7.5/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw=
github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tidwall/btree v0.4.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/redcon v1.4.1/go.mod h1:XwNPFbJ4ShWNNSA2Jazhbdje6jegTCcwFR6mfaADvHA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/cc/v4 v4.19.5/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
modernc.org/ccgo/v4 v4.13.1/go.mod h1:Td6RI9W9G2ZpKHaJ7UeGEiB2aIpoDqLBnm4wtkbJTbQ=
modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.34.9/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/libc v1.40.5/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/libc v1.47.0/go.mod h1:gzCncw0a74aCiVqHeWAYHHaW//fkSHHS/3S/gfhLlCI=
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ=
modernc.org/libc v1.53.4/go.mod h1:aGsLofnkcct8lTJnKQnCqJO37ERAXSHamSuWLFoF2Cw=
modernc.org/libc v1.61.4/go.mod h1:VfXVuM/Shh5XsMNrh3C6OkfL78G3loa4ZC/Ljv9k7xc=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
modernc.org/sqlite v1.29.8/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk=
modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw=
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
modernc.org/sqlite v1.34.2/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+18 -1
View File
@@ -8,6 +8,7 @@ Main routine
import (
"fmt"
"github.com/forceu/gokapi/internal/configuration/configupgrade"
"github.com/forceu/gokapi/internal/configuration/database/migration"
"github.com/forceu/gokapi/internal/helper/systemd"
"os"
@@ -33,11 +34,12 @@ import (
// versionGokapi is the current version in readable form.
// Other version numbers can be modified in /build/go-generate/updateVersionNumbers.go
const versionGokapi = "1.9.6"
const versionGokapi = "2.0.0-beta1"
// The following calls update the version numbers, update documentation, minify Js/CSS and build the WASM modules
//go:generate go run "../../build/go-generate/updateVersionNumbers.go"
//go:generate go run "../../build/go-generate/updateProtectedUrls.go"
//go:generate go run "../../build/go-generate/updateApiRouting.go"
//go:generate go run "../../build/go-generate/buildWasm.go"
//go:generate go run "../../build/go-generate/copyStaticFiles.go"
//go:generate go run "../../build/go-generate/minifyStaticContent.go"
@@ -56,7 +58,15 @@ func main() {
if !reconfigureServer(passedFlags) {
configuration.ConnectDatabase()
}
// Temporary solution to migrate admin user to DB
// Will be removed in v2.1.0
if configupgrade.RequiresUpgradeV1ToV2 {
configuration.MigrateToV2(configupgrade.LegacyPasswordHash, configupgrade.LegacyUsersHeaderOauth)
}
setDeploymentPassword(passedFlags)
checkIfUserExists()
encryption.Init(*configuration.Get())
authentication.Init(configuration.Get().Authentication)
createSsl(passedFlags)
@@ -166,6 +176,13 @@ func createSsl(passedFlags flagparser.MainFlags) {
}
}
func checkIfUserExists() {
if len(database.GetAllUsers()) == 0 {
fmt.Println("No user found in database. Please run setup first or crate user with --deployment-password")
os.Exit(1)
}
}
func handleDbMigration(passedFlags flagparser.MainFlags) {
if !passedFlags.Migration.DoMigration {
return
+3 -2
View File
@@ -93,7 +93,7 @@ During the first start, a new configuration file will be created and you will be
Database
""""""""""""""
By default, Gokapi stores its data in a database located in the ``data`` directory. You can specify a different database location in this menu. If no changes are needed, you can proceed as is. Experimental Redis support is also available.
By default, Gokapi stores its data in a database located in the ``data`` directory. You can specify a different database location in this menu. If no changes are needed, you can proceed as is. Redis is recommended for servers with a high frequency of downloads.
You can configure the following settings:
@@ -229,12 +229,13 @@ This option disables Gokapis internal authentication completely, except for API
- ``/admin``
- ``/apiKeys``
- ``/changePassword``
- ``/e2eInfo``
- ``/e2eSetup``
- ``/logs``
- ``/uploadChunk``
- ``/uploadComplete``
- ``/uploadStatus``
- ``/users``
.. warning::
This option has potential to be *very* dangerous, only proceed if you know what you are doing!
+66 -5
View File
@@ -104,6 +104,57 @@ func ConnectDatabase() {
database.Upgrade()
}
// MigrateToV2 is used to migrate the previous admin user to the DB
func MigrateToV2(authPassword string, allowedUsers []string) {
fmt.Println("Migrating v1 user, metadata and API keys to v2 scheme...")
var adminName = "admin@gokapi"
if serverSettings.Authentication.Method != models.AuthenticationDisabled &&
serverSettings.Authentication.Username != "" {
adminName = serverSettings.Authentication.Username
}
newAdmin := models.User{
Name: adminName,
Permissions: models.UserPermissionAll,
UserLevel: models.UserLevelSuperAdmin,
Password: authPassword,
}
database.SaveUser(newAdmin, true)
adminUser, ok := database.GetUserByName(adminName)
if !ok {
fmt.Println("ERROR: Could not retrieve new admin user after saving")
os.Exit(1)
}
fmt.Println("Created admin user ", adminUser.Name)
for _, user := range allowedUsers {
newUser := models.User{
Name: user,
Permissions: models.UserPermissionNone,
UserLevel: models.UserLevelUser,
}
database.SaveUser(newUser, true)
fmt.Println("Created admin user ", user)
}
for _, apiKey := range database.GetAllApiKeys() {
apiKey.UserId = adminUser.Id
apiKey.PublicId = helper.GenerateRandomString(35)
database.SaveApiKey(apiKey)
}
e2eConfig := database.GetEnd2EndInfo(0)
database.DeleteEnd2EndInfo(0)
database.SaveEnd2EndInfo(e2eConfig, adminUser.Id)
for _, file := range database.GetAllMetadata() {
file.UserId = adminUser.Id
database.SaveMetaData(file)
}
database.DeleteAllSessions()
fmt.Println("Migration complete")
}
// UsesHttps returns true if Gokapi URL is set to a secure URL
func UsesHttps() bool {
return usesHttps
@@ -132,7 +183,7 @@ func save() {
// LoadFromSetup creates a new configuration file after a user completed the setup. If cloudConfig is not nil, a new
// cloud config file is created. If it is nil an existing cloud config file will be deleted.
func LoadFromSetup(config models.Configuration, cloudConfig *cloudconfig.CloudConfig, e2eConfig End2EndReconfigParameters) {
func LoadFromSetup(config models.Configuration, cloudConfig *cloudconfig.CloudConfig, e2eConfig End2EndReconfigParameters, passwordHash string) {
Environment = environment.New()
helper.CreateDir(Environment.ConfigDir)
@@ -153,9 +204,16 @@ func LoadFromSetup(config models.Configuration, cloudConfig *cloudconfig.CloudCo
save()
Load()
ConnectDatabase()
err := database.EditSuperAdmin(serverSettings.Authentication.Username, passwordHash)
if err != nil {
fmt.Println("Could not edit superadmin, as none was found, but other users were present.")
os.Exit(1)
}
database.DeleteAllSessions()
if e2eConfig.DeleteEnd2EndEncryption {
database.DeleteEnd2EndInfo()
for _, user := range database.GetAllUsers() {
database.DeleteEnd2EndInfo(user.Id)
}
}
if e2eConfig.DeleteEncryptedStorage {
deleteAllEncryptedStorage()
@@ -180,10 +238,13 @@ func SetDeploymentPassword(newPassword string) {
os.Exit(1)
}
serverSettings.Authentication.SaltAdmin = helper.GenerateRandomString(30)
serverSettings.Authentication.Password = hashUserPassword(newPassword)
database.DeleteAllSessions()
err := database.EditSuperAdmin(serverSettings.Authentication.Username, hashUserPassword(newPassword))
if err != nil {
fmt.Println("No super-admin user found, but database contains other users. Aborting.")
os.Exit(1)
}
save()
fmt.Println("New password has been set successfully")
fmt.Println("New password has been set successfully for user " + serverSettings.Authentication.Username + ".")
os.Exit(0)
}
+2 -3
View File
@@ -24,7 +24,6 @@ func TestLoad(t *testing.T) {
test.IsEqualString(t, serverSettings.Port, "127.0.0.1:53843")
test.IsEqualString(t, serverSettings.Authentication.Username, "test")
test.IsEqualString(t, serverSettings.ServerUrl, "http://127.0.0.1:53843/")
test.IsEqualString(t, serverSettings.Authentication.Password, "10340aece68aa4fb14507ae45b05506026f276cf")
test.IsEqualString(t, HashPassword("testtest", false), "10340aece68aa4fb14507ae45b05506026f276cf")
test.IsEqualBool(t, serverSettings.UseSsl, false)
test.IsEqualInt(t, serverSettings.LengthId, 20)
@@ -71,11 +70,11 @@ func TestLoadFromSetup(t *testing.T) {
}}
testconfiguration.WriteCloudConfigFile(true)
LoadFromSetup(newConfig, nil, End2EndReconfigParameters{})
LoadFromSetup(newConfig, nil, End2EndReconfigParameters{}, "")
test.FileDoesNotExist(t, "test/cloudconfig.yml")
test.IsEqualString(t, serverSettings.RedirectUrl, "redirect")
LoadFromSetup(newConfig, &newCloudConfig, End2EndReconfigParameters{})
LoadFromSetup(newConfig, &newCloudConfig, End2EndReconfigParameters{}, "")
test.FileExists(t, "test/cloudconfig.yml")
config, ok := cloudconfig.Load()
test.IsEqualBool(t, ok, true)
+87 -20
View File
@@ -1,14 +1,40 @@
package configupgrade
import (
"encoding/json"
"fmt"
"github.com/forceu/gokapi/internal/environment"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"os"
"strings"
)
// RequiresUpgradeV1ToV2 is an indicator for migrating the admin user to the database
//
// It will be removed in v2.1.0.
// Deprecated: This is a temporary solution
var RequiresUpgradeV1ToV2 = false
// LegacyPasswordHash is the hash, which was originally stored in
// AuthenticationConfig.Password and needs to be passed to the migration
//
// It will be removed in v2.1.0
// Deprecated: This is a temporary solution
var LegacyPasswordHash string
// LegacyUsersHeaderOauth is the user restriction from OAuth2 or Header authentication
//
// and needs to be passed to the migration
//
// It will be removed in v2.1.0
// Deprecated: This is a temporary solution
var LegacyUsersHeaderOauth []string
// CurrentConfigVersion is the version of the configuration structure. Used for upgrading
const CurrentConfigVersion = 21
const CurrentConfigVersion = 22
const minConfigVersion = 21
// DoUpgrade checks if an old version is present and updates it to the current version if required
func DoUpgrade(settings *models.Configuration, env *environment.Environment) bool {
@@ -23,31 +49,72 @@ func DoUpgrade(settings *models.Configuration, env *environment.Environment) boo
// Upgrades the settings if saved with a previous version
func updateConfig(settings *models.Configuration, env *environment.Environment) {
// < v1.8.0
if settings.ConfigVersion < 16 {
fmt.Println("Please update to version 1.8 before running this version,")
// < v1.9.0
if settings.ConfigVersion < minConfigVersion {
fmt.Println("Please update to version 1.9.6 before running this version.")
osExit(1)
return
}
// < v1.8.2
if settings.ConfigVersion < 18 {
if len(settings.Authentication.OAuthUsers) > 0 {
settings.Authentication.OAuthUserScope = "email"
// < v2.0.0
if settings.ConfigVersion < 22 {
RequiresUpgradeV1ToV2 = true
oldAuth, err := getLegacyAuth(env.ConfigPath)
helper.Check(err)
LegacyPasswordHash = oldAuth.Authentication.Password
LegacyUsersHeaderOauth = make([]string, 0)
if settings.Authentication.Method == models.AuthenticationOAuth2 || settings.Authentication.Method == models.AuthenticationHeader {
adminUser := os.Getenv("GOKAPI_ADMIN_USER")
if adminUser == "" {
fmt.Println("FAILED UPDATE")
fmt.Println("--> If using Oauth or Header authentication, please set the env variable GOKAPI_ADMIN_USER to the value of the expected user name / email")
fmt.Println("--> See the release notes for more information")
osExit(1)
return
} else {
fmt.Println("Setting admin user to " + adminUser)
settings.Authentication.Username = adminUser
}
if settings.Authentication.Method == models.AuthenticationOAuth2 && len(oldAuth.Authentication.OAuthUsers) > 0 {
LegacyUsersHeaderOauth = oldAuth.Authentication.OAuthUsers
settings.Authentication.OnlyRegisteredUsers = true
}
if settings.Authentication.Method == models.AuthenticationHeader && len(oldAuth.Authentication.HeaderUsers) > 0 {
LegacyUsersHeaderOauth = oldAuth.Authentication.HeaderUsers
settings.Authentication.OnlyRegisteredUsers = true
}
for _, user := range LegacyUsersHeaderOauth {
if strings.Contains(user, "*") {
fmt.Println("FAILED UPDATE")
fmt.Println("--> If using Oauth or Header authentication and restricting the users, please remove any wildcards before upgrading.")
fmt.Println("--> See the release notes for more information")
osExit(1)
return
}
}
}
settings.Authentication.OAuthRecheckInterval = 168
}
// < v1.8.5beta
if settings.ConfigVersion < 19 {
if settings.MaxMemory == 40 {
settings.MaxMemory = 50
}
settings.ChunkSize = env.ChunkSizeMB
settings.MaxParallelUploads = env.MaxParallelUploads
}
func getLegacyAuth(configFile string) (legacyAuthentication, error) {
file, err := os.Open(configFile)
if err != nil {
return legacyAuthentication{}, err
}
// < v1.9.0
if settings.ConfigVersion < 21 {
settings.DatabaseUrl = "sqlite://" + env.DataDir + "/" + env.DatabaseName
decoder := json.NewDecoder(file)
settings := legacyAuthentication{}
err = decoder.Decode(&settings)
if err != nil {
return legacyAuthentication{}, err
}
return settings, nil
}
type legacyAuthentication struct {
Authentication struct {
Password string `json:"password"`
HeaderUsers []string `json:"HeaderUsers"`
OAuthUsers []string `json:"OauthUsers"`
}
}
@@ -29,21 +29,21 @@ func TestUpgradeDb(t *testing.T) {
exitCode = code
}
env := environment.New()
oldConfigFile.ConfigVersion = 15
// Too old to update
oldConfigFile.ConfigVersion = minConfigVersion - 1
upgradeDone := DoUpgrade(&oldConfigFile, &env)
test.IsEqualBool(t, upgradeDone, true)
test.IsEqualInt(t, exitCode, 1)
// Updatable version
exitCode = 0
oldConfigFile.ConfigVersion = 17
oldConfigFile.Authentication.OAuthUsers = []string{"test"}
oldConfigFile.MaxMemory = 40
oldConfigFile.ConfigVersion = 21
upgradeDone = DoUpgrade(&oldConfigFile, &env)
test.IsEqualBool(t, upgradeDone, true)
test.IsEqualString(t, oldConfigFile.Authentication.OAuthUserScope, "email")
test.IsEqualInt(t, oldConfigFile.MaxMemory, 50)
// TODO
test.IsEqualInt(t, exitCode, 0)
// Current Version
exitCode = 0
oldConfigFile.ConfigVersion = CurrentConfigVersion
upgradeDone = DoUpgrade(&oldConfigFile, &env)
+98 -9
View File
@@ -66,7 +66,11 @@ func Migrate(configOld, configNew models.DbConnection) {
for _, apiKey := range apiKeys {
dbNew.SaveApiKey(apiKey)
}
dbNew.SaveEnd2EndInfo(dbOld.GetEnd2EndInfo())
users := dbOld.GetAllUsers()
for _, user := range users {
dbNew.SaveUser(user, false)
dbNew.SaveEnd2EndInfo(dbOld.GetEnd2EndInfo(user.Id), user.Id)
}
files := dbOld.GetAllMetadata()
for _, file := range files {
dbNew.SaveMetaData(file)
@@ -127,28 +131,33 @@ func DeleteApiKey(id string) {
}
// GetSystemKey returns the latest UI API key
func GetSystemKey() (models.ApiKey, bool) {
return db.GetSystemKey()
func GetSystemKey(userId int) (models.ApiKey, bool) {
return db.GetSystemKey(userId)
}
// GetApiKeyByPublicKey returns an API key by using the public key
func GetApiKeyByPublicKey(publicKey string) (string, bool) {
return db.GetApiKeyByPublicKey(publicKey)
}
// E2E Section
// SaveEnd2EndInfo stores the encrypted e2e info
func SaveEnd2EndInfo(info models.E2EInfoEncrypted) {
func SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int) {
info.AvailableFiles = nil
db.SaveEnd2EndInfo(info)
db.SaveEnd2EndInfo(info, userId)
}
// GetEnd2EndInfo retrieves the encrypted e2e info
func GetEnd2EndInfo() models.E2EInfoEncrypted {
info := db.GetEnd2EndInfo()
func GetEnd2EndInfo(userId int) models.E2EInfoEncrypted {
info := db.GetEnd2EndInfo(userId)
info.AvailableFiles = GetAllMetaDataIds()
return info
}
// DeleteEnd2EndInfo resets the encrypted e2e info
func DeleteEnd2EndInfo() {
db.DeleteEnd2EndInfo()
func DeleteEnd2EndInfo(userId int) {
db.DeleteEnd2EndInfo(userId)
}
// Hotlink Section
@@ -226,3 +235,83 @@ func DeleteSession(id string) {
func DeleteAllSessions() {
db.DeleteAllSessions()
}
// DeleteAllSessionsByUser logs the specific users out
func DeleteAllSessionsByUser(userId int) {
db.DeleteAllSessionsByUser(userId)
}
// User Section
// GetAllUsers returns a map with all users
func GetAllUsers() []models.User {
return db.GetAllUsers()
}
// GetUser returns a models.User if valid or false if the ID is not valid
func GetUser(id int) (models.User, bool) {
return db.GetUser(id)
}
// GetUserByName returns a models.User if valid or false if the email is not valid
func GetUserByName(username string) (models.User, bool) {
username = strings.ToLower(username)
return db.GetUserByName(username)
}
// SaveUser saves a user to the database. If isNewUser is true, a new Id will be generated
func SaveUser(user models.User, isNewUser bool) {
if user.Name == "" {
panic("username cannot be empty")
}
user.Name = strings.ToLower(user.Name)
db.SaveUser(user, isNewUser)
}
// UpdateUserLastOnline writes the last online time to the database
func UpdateUserLastOnline(id int) {
db.UpdateUserLastOnline(id)
}
// DeleteUser deletes a user with the given ID
func DeleteUser(id int) {
db.DeleteUser(id)
}
// GetSuperAdmin returns the models.User data for the super admin
func GetSuperAdmin() (models.User, bool) {
users := db.GetAllUsers()
for _, user := range users {
if user.UserLevel == models.UserLevelSuperAdmin {
return user, true
}
}
return models.User{}, false
}
// EditSuperAdmin changes parameters of the super admin. If no user exists, a new superadmin will be created
// Returns an error if at least one user exists, but no superadmin
func EditSuperAdmin(username, passwordHash string) error {
user, ok := GetSuperAdmin()
if !ok {
if len(GetAllUsers()) != 0 {
return errors.New("at least one user exists, but no superadmin found")
}
newAdmin := models.User{
Name: username,
Permissions: models.UserPermissionAll,
UserLevel: models.UserLevelSuperAdmin,
Password: passwordHash,
}
db.SaveUser(newAdmin, true)
return nil
}
if username != "" {
user.Name = username
}
if passwordHash != "" {
user.Password = passwordHash
}
db.SaveUser(user, false)
return nil
}
@@ -54,6 +54,7 @@ func TestApiKeys(t *testing.T) {
newApiKey := models.ApiKey{
Id: "test",
FriendlyName: "testKey",
PublicId: "wfwefewwfefwe",
LastUsed: 1000,
Permissions: 10,
}
@@ -68,6 +69,54 @@ func TestApiKeys(t *testing.T) {
runAllTypesCompareTwoOutputs(t, func() (any, any) {
return GetApiKey("test")
}, models.ApiKey{}, false)
runAllTypesNoOutput(t, func() {
SaveApiKey(models.ApiKey{
Id: "publicTest",
PublicId: "publicId",
})
})
runAllTypesCompareOutput(t, func() any {
_, ok := GetApiKey("publicTest")
return ok
}, true)
runAllTypesCompareOutput(t, func() any {
_, ok := GetApiKeyByPublicKey("publicTest")
return ok
}, false)
runAllTypesCompareOutput(t, func() any {
_, ok := GetApiKey("publicId")
return ok
}, false)
runAllTypesCompareTwoOutputs(t, func() (any, any) {
return GetApiKeyByPublicKey("publicId")
}, "publicTest", true)
runAllTypesCompareOutput(t, func() any {
_, ok := GetSystemKey(6)
return ok
}, false)
runAllTypesNoOutput(t, func() {
SaveApiKey(models.ApiKey{
Id: "sysKey1",
PublicId: "sysKey1",
IsSystemKey: true,
Expiry: time.Now().Add(1 * time.Hour).Unix(),
UserId: 6,
})
SaveApiKey(models.ApiKey{
Id: "sysKey2",
PublicId: "sysKey2",
IsSystemKey: true,
Expiry: time.Now().Add(2 * time.Hour).Unix(),
UserId: 6,
})
})
runAllTypesCompareTwoOutputs(t, func() (any, any) {
key, ok := GetSystemKey(6)
return key.Id, ok
}, "sysKey2", true)
}
func TestE2E(t *testing.T) {
@@ -77,11 +126,11 @@ func TestE2E(t *testing.T) {
Content: []byte("test2"),
AvailableFiles: []string{"should", "not", "be", "saved"},
}
runAllTypesNoOutput(t, func() { SaveEnd2EndInfo(input) })
runAllTypesNoOutput(t, func() { SaveEnd2EndInfo(input, 3) })
input.AvailableFiles = []string{}
runAllTypesCompareOutput(t, func() any { return GetEnd2EndInfo() }, input)
runAllTypesNoOutput(t, func() { DeleteEnd2EndInfo() })
runAllTypesCompareOutput(t, func() any { return GetEnd2EndInfo() }, models.E2EInfoEncrypted{AvailableFiles: []string{}})
runAllTypesCompareOutput(t, func() any { return GetEnd2EndInfo(3) }, input)
runAllTypesNoOutput(t, func() { DeleteEnd2EndInfo(3) })
runAllTypesCompareOutput(t, func() any { return GetEnd2EndInfo(3) }, models.E2EInfoEncrypted{AvailableFiles: []string{}})
}
func TestSessions(t *testing.T) {
@@ -98,6 +147,52 @@ func TestSessions(t *testing.T) {
runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, input, true)
runAllTypesNoOutput(t, func() { DeleteAllSessions() })
runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, models.Session{}, false)
runAllTypesNoOutput(t, func() {
SaveSession("session1", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 20,
})
SaveSession("session2", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 20,
})
SaveSession("session3", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 40,
})
})
runAllTypesCompareOutput(t, func() any {
_, ok := GetSession("session1")
return ok
}, true)
runAllTypesCompareOutput(t, func() any {
_, ok := GetSession("session2")
return ok
}, true)
runAllTypesCompareOutput(t, func() any {
_, ok := GetSession("session3")
return ok
}, true)
runAllTypesNoOutput(t, func() {
DeleteAllSessionsByUser(20)
})
runAllTypesCompareOutput(t, func() any {
_, ok := GetSession("session1")
return ok
}, false)
runAllTypesCompareOutput(t, func() any {
_, ok := GetSession("session2")
return ok
}, false)
runAllTypesCompareOutput(t, func() any {
_, ok := GetSession("session3")
return ok
}, true)
}
func TestHotlinks(t *testing.T) {
@@ -166,12 +261,127 @@ func TestMetaData(t *testing.T) {
runAllTypesNoOutput(t, func() { DeleteMetaData(file.Id) })
}
func TestUsers(t *testing.T) {
runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 0)
user := models.User{
Id: 1000,
Name: "test2",
Permissions: models.UserPermissionNone,
UserLevel: models.UserLevelAdmin,
LastOnline: 1338,
Password: "1234568",
ResetPassword: true,
}
runAllTypesNoOutput(t, func() { SaveUser(user, true) })
user.Id = 1
runAllTypesCompareTwoOutputs(t, func() (any, any) {
return GetUser(1)
}, user, true)
runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 1)
user.Name = "test3"
runAllTypesNoOutput(t, func() { SaveUser(user, false) })
runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 1)
runAllTypesCompareTwoOutputs(t, func() (any, any) {
return GetUserByName("test3")
}, user, true)
runAllTypesCompareTwoOutputs(t, func() (any, any) {
return GetUserByName("TEST3")
}, user, true)
user.Name = "test4"
runAllTypesNoOutput(t, func() { SaveUser(user, true) })
var allUsersSqlite []models.User
var allUsersRedis []models.User
runAllTypesCompareOutput(t, func() any {
allUsers := GetAllUsers()
switch db.GetType() {
case dbabstraction.TypeSqlite:
allUsersSqlite = allUsers
case dbabstraction.TypeRedis:
allUsersRedis = allUsers
default:
t.Fatal("Unrecognized database type")
}
return len(GetAllUsers())
}, 2)
test.IsEqual(t, allUsersSqlite, allUsersRedis)
runAllTypesNoOutput(t, func() { UpdateUserLastOnline(1) })
runAllTypesCompareTwoOutputs(t, func() (any, any) {
retrievedUser, ok := GetUser(1)
isUpdated := time.Now().Unix()-retrievedUser.LastOnline < 5 && time.Now().Unix()-retrievedUser.LastOnline > -1
return isUpdated, ok
}, true, true)
runAllTypesNoOutput(t, func() { DeleteUser(1) })
runAllTypesCompareOutput(t, func() any {
_, ok := GetUser(1)
return ok
}, false)
user.Id = 10
user.Name = "TEST5"
runAllTypesNoOutput(t, func() { SaveUser(user, false) })
runAllTypesCompareOutput(t, func() any {
retrievedUser, _ := GetUser(10)
return retrievedUser.Name
}, "test5")
runAllTypesCompareOutput(t, func() any {
_, ok := GetSuperAdmin()
return ok
}, false)
runAllTypesCompareOutput(t, func() any {
err := EditSuperAdmin("user", "password")
return err == nil
}, false)
runAllTypesNoOutput(t, func() {
users := GetAllUsers()
for _, rUser := range users {
DeleteUser(rUser.Id)
}
})
runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 0)
runAllTypesCompareOutput(t, func() any {
return EditSuperAdmin("username", "pwhash")
}, nil)
runAllTypesCompareOutput(t, func() any {
admin, ok := GetSuperAdmin()
test.IsEqualInt(t, int(admin.Permissions), int(models.UserPermissionAll))
test.IsEqualInt(t, int(admin.UserLevel), int(models.UserLevelSuperAdmin))
test.IsEqualString(t, admin.Name, "username")
test.IsEqualString(t, admin.Password, "pwhash")
return ok
}, true)
runAllTypesNoOutput(t, func() {
err := EditSuperAdmin("username2", "")
test.IsNil(t, err)
admin, ok := GetSuperAdmin()
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, admin.Name, "username2")
test.IsEqualString(t, admin.Password, "pwhash")
})
runAllTypesNoOutput(t, func() {
err := EditSuperAdmin("", "pwhash2")
test.IsNil(t, err)
admin, ok := GetSuperAdmin()
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, admin.Name, "username2")
test.IsEqualString(t, admin.Password, "pwhash2")
})
user.Name = ""
defer test.ExpectPanic(t)
SaveUser(user, true)
}
func TestUpgrade(t *testing.T) {
runAllTypesNoOutput(t, func() { test.IsEqualBool(t, db.GetDbVersion() != 1, true) })
runAllTypesNoOutput(t, func() { db.SetDbVersion(1) })
runAllTypesNoOutput(t, func() { test.IsEqualInt(t, db.GetDbVersion(), 1) })
runAllTypesNoOutput(t, func() { Upgrade() })
runAllTypesNoOutput(t, func() { test.IsEqualInt(t, db.GetDbVersion(), db.GetSchemaVersion()) })
// runAllTypesNoOutput(t, func() { Upgrade() })
// runAllTypesNoOutput(t, func() { test.IsEqualInt(t, db.GetDbVersion(), db.GetSchemaVersion()) })
}
func TestRunGarbageCollection(t *testing.T) {
@@ -44,14 +44,16 @@ type Database interface {
// DeleteApiKey deletes an API key with the given ID
DeleteApiKey(id string)
// GetSystemKey returns the latest UI API key
GetSystemKey() (models.ApiKey, bool)
GetSystemKey(userId int) (models.ApiKey, bool)
// GetApiKeyByPublicKey returns an API key by using the public key
GetApiKeyByPublicKey(publicKey string) (string, bool)
// SaveEnd2EndInfo stores the encrypted e2e info
SaveEnd2EndInfo(info models.E2EInfoEncrypted)
SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int)
// GetEnd2EndInfo retrieves the encrypted e2e info
GetEnd2EndInfo() models.E2EInfoEncrypted
GetEnd2EndInfo(userId int) models.E2EInfoEncrypted
// DeleteEnd2EndInfo resets the encrypted e2e info
DeleteEnd2EndInfo()
DeleteEnd2EndInfo(userId int)
// GetHotlink returns the id of the file associated or false if not found
GetHotlink(id string) (string, bool)
@@ -83,6 +85,21 @@ type Database interface {
DeleteSession(id string)
// DeleteAllSessions logs all users out
DeleteAllSessions()
// DeleteAllSessionsByUser logs the specific users out
DeleteAllSessionsByUser(userId int)
// GetAllUsers returns a map with all users
GetAllUsers() []models.User
// GetUser returns a models.User if valid or false if the ID is not valid
GetUser(id int) (models.User, bool)
// GetUserByName returns a models.User if valid or false if the username is not valid
GetUserByName(email string) (models.User, bool)
// SaveUser saves a user to the database. If isNewUser is true, a new Id will be generated
SaveUser(user models.User, isNewUser bool)
// UpdateUserLastOnline writes the last online time to the database
UpdateUserLastOnline(id int)
// DeleteUser deletes a user with the given ID
DeleteUser(id int)
}
// GetNew connects to the given database and initialises it
@@ -54,7 +54,7 @@ func TestMigration(t *testing.T) {
err = os.Remove("tempfile")
test.IsNil(t, err)
dbUrl := testconfiguration.GetSqliteUrl()
dbUrl := testconfiguration.SqliteUrl
dbUrlNew := dbUrl + "2"
Do(flagparser.MigrateFlags{
Source: dbUrl,
@@ -1,8 +1,6 @@
package redis
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"github.com/forceu/gokapi/internal/helper"
@@ -20,7 +18,7 @@ type DatabaseProvider struct {
}
// DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed
const DatabaseSchemeVersion = 3
const DatabaseSchemeVersion = 4
// New returns an instance
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -94,45 +92,38 @@ func newPool(config models.DbConnection) *redigo.Pool {
// Upgrade migrates the DB to a new Gokapi version, if required
func (p DatabaseProvider) Upgrade(currentDbVersion int) {
// < 1.9.6
// < v1.9.6
if currentDbVersion < 3 {
allFiles := getAllLegacyMetaDataAndDelete(p)
for _, file := range allFiles {
p.SaveMetaData(file)
}
for _, apiKey := range p.GetAllApiKeys() {
if apiKey.HasPermissionEdit() {
apiKey.SetPermission(models.ApiPermReplace)
p.SaveApiKey(apiKey)
fmt.Println("Please update to v1.9.6 before upgrading to 2.0.0")
}
// < v2.0.0-beta
if currentDbVersion < 4 {
p.DeleteAllSessions()
apiKeys := p.GetAllApiKeys()
for _, apiKey := range apiKeys {
if apiKey.IsSystemKey {
p.DeleteApiKey(apiKey.Id)
}
}
legacyE2e := p.getLegacyE2EData()
p.SaveEnd2EndInfo(legacyE2e, 0)
p.deleteKey("e2einfo")
}
}
func getAllLegacyMetaDataAndDelete(p DatabaseProvider) map[string]models.File {
result := make(map[string]models.File)
allMetaData := p.getAllValuesWithPrefix(prefixMetaData)
for _, metaData := range allMetaData {
content, err := redigo.Bytes(metaData, nil)
helper.Check(err)
file := legacyDbToMetaData(content)
result[file.Id] = file
p.deleteKey(prefixMetaData + file.Id)
}
return result
}
func legacyDbToMetaData(input []byte) models.File {
var result models.File
buf := bytes.NewBuffer(input)
dec := gob.NewDecoder(buf)
err := dec.Decode(&result)
helper.Check(err)
return result
}
const keyDbVersion = "dbversion"
func (p DatabaseProvider) getLegacyE2EData() models.E2EInfoEncrypted {
result := models.E2EInfoEncrypted{}
value, ok := p.getHashMap("e2einfo")
if !ok {
return models.E2EInfoEncrypted{}
}
err := redigo.ScanStruct(value, &result)
helper.Check(err)
return result
}
// GetDbVersion gets the version number of the database
func (p DatabaseProvider) GetDbVersion() int {
key, _ := p.getKeyInt(keyDbVersion)
@@ -310,6 +301,22 @@ func (p DatabaseProvider) decreaseHashmapIntField(id string, field string) {
helper.Check(err)
}
func (p DatabaseProvider) setHashmapField(id string, field string, content any) {
conn := p.pool.Get()
defer conn.Close()
_, err := conn.Do("HSET", p.dbPrefix+id, field, content)
helper.Check(err)
}
func (p DatabaseProvider) getIncreasedInt(id string) int {
conn := p.pool.Get()
defer conn.Close()
result, err := conn.Do("INCR", p.dbPrefix+id)
resultInt, err2 := redigo.Int(result, err)
helper.Check(err2)
return resultInt
}
func (p DatabaseProvider) runEval(cmd string) {
conn := p.pool.Get()
defer conn.Close()
@@ -239,6 +239,61 @@ func TestApiKeys(t *testing.T) {
key, ok := dbInstance.GetApiKey("newkey")
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, key.LastUsed == 10, true)
dbInstance.SaveApiKey(models.ApiKey{
Id: "publicTest",
PublicId: "publicId",
})
_, ok = dbInstance.GetApiKey("publicTest")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetApiKey("publicId")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetApiKeyByPublicKey("publicTest")
test.IsEqualBool(t, ok, false)
keyName, ok := dbInstance.GetApiKeyByPublicKey("publicId")
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, keyName, "publicTest")
_, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, false)
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey1",
PublicId: "publicSysKey1",
IsSystemKey: true,
UserId: 5,
Expiry: time.Now().Add(time.Hour).Unix(),
})
_, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, false)
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey2",
PublicId: "publicSysKey2",
IsSystemKey: true,
UserId: 4,
Expiry: time.Now().Add(-1 * time.Hour).Unix(),
})
_, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSystemKey(5)
test.IsEqualBool(t, ok, true)
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey3",
PublicId: "publicSysKey2",
IsSystemKey: true,
UserId: 4,
Expiry: time.Now().Add(2 * time.Hour).Unix(),
})
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey4",
PublicId: "publicSysKey4",
IsSystemKey: true,
UserId: 4,
Expiry: time.Now().Add(4 * time.Hour).Unix(),
})
key, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, key.Id, "sysKey4")
test.IsEqualBool(t, key.IsSystemKey, true)
}
func TestDatabaseProvider_IncreaseDownloadCount(t *testing.T) {
@@ -291,13 +346,13 @@ func TestE2EConfig(t *testing.T) {
Content: []byte("testcontent"),
AvailableFiles: nil,
}
dbInstance.SaveEnd2EndInfo(e2econfig)
retrieved := dbInstance.GetEnd2EndInfo()
dbInstance.SaveEnd2EndInfo(e2econfig, 2)
retrieved := dbInstance.GetEnd2EndInfo(2)
test.IsEqualInt(t, retrieved.Version, 1)
test.IsEqualString(t, string(retrieved.Nonce), "testnonce")
test.IsEqualString(t, string(retrieved.Content), "testcontent")
dbInstance.DeleteEnd2EndInfo()
retrieved = dbInstance.GetEnd2EndInfo()
dbInstance.DeleteEnd2EndInfo(2)
retrieved = dbInstance.GetEnd2EndInfo(2)
test.IsEqualInt(t, retrieved.Version, 0)
}
@@ -372,6 +427,34 @@ func TestSession(t *testing.T) {
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("anothersession")
test.IsEqualBool(t, ok, false)
session = models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 20,
}
dbInstance.SaveSession("sess_user1", session)
dbInstance.SaveSession("sess_user2", session)
dbInstance.SaveSession("sess_user3", session)
session.UserId = 40
dbInstance.SaveSession("sess_user4", session)
_, ok = dbInstance.GetSession("sess_user1")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSession("sess_user2")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSession("sess_user3")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSession("sess_user4")
test.IsEqualBool(t, ok, true)
dbInstance.DeleteAllSessionsByUser(20)
_, ok = dbInstance.GetSession("sess_user1")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("sess_user2")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("sess_user3")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("sess_user4")
test.IsEqualBool(t, ok, true)
}
func TestMetaData(t *testing.T) {
@@ -438,3 +521,85 @@ func TestGetAllMetaDataIds(t *testing.T) {
defer test.ExpectPanic(t)
_ = instance.GetAllMetaDataIds()
}
func TestUsers(t *testing.T) {
instance, err := New(config)
test.IsNil(t, err)
users := instance.GetAllUsers()
test.IsEqualInt(t, len(users), 0)
user := models.User{
Id: 2,
Name: "test",
Permissions: models.UserPermissionAll,
UserLevel: models.UserLevelUser,
LastOnline: 1337,
Password: "123456",
ResetPassword: true,
}
instance.SaveUser(user, false)
retrievedUser, ok := instance.GetUser(2)
test.IsEqualBool(t, ok, true)
test.IsEqual(t, retrievedUser, user)
users = instance.GetAllUsers()
test.IsEqualInt(t, len(users), 1)
test.IsEqualInt(t, retrievedUser.Id, 2)
_, ok = instance.GetUser(0)
test.IsEqualBool(t, ok, false)
_, ok = instance.GetUserByName("invalid")
test.IsEqualBool(t, ok, false)
retrievedUser, ok = instance.GetUserByName("test")
test.IsEqualBool(t, ok, true)
test.IsEqual(t, retrievedUser, user)
instance.DeleteUser(2)
_, ok = instance.GetUser(2)
test.IsEqualBool(t, ok, false)
user = models.User{
Id: 1000,
Name: "test2",
Permissions: models.UserPermissionNone,
UserLevel: models.UserLevelAdmin,
LastOnline: 1338,
Password: "1234568",
ResetPassword: true,
}
instance.SaveUser(user, true)
_, ok = instance.GetUser(1000)
test.IsEqualBool(t, ok, false)
retrievedUser, ok = instance.GetUserByName("test2")
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, retrievedUser.Id == 1000, false)
user.Id = retrievedUser.Id
test.IsEqual(t, retrievedUser, user)
instance.UpdateUserLastOnline(retrievedUser.Id)
retrievedUser, ok = instance.GetUser(retrievedUser.Id)
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, time.Now().Unix()-retrievedUser.LastOnline < 5, true)
test.IsEqualBool(t, time.Now().Unix()-retrievedUser.LastOnline > -1, true)
user.Name = "test1"
instance.SaveUser(user, true)
user.Name = "test3"
instance.SaveUser(user, true)
user.Name = "test99"
user.UserLevel = models.UserLevelSuperAdmin
instance.SaveUser(user, true)
user.Name = "test0"
user.UserLevel = models.UserLevelUser
instance.SaveUser(user, true)
users = instance.GetAllUsers()
test.IsEqualInt(t, len(users), 5)
test.IsEqualString(t, users[0].Name, "test99")
test.IsEqualString(t, users[1].Name, "test2")
test.IsEqualString(t, users[2].Name, "test1")
test.IsEqualString(t, users[3].Name, "test3")
test.IsEqualString(t, users[4].Name, "test0")
_, err = dbToUser([]any{"invalid"})
test.IsNotNil(t, err)
defer instance.Close()
}
@@ -42,7 +42,7 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
}
// GetSystemKey returns the latest UI API key
func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) {
keys := p.GetAllApiKeys()
foundKey := ""
var latestExpiry int64
@@ -50,6 +50,9 @@ func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
if !key.IsSystemKey {
continue
}
if key.UserId != userId {
continue
}
if key.Expiry > latestExpiry {
foundKey = key.Id
latestExpiry = key.Expiry
@@ -61,6 +64,17 @@ func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
return keys[foundKey], true
}
// GetApiKeyByPublicKey returns an API key by using the public key
func (p DatabaseProvider) GetApiKeyByPublicKey(publicKey string) (string, bool) {
keys := p.GetAllApiKeys()
for _, key := range keys {
if key.PublicId == publicKey {
return key.Id, true
}
}
return "", false
}
// SaveApiKey saves the API key to the database
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
p.setHashMap(p.buildArgs(prefixApiKeys + apikey.Id).AddFlat(apikey))
@@ -4,19 +4,20 @@ import (
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
redigo "github.com/gomodule/redigo/redis"
"strconv"
)
const idE2EInfo = "e2einfo"
const idE2EInfo = "e2einfo:"
// SaveEnd2EndInfo stores the encrypted e2e info
func (p DatabaseProvider) SaveEnd2EndInfo(info models.E2EInfoEncrypted) {
p.setHashMap(p.buildArgs(idE2EInfo).AddFlat(info))
func (p DatabaseProvider) SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int) {
p.setHashMap(p.buildArgs(idE2EInfo + strconv.Itoa(userId)).AddFlat(info))
}
// GetEnd2EndInfo retrieves the encrypted e2e info
func (p DatabaseProvider) GetEnd2EndInfo() models.E2EInfoEncrypted {
func (p DatabaseProvider) GetEnd2EndInfo(userId int) models.E2EInfoEncrypted {
result := models.E2EInfoEncrypted{}
value, ok := p.getHashMap(idE2EInfo)
value, ok := p.getHashMap(idE2EInfo + strconv.Itoa(userId))
if !ok {
return models.E2EInfoEncrypted{}
}
@@ -26,6 +27,6 @@ func (p DatabaseProvider) GetEnd2EndInfo() models.E2EInfoEncrypted {
}
// DeleteEnd2EndInfo resets the encrypted e2e info
func (p DatabaseProvider) DeleteEnd2EndInfo() {
p.deleteKey(idE2EInfo)
func (p DatabaseProvider) DeleteEnd2EndInfo(userId int) {
p.deleteKey(idE2EInfo + strconv.Itoa(userId))
}
@@ -4,6 +4,7 @@ import (
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
redigo "github.com/gomodule/redigo/redis"
"strings"
)
const (
@@ -37,3 +38,16 @@ func (p DatabaseProvider) DeleteSession(id string) {
func (p DatabaseProvider) DeleteAllSessions() {
p.deleteAllWithPrefix(prefixSessions)
}
// DeleteAllSessionsByUser logs the specific users out
func (p DatabaseProvider) DeleteAllSessionsByUser(userId int) {
maps := p.getAllHashesWithPrefix(prefixSessions)
for k, v := range maps {
var result models.Session
err := redigo.ScanStruct(v, &result)
helper.Check(err)
if result.UserId == userId {
p.DeleteSession(strings.Replace(k, prefixSessions, "", 1))
}
}
}
@@ -0,0 +1,94 @@
package redis
import (
"cmp"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
redigo "github.com/gomodule/redigo/redis"
"slices"
"strconv"
"time"
)
const (
prefixUsers = "users:"
prefixUserIdCounter = "userid_max"
)
func dbToUser(input []any) (models.User, error) {
var result models.User
err := redigo.ScanStruct(input, &result)
if err != nil {
return models.User{}, err
}
return result, nil
}
// GetAllUsers returns a map with all users
func (p DatabaseProvider) GetAllUsers() []models.User {
var result []models.User
maps := p.getAllHashesWithPrefix(prefixUsers)
for _, v := range maps {
user, err := dbToUser(v)
helper.Check(err)
result = append(result, user)
}
return orderUsers(result)
}
func orderUsers(users []models.User) []models.User {
slices.SortFunc(users, func(a, b models.User) int {
return cmp.Or(
cmp.Compare(a.UserLevel, b.UserLevel),
cmp.Compare(b.LastOnline, a.LastOnline),
cmp.Compare(a.Name, b.Name),
)
})
return users
}
// GetUserByName returns a models.User if valid or false if the email is not valid
func (p DatabaseProvider) GetUserByName(username string) (models.User, bool) {
users := p.GetAllUsers()
for _, user := range users {
if user.Name == username {
return user, true
}
}
return models.User{}, false
}
// GetUser returns a models.User if valid or false if the ID is not valid
func (p DatabaseProvider) GetUser(id int) (models.User, bool) {
result, ok := p.getHashMap(prefixUsers + strconv.Itoa(id))
if !ok {
return models.User{}, false
}
user, err := dbToUser(result)
helper.Check(err)
return user, true
}
// SaveUser saves a user to the database. If isNewUser is true, a new Id will be generated
func (p DatabaseProvider) SaveUser(user models.User, isNewUser bool) {
if isNewUser {
id := p.getIncreasedInt(prefixUserIdCounter)
user.Id = id
} else {
counter, _ := p.getKeyInt(prefixUserIdCounter)
if counter < user.Id {
p.setKey(prefixUserIdCounter, user.Id)
}
}
p.setHashMap(p.buildArgs(prefixUsers + strconv.Itoa(user.Id)).AddFlat(user))
}
// UpdateUserLastOnline writes the last online time to the database
func (p DatabaseProvider) UpdateUserLastOnline(id int) {
p.setHashmapField(prefixUsers+strconv.Itoa(id), "LastOnline", time.Now().Unix())
}
// DeleteUser deletes a user with the given ID
func (p DatabaseProvider) DeleteUser(id int) {
p.deleteKey(prefixUsers + strconv.Itoa(id))
}
@@ -18,7 +18,7 @@ type DatabaseProvider struct {
}
// DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed
const DatabaseSchemeVersion = 6
const DatabaseSchemeVersion = 7
// New returns an instance
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -32,68 +32,41 @@ func (p DatabaseProvider) GetType() int {
// Upgrade migrates the DB to a new Gokapi version, if required
func (p DatabaseProvider) Upgrade(currentDbVersion int) {
// < v1.9.4
// there might be an instance where the LastUsedString column still exists. This reads all
// API keys, drops the table and then recreates it.
if currentDbVersion < 5 {
// Add Column LastUsedString, keeping old data
apiKeys := p.getLegacyApiKeys()
err := p.rawSqlite(`DROP TABLE "ApiKeys";
CREATE TABLE "ApiKeys" (
"Id" TEXT NOT NULL UNIQUE,
"FriendlyName" TEXT NOT NULL,
"LastUsed" INTEGER NOT NULL,
"Permissions" INTEGER NOT NULL DEFAULT 0,
"Expiry" INTEGER,
"IsSystemKey" INTEGER,
PRIMARY KEY("Id")
) WITHOUT ROWID;`)
helper.Check(err)
for _, apiKey := range apiKeys {
p.SaveApiKey(apiKey)
}
}
// < v1.9.6
if currentDbVersion < 6 {
// Add Column LastUsedString, keeping old data
err := p.rawSqlite(`DROP TABLE IF EXISTS "UploadStatus";`)
helper.Check(err)
for _, apiKey := range p.GetAllApiKeys() {
if apiKey.HasPermissionEdit() {
apiKey.SetPermission(models.ApiPermReplace)
p.SaveApiKey(apiKey)
}
}
fmt.Println("Please update to v1.9.6 before upgrading to 2.0.0")
}
}
type legacySchemaApiKeys struct {
Id string
FriendlyName string
LastUsed int64
Permissions int
}
// getLegacyApiKeys returns a map with all API keys with the legacy scheme
func (p DatabaseProvider) getLegacyApiKeys() map[string]models.ApiKey {
result := make(map[string]models.ApiKey)
rows, err := p.sqliteDb.Query("SELECT Id,FriendlyName,LastUsed,Permissions FROM ApiKeys")
helper.Check(err)
defer rows.Close()
for rows.Next() {
rowData := legacySchemaApiKeys{}
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions)
// < v2.0.0-beta
if currentDbVersion < 7 {
err := p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN UserId INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "ApiKeys" ADD COLUMN PublicId TEXT NOT NULL DEFAULT '';`)
helper.Check(err)
err = p.rawSqlite(`DELETE FROM "ApiKeys" WHERE IsSystemKey = 1`)
helper.Check(err)
err = p.rawSqlite(`ALTER TABLE "E2EConfig" ADD COLUMN UserId INTEGER NOT NULL DEFAULT 0;`)
helper.Check(err)
err = p.rawSqlite(`ALTER TABLE "FileMetaData" ADD COLUMN UserId INTEGER NOT NULL DEFAULT 0;`)
helper.Check(err)
err = p.rawSqlite(`DROP TABLE "Sessions"; CREATE TABLE "Sessions" (
"Id" TEXT NOT NULL UNIQUE,
"RenewAt" INTEGER NOT NULL,
"ValidUntil" INTEGER NOT NULL,
"UserId" INTEGER NOT NULL,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "Users" (
"Id" INTEGER NOT NULL UNIQUE,
"Name" TEXT NOT NULL UNIQUE,
"Password" TEXT,
"Permissions" INTEGER NOT NULL,
"Userlevel" INTEGER NOT NULL,
"LastOnline" INTEGER NOT NULL DEFAULT 0,
"ResetPassword" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("Id" AUTOINCREMENT)
);
DROP TABLE IF EXISTS "UploadConfig";`)
helper.Check(err)
result[rowData.Id] = models.ApiKey{
Id: rowData.Id,
FriendlyName: rowData.FriendlyName,
LastUsed: rowData.LastUsed,
Permissions: uint8(rowData.Permissions),
}
}
return result
}
// GetDbVersion gets the version number of the database
@@ -172,11 +145,14 @@ func (p DatabaseProvider) createNewDatabase() error {
"Permissions" INTEGER NOT NULL DEFAULT 0,
"Expiry" INTEGER,
"IsSystemKey" INTEGER,
"UserId" INTEGER NOT NULL,
"PublicId" TEXT NOT NULL UNIQUE ,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "E2EConfig" (
"id" INTEGER NOT NULL UNIQUE,
"Config" BLOB NOT NULL,
"UserId" INTEGER NOT NULL UNIQUE ,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE "FileMetaData" (
@@ -196,6 +172,7 @@ func (p DatabaseProvider) createNewDatabase() error {
"Encryption" BLOB NOT NULL,
"UnlimitedDownloads" INTEGER NOT NULL,
"UnlimitedTime" INTEGER NOT NULL,
"UserId" INTEGER NOT NULL,
PRIMARY KEY("Id")
);
CREATE TABLE "Hotlinks" (
@@ -207,8 +184,19 @@ func (p DatabaseProvider) createNewDatabase() error {
"Id" TEXT NOT NULL UNIQUE,
"RenewAt" INTEGER NOT NULL,
"ValidUntil" INTEGER NOT NULL,
"UserId" INTEGER NOT NULL,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "Users" (
"Id" INTEGER NOT NULL UNIQUE,
"Name" TEXT NOT NULL UNIQUE,
"Password" TEXT,
"Permissions" INTEGER NOT NULL,
"Userlevel" INTEGER NOT NULL,
"LastOnline" INTEGER NOT NULL DEFAULT 0,
"ResetPassword" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("Id" AUTOINCREMENT)
);
`
err := p.rawSqlite(sqlStmt)
if err != nil {
@@ -18,6 +18,10 @@ var config = models.DbConnection{
HostUrl: "./test/newfolder/gokapi.sqlite",
Type: 0, // dbabstraction.TypeSqlite
}
var configUpgrade = models.DbConnection{
HostUrl: "./test/newfolder/gokapi_old.sqlite",
Type: 0, // dbabstraction.TypeSqlite
}
func TestMain(m *testing.M) {
_ = os.Mkdir("test", 0777)
@@ -220,33 +224,45 @@ func TestDatabaseProvider_IncreaseDownloadCount(t *testing.T) {
}
func TestApiKey(t *testing.T) {
dbInstance.SaveApiKey(models.ApiKey{
key1 := models.ApiKey{
Id: "newkey",
FriendlyName: "New Key",
LastUsed: 100,
Permissions: 20,
})
dbInstance.SaveApiKey(models.ApiKey{
PublicId: "_n3wkey",
Expiry: 0,
IsSystemKey: false,
UserId: 5,
}
key2 := models.ApiKey{
Id: "newkey2",
FriendlyName: "New Key2",
PublicId: "_n3wkey2",
Expiry: 17362039396,
LastUsed: 200,
Permissions: 40,
IsSystemKey: true,
UserId: 10,
}
dbInstance.SaveApiKey(key1)
dbInstance.SaveApiKey(key2)
dbInstance.SaveApiKey(models.ApiKey{
Id: "expiredKey",
PublicId: "expiredKey",
FriendlyName: "expiredKey",
Expiry: 1,
})
keys := dbInstance.GetAllApiKeys()
test.IsEqualInt(t, len(keys), 2)
test.IsEqualString(t, keys["newkey"].FriendlyName, "New Key")
test.IsEqualString(t, keys["newkey"].Id, "newkey")
test.IsEqualInt64(t, keys["newkey"].LastUsed, 100)
test.IsEqualBool(t, keys["newkey"].Permissions == 20, true)
test.IsEqualInt(t, len(dbInstance.GetAllApiKeys()), 2)
test.IsEqual(t, keys["newkey"], key1)
test.IsEqual(t, keys["newkey2"], key2)
dbInstance.DeleteApiKey("newkey2")
test.IsEqualInt(t, len(dbInstance.GetAllApiKeys()), 1)
key, ok := dbInstance.GetApiKey("newkey")
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, key.FriendlyName, "New Key")
test.IsEqual(t, key, key1)
_, ok = dbInstance.GetApiKey("newkey2")
test.IsEqualBool(t, ok, false)
@@ -294,6 +310,34 @@ func TestSession(t *testing.T) {
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("anothersession")
test.IsEqualBool(t, ok, false)
session = models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 20,
}
dbInstance.SaveSession("sess_user1", session)
dbInstance.SaveSession("sess_user2", session)
dbInstance.SaveSession("sess_user3", session)
session.UserId = 40
dbInstance.SaveSession("sess_user4", session)
_, ok = dbInstance.GetSession("sess_user1")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSession("sess_user2")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSession("sess_user3")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSession("sess_user4")
test.IsEqualBool(t, ok, true)
dbInstance.DeleteAllSessionsByUser(20)
_, ok = dbInstance.GetSession("sess_user1")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("sess_user2")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("sess_user3")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetSession("sess_user4")
test.IsEqualBool(t, ok, true)
}
func TestGarbageCollectionSessions(t *testing.T) {
@@ -329,7 +373,7 @@ func TestGarbageCollectionSessions(t *testing.T) {
}
func TestEnd2EndInfo(t *testing.T) {
info := dbInstance.GetEnd2EndInfo()
info := dbInstance.GetEnd2EndInfo(4)
test.IsEqualInt(t, info.Version, 0)
test.IsEqualBool(t, info.HasBeenSetUp(), false)
@@ -338,9 +382,9 @@ func TestEnd2EndInfo(t *testing.T) {
Nonce: []byte("testNonce1"),
Content: []byte("testContent1"),
AvailableFiles: nil,
})
}, 4)
info = dbInstance.GetEnd2EndInfo()
info = dbInstance.GetEnd2EndInfo(4)
test.IsEqualInt(t, info.Version, 1)
test.IsEqualBool(t, info.HasBeenSetUp(), true)
test.IsEqualByteSlice(t, info.Nonce, []byte("testNonce1"))
@@ -352,17 +396,17 @@ func TestEnd2EndInfo(t *testing.T) {
Nonce: []byte("testNonce2"),
Content: []byte("testContent2"),
AvailableFiles: nil,
})
}, 4)
info = dbInstance.GetEnd2EndInfo()
info = dbInstance.GetEnd2EndInfo(4)
test.IsEqualInt(t, info.Version, 2)
test.IsEqualBool(t, info.HasBeenSetUp(), true)
test.IsEqualByteSlice(t, info.Nonce, []byte("testNonce2"))
test.IsEqualByteSlice(t, info.Content, []byte("testContent2"))
test.IsEqualBool(t, len(info.AvailableFiles) == 0, true)
dbInstance.DeleteEnd2EndInfo()
info = dbInstance.GetEnd2EndInfo()
dbInstance.DeleteEnd2EndInfo(4)
info = dbInstance.GetEnd2EndInfo(4)
test.IsEqualInt(t, info.Version, 0)
test.IsEqualBool(t, info.HasBeenSetUp(), false)
}
@@ -375,12 +419,14 @@ func TestUpdateTimeApiKey(t *testing.T) {
key := models.ApiKey{
Id: "key1",
FriendlyName: "key1",
PublicId: "key1",
LastUsed: 100,
}
dbInstance.SaveApiKey(key)
key = models.ApiKey{
Id: "key2",
FriendlyName: "key2",
PublicId: "key2",
LastUsed: 200,
}
dbInstance.SaveApiKey(key)
@@ -405,6 +451,61 @@ func TestUpdateTimeApiKey(t *testing.T) {
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, retrievedKey.Id, "key2")
test.IsEqualInt64(t, retrievedKey.LastUsed, 300)
dbInstance.SaveApiKey(models.ApiKey{
Id: "publicTest",
PublicId: "publicId",
})
_, ok = dbInstance.GetApiKey("publicTest")
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetApiKey("publicId")
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetApiKeyByPublicKey("publicTest")
test.IsEqualBool(t, ok, false)
keyName, ok := dbInstance.GetApiKeyByPublicKey("publicId")
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, keyName, "publicTest")
_, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, false)
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey1",
PublicId: "publicSysKey1",
IsSystemKey: true,
UserId: 5,
Expiry: time.Now().Add(time.Hour).Unix(),
})
_, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, false)
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey2",
PublicId: "publicSysKey2",
IsSystemKey: true,
UserId: 4,
Expiry: time.Now().Add(-1 * time.Hour).Unix(),
})
_, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, true)
_, ok = dbInstance.GetSystemKey(5)
test.IsEqualBool(t, ok, true)
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey3",
PublicId: "publicSysKey2",
IsSystemKey: true,
UserId: 4,
Expiry: time.Now().Add(2 * time.Hour).Unix(),
})
dbInstance.SaveApiKey(models.ApiKey{
Id: "sysKey4",
PublicId: "publicSysKey4",
IsSystemKey: true,
UserId: 4,
Expiry: time.Now().Add(4 * time.Hour).Unix(),
})
key, ok = dbInstance.GetSystemKey(4)
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, key.Id, "sysKey4")
test.IsEqualBool(t, key.IsSystemKey, true)
}
func TestParallelConnectionsWritingAndReading(t *testing.T) {
@@ -471,8 +572,100 @@ func TestParallelConnectionsReading(t *testing.T) {
wg.Wait()
}
func TestUsers(t *testing.T) {
users := dbInstance.GetAllUsers()
test.IsEqualInt(t, len(users), 0)
user := models.User{
Id: 2,
Name: "test",
Permissions: models.UserPermissionAll,
UserLevel: models.UserLevelUser,
LastOnline: 1337,
Password: "123456",
ResetPassword: true,
}
dbInstance.SaveUser(user, false)
retrievedUser, ok := dbInstance.GetUser(2)
test.IsEqualBool(t, ok, true)
test.IsEqual(t, retrievedUser, user)
users = dbInstance.GetAllUsers()
test.IsEqualInt(t, len(users), 1)
test.IsEqualInt(t, retrievedUser.Id, 2)
_, ok = dbInstance.GetUser(0)
test.IsEqualBool(t, ok, false)
_, ok = dbInstance.GetUserByName("invalid")
test.IsEqualBool(t, ok, false)
retrievedUser, ok = dbInstance.GetUserByName("test")
test.IsEqualBool(t, ok, true)
test.IsEqual(t, retrievedUser, user)
dbInstance.DeleteUser(2)
_, ok = dbInstance.GetUser(2)
test.IsEqualBool(t, ok, false)
user = models.User{
Id: 1000,
Name: "test2",
Permissions: models.UserPermissionNone,
UserLevel: models.UserLevelAdmin,
LastOnline: 1338,
Password: "1234568",
ResetPassword: true,
}
dbInstance.SaveUser(user, true)
_, ok = dbInstance.GetUser(1000)
test.IsEqualBool(t, ok, false)
retrievedUser, ok = dbInstance.GetUserByName("test2")
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, retrievedUser.Id == 1000, false)
user.Id = retrievedUser.Id
test.IsEqual(t, retrievedUser, user)
dbInstance.UpdateUserLastOnline(retrievedUser.Id)
retrievedUser, ok = dbInstance.GetUser(retrievedUser.Id)
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, time.Now().Unix()-retrievedUser.LastOnline < 5, true)
test.IsEqualBool(t, time.Now().Unix()-retrievedUser.LastOnline > -1, true)
user.Name = "test1"
dbInstance.SaveUser(user, true)
user.Name = "test3"
dbInstance.SaveUser(user, true)
user.Name = "test99"
user.UserLevel = models.UserLevelSuperAdmin
dbInstance.SaveUser(user, true)
user.Name = "test0"
user.UserLevel = models.UserLevelUser
dbInstance.SaveUser(user, true)
users = dbInstance.GetAllUsers()
test.IsEqualInt(t, len(users), 5)
test.IsEqualString(t, users[0].Name, "test99")
test.IsEqualString(t, users[1].Name, "test2")
test.IsEqualString(t, users[2].Name, "test1")
test.IsEqualString(t, users[3].Name, "test3")
test.IsEqualString(t, users[4].Name, "test0")
}
func TestDatabaseProvider_Upgrade(t *testing.T) {
dbInstance.Upgrade(0)
instance, err := New(configUpgrade)
test.IsNil(t, err)
err = instance.rawSqlite(`
DROP TABLE IF EXISTS ApiKeys;
DROP TABLE IF EXISTS E2EConfig;
DROP TABLE IF EXISTS FileMetaData;
DROP TABLE IF EXISTS Hotlinks;
DROP TABLE IF EXISTS Sessions;
DROP TABLE IF EXISTS Users;
DROP TABLE IF EXISTS UploadConfig;`)
test.IsNil(t, err)
sqliteInit, version := getSqlInitV6()
err = instance.rawSqlite(sqliteInit)
test.IsNil(t, err)
dbInstance.SetDbVersion(version)
dbInstance.Upgrade(DatabaseSchemeVersion)
}
func TestRawSql(t *testing.T) {
@@ -481,3 +674,57 @@ func TestRawSql(t *testing.T) {
defer test.ExpectPanic(t)
_ = dbInstance.rawSqlite("Select * from Sessions")
}
func getSqlInitV6() (string, int) {
return `CREATE TABLE IF NOT EXISTS "ApiKeys" (
"Id" TEXT NOT NULL UNIQUE,
"FriendlyName" TEXT NOT NULL,
"LastUsed" INTEGER NOT NULL,
"Permissions" INTEGER NOT NULL DEFAULT 0,
"Expiry" INTEGER,
"IsSystemKey" INTEGER,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS "E2EConfig" (
"id" INTEGER NOT NULL UNIQUE,
"Config" BLOB NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "FileMetaData" (
"Id" TEXT NOT NULL UNIQUE,
"Name" TEXT NOT NULL,
"Size" TEXT NOT NULL,
"SHA1" TEXT NOT NULL,
"ExpireAt" INTEGER NOT NULL,
"SizeBytes" INTEGER NOT NULL,
"ExpireAtString" TEXT NOT NULL,
"DownloadsRemaining" INTEGER NOT NULL,
"DownloadCount" INTEGER NOT NULL,
"PasswordHash" TEXT NOT NULL,
"HotlinkId" TEXT NOT NULL,
"ContentType" TEXT NOT NULL,
"AwsBucket" TEXT NOT NULL,
"Encryption" BLOB NOT NULL,
"UnlimitedDownloads" INTEGER NOT NULL,
"UnlimitedTime" INTEGER NOT NULL,
PRIMARY KEY("Id")
);
CREATE TABLE IF NOT EXISTS "Hotlinks" (
"Id" TEXT NOT NULL UNIQUE,
"FileId" TEXT NOT NULL UNIQUE,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS "Sessions" (
"Id" TEXT NOT NULL UNIQUE,
"RenewAt" INTEGER NOT NULL,
"ValidUntil" INTEGER NOT NULL,
PRIMARY KEY("Id")
) WITHOUT ROWID;
INSERT INTO "ApiKeys" VALUES ('E9xZ1DEOclzKgxPNoyldlmCpWsHmPF','Internal System Key',1736202872,63,1736375583,1);
INSERT INTO "ApiKeys" VALUES ('UTODvOEqqjAs5cpvJK77opuGdegUSP','Unnamed key',0,23,0,0);
INSERT INTO "E2EConfig" VALUES (1,X'537f03010110453245496e666f456e6372797074656401ff80000104010756657273696f6e01040001054e6f6e6365010a000107436f6e74656e74010a00010e417661696c61626c6546696c657301ff8200000016ff81020101085b5d737472696e6701ff8200010c0000fff4ff800102010cd342c099f1bf4493012c109f01ffde0a11bcd7feac15b16db121f77c8f2105972aee4cc734af6cdd99d84b7c32deeb04ecd59bd307145ae0b389139d30a2ed6c7b4927c5910405912a0ec50d1480bee1a7014b13bbf4fe25b1d8973235e2270d4adf3003aa648171d4b3de36d91bc4380653b3f37940da018230c2f46e8dc646526cbbb3c2a898509121a4bd129689ff7143633d506e8de308d2489888dd4d9805f25d04332e45f7514c339065bc5c445a0779bf21aeaf7c8fbd210d31ce26f078ab8619df0814112bf443b9064ade8054f4aa7a2b3f5bb23df6a40abae83a5f44944121eed39fbdc608dab40200');
INSERT INTO "FileMetaData" VALUES ('M3dEz99HKN9sOgU','kodi_crashlog-20241106_102509.log','131.6 kB','0e9c019ec2698587cc973a9ee368713eb77e4fae',1737412393,134794,'2025-01-20 23:33',10,0,'','','text/x-log','',X'5f7f0301010e456e6372797074696f6e496e666f01ff80000104010b4973456e6372797074656401020001134973456e64546f456e64456e63727970746564010200010d44656372797074696f6e4b6579010a0001054e6f6e6365010a00000003ff8000',0,0);
INSERT INTO "FileMetaData" VALUES ('b5Mf07AgTkwqpW2','Encrypted File','131.6 kB','e2e-ivCiN4YePueE1PcjYirB',1737412472,134938,'2025-01-20 23:34',10,0,'','','application/octet-stream','',X'60ff830301010e456e6372797074696f6e496e666f01ff84000104010b4973456e6372797074656401020001134973456e64546f456e64456e63727970746564010200010d44656372797074696f6e4b6579010a0001054e6f6e6365010a00000007ff840101010100',0,0);
INSERT INTO "Hotlinks" VALUES ('Phie2AiW2aecaecahWoo','jun9keeNokae9iehinee');
INSERT INTO "Sessions" VALUES ('zMUYkok9UZZiKBCHB5pO7KPTPzPP71ashpRf11W37wP0HMhMjTKcFL8Ai6Z3',173624606799,173879486799);`, 6
}
@@ -15,6 +15,8 @@ type schemaApiKeys struct {
Permissions int
Expiry int64
IsSystemKey int
UserId int
PublicId string
}
// currentTime is used in order to modify the current time for testing purposes in unit tests
@@ -31,15 +33,17 @@ func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
defer rows.Close()
for rows.Next() {
rowData := schemaApiKeys{}
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions, &rowData.Expiry, &rowData.IsSystemKey)
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions, &rowData.Expiry, &rowData.IsSystemKey, &rowData.UserId, &rowData.PublicId)
helper.Check(err)
result[rowData.Id] = models.ApiKey{
Id: rowData.Id,
PublicId: rowData.PublicId,
FriendlyName: rowData.FriendlyName,
LastUsed: rowData.LastUsed,
Permissions: uint8(rowData.Permissions),
Permissions: models.ApiPermission(rowData.Permissions),
Expiry: rowData.Expiry,
IsSystemKey: rowData.IsSystemKey == 1,
UserId: rowData.UserId,
}
}
return result
@@ -49,7 +53,7 @@ func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE Id = ?", id)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey, &rowResult.UserId, &rowResult.PublicId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.ApiKey{}, false
@@ -60,21 +64,23 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
result := models.ApiKey{
Id: rowResult.Id,
PublicId: rowResult.PublicId,
FriendlyName: rowResult.FriendlyName,
LastUsed: rowResult.LastUsed,
Permissions: uint8(rowResult.Permissions),
Permissions: models.ApiPermission(rowResult.Permissions),
Expiry: rowResult.Expiry,
IsSystemKey: rowResult.IsSystemKey == 1,
UserId: rowResult.UserId,
}
return result, true
}
// GetSystemKey returns the latest UI API key
func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE IsSystemKey = 1 ORDER BY Expiry DESC LIMIT 1")
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey)
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE IsSystemKey = 1 AND UserId = ? ORDER BY Expiry DESC LIMIT 1", userId)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey, &rowResult.UserId, &rowResult.PublicId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.ApiKey{}, false
@@ -85,23 +91,40 @@ func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
result := models.ApiKey{
Id: rowResult.Id,
PublicId: rowResult.PublicId,
FriendlyName: rowResult.FriendlyName,
LastUsed: rowResult.LastUsed,
Permissions: uint8(rowResult.Permissions),
Permissions: models.ApiPermission(rowResult.Permissions),
Expiry: rowResult.Expiry,
IsSystemKey: rowResult.IsSystemKey == 1,
UserId: rowResult.UserId,
}
return result, true
}
// GetApiKeyByPublicKey returns an API key by using the public key
func (p DatabaseProvider) GetApiKeyByPublicKey(publicKey string) (string, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT Id FROM ApiKeys WHERE PublicId = ? LIMIT 1", publicKey)
err := row.Scan(&rowResult.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", false
}
helper.Check(err)
return "", false
}
return rowResult.Id, true
}
// SaveApiKey saves the API key to the database
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
isSystemKey := 0
if apikey.IsSystemKey {
isSystemKey = 1
}
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions, Expiry, IsSystemKey) VALUES (?, ?, ?, ?, ?, ?)",
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions, apikey.Expiry, isSystemKey)
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions, Expiry, IsSystemKey, UserId, PublicId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions, apikey.Expiry, isSystemKey, apikey.UserId, apikey.PublicId)
helper.Check(err)
}
@@ -12,31 +12,27 @@ import (
type schemaE2EConfig struct {
Id int64
Config []byte
UserId int
}
// SaveEnd2EndInfo stores the encrypted e2e info
func (p DatabaseProvider) SaveEnd2EndInfo(info models.E2EInfoEncrypted) {
func (p DatabaseProvider) SaveEnd2EndInfo(info models.E2EInfoEncrypted, userId int) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(info)
helper.Check(err)
newData := schemaE2EConfig{
Id: 1,
Config: buf.Bytes(),
}
_, err = p.sqliteDb.Exec("INSERT OR REPLACE INTO E2EConfig (id, Config) VALUES (?, ?)",
newData.Id, newData.Config)
_, err = p.sqliteDb.Exec("INSERT OR REPLACE INTO E2EConfig ( Config, UserId) VALUES ( ?, ?)",
buf.Bytes(), userId)
helper.Check(err)
}
// GetEnd2EndInfo retrieves the encrypted e2e info
func (p DatabaseProvider) GetEnd2EndInfo() models.E2EInfoEncrypted {
func (p DatabaseProvider) GetEnd2EndInfo(userId int) models.E2EInfoEncrypted {
result := models.E2EInfoEncrypted{}
rowResult := schemaE2EConfig{}
row := p.sqliteDb.QueryRow("SELECT Config FROM E2EConfig WHERE id = 1")
row := p.sqliteDb.QueryRow("SELECT Config FROM E2EConfig WHERE UserId = ?", userId)
err := row.Scan(&rowResult.Config)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@@ -54,8 +50,7 @@ func (p DatabaseProvider) GetEnd2EndInfo() models.E2EInfoEncrypted {
}
// DeleteEnd2EndInfo resets the encrypted e2e info
func (p DatabaseProvider) DeleteEnd2EndInfo() {
//goland:noinspection SqlWithoutWhere
_, err := p.sqliteDb.Exec("DELETE FROM E2EConfig")
func (p DatabaseProvider) DeleteEnd2EndInfo(userId int) {
_, err := p.sqliteDb.Exec("DELETE FROM E2EConfig WHERE UserId = ?", userId)
helper.Check(err)
}
@@ -26,6 +26,7 @@ type schemaMetaData struct {
Encryption []byte
UnlimitedDownloads int
UnlimitedTime int
UserId int
}
func (rowData schemaMetaData) ToFileModel() (models.File, error) {
@@ -46,6 +47,7 @@ func (rowData schemaMetaData) ToFileModel() (models.File, error) {
Encryption: models.EncryptionInfo{},
UnlimitedDownloads: rowData.UnlimitedDownloads == 1,
UnlimitedTime: rowData.UnlimitedTime == 1,
UserId: rowData.UserId,
}
buf := bytes.NewBuffer(rowData.Encryption)
@@ -56,9 +58,6 @@ func (rowData schemaMetaData) ToFileModel() (models.File, error) {
// GetAllMetadata returns a map of all available files
func (p DatabaseProvider) GetAllMetadata() map[string]models.File {
if p.sqliteDb == nil {
panic("Database not loaded!")
}
result := make(map[string]models.File)
rows, err := p.sqliteDb.Query("SELECT * FROM FileMetaData")
helper.Check(err)
@@ -68,7 +67,7 @@ func (p DatabaseProvider) GetAllMetadata() map[string]models.File {
err = rows.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes,
&rowData.ExpireAtString, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash,
&rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption,
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime)
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId)
helper.Check(err)
var metaData models.File
metaData, err = rowData.ToFileModel()
@@ -80,9 +79,6 @@ func (p DatabaseProvider) GetAllMetadata() map[string]models.File {
// GetAllMetaDataIds returns all Ids that contain metadata
func (p DatabaseProvider) GetAllMetaDataIds() []string {
if p.sqliteDb == nil {
panic("Database not loaded!")
}
keys := make([]string, 0)
rows, err := p.sqliteDb.Query("SELECT Id FROM FileMetaData")
helper.Check(err)
@@ -105,7 +101,7 @@ func (p DatabaseProvider) GetMetaDataById(id string) (models.File, bool) {
err := row.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes,
&rowData.ExpireAtString, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash,
&rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption,
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime)
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return result, false
@@ -134,6 +130,7 @@ func (p DatabaseProvider) SaveMetaData(file models.File) {
HotlinkId: file.HotlinkId,
ContentType: file.ContentType,
AwsBucket: file.AwsBucket,
UserId: file.UserId,
}
if file.UnlimitedDownloads {
@@ -151,10 +148,10 @@ func (p DatabaseProvider) SaveMetaData(file models.File) {
_, err = p.sqliteDb.Exec(`INSERT OR REPLACE INTO FileMetaData (Id, Name, Size, SHA1, ExpireAt, SizeBytes, ExpireAtString,
DownloadsRemaining, DownloadCount, PasswordHash, HotlinkId, ContentType, AwsBucket, Encryption,
UnlimitedDownloads, UnlimitedTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
UnlimitedDownloads, UnlimitedTime, UserId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
newData.Id, newData.Name, newData.Size, newData.SHA1, newData.ExpireAt, newData.SizeBytes, newData.ExpireAtString,
newData.DownloadsRemaining, newData.DownloadCount, newData.PasswordHash, newData.HotlinkId, newData.ContentType,
newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime)
newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime, newData.UserId)
helper.Check(err)
}
@@ -12,13 +12,14 @@ type schemaSessions struct {
Id string
RenewAt int64
ValidUntil int64
UserId int
}
// GetSession returns the session with the given ID or false if not a valid ID
func (p DatabaseProvider) GetSession(id string) (models.Session, bool) {
var rowResult schemaSessions
row := p.sqliteDb.QueryRow("SELECT * FROM Sessions WHERE Id = ?", id)
err := row.Scan(&rowResult.Id, &rowResult.RenewAt, &rowResult.ValidUntil)
err := row.Scan(&rowResult.Id, &rowResult.RenewAt, &rowResult.ValidUntil, &rowResult.UserId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.Session{}, false
@@ -29,6 +30,7 @@ func (p DatabaseProvider) GetSession(id string) (models.Session, bool) {
result := models.Session{
RenewAt: rowResult.RenewAt,
ValidUntil: rowResult.ValidUntil,
UserId: rowResult.UserId,
}
return result, true
}
@@ -39,10 +41,11 @@ func (p DatabaseProvider) SaveSession(id string, session models.Session) {
Id: id,
RenewAt: session.RenewAt,
ValidUntil: session.ValidUntil,
UserId: session.UserId,
}
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Sessions (Id, RenewAt, ValidUntil) VALUES (?, ?, ?)",
newData.Id, newData.RenewAt, newData.ValidUntil)
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Sessions (Id, RenewAt, ValidUntil, UserId) VALUES (?, ?, ?, ?)",
newData.Id, newData.RenewAt, newData.ValidUntil, newData.UserId)
helper.Check(err)
}
@@ -59,6 +62,12 @@ func (p DatabaseProvider) DeleteAllSessions() {
helper.Check(err)
}
// DeleteAllSessionsByUser logs the specific users out
func (p DatabaseProvider) DeleteAllSessionsByUser(userId int) {
_, err := p.sqliteDb.Exec("DELETE FROM Sessions WHERE UserId = ?", userId)
helper.Check(err)
}
func (p DatabaseProvider) cleanExpiredSessions() {
_, err := p.sqliteDb.Exec("DELETE FROM Sessions WHERE Sessions.ValidUntil < ?", time.Now().Unix())
helper.Check(err)
@@ -0,0 +1,110 @@
package sqlite
import (
"database/sql"
"errors"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"time"
)
type schemaUser struct {
Id int
Name string
Password sql.NullString
Permissions models.UserPermission
UserLevel models.UserRank
LastOnline int64
ResetPassword int
}
func (s schemaUser) ToUser() models.User {
pw := ""
if s.Password.Valid {
pw = s.Password.String
}
return models.User{
Id: s.Id,
Name: s.Name,
Permissions: s.Permissions,
UserLevel: s.UserLevel,
LastOnline: s.LastOnline,
Password: pw,
ResetPassword: s.ResetPassword == 1,
}
}
// GetAllUsers returns a map with all users
func (p DatabaseProvider) GetAllUsers() []models.User {
var result []models.User
rows, err := p.sqliteDb.Query("SELECT * FROM Users ORDER BY Userlevel ASC, LastOnline DESC, Name ASC")
helper.Check(err)
defer rows.Close()
for rows.Next() {
row := schemaUser{}
err = rows.Scan(&row.Id, &row.Name, &row.Password, &row.Permissions, &row.UserLevel, &row.LastOnline, &row.ResetPassword)
helper.Check(err)
result = append(result, row.ToUser())
}
return result
}
func (p DatabaseProvider) getUserWithConstraint(isName bool, searchValue any) (models.User, bool) {
rowResult := schemaUser{}
query := "SELECT * FROM Users WHERE Id = ?"
if isName {
query = "SELECT * FROM Users WHERE Name = ?"
}
row := p.sqliteDb.QueryRow(query, searchValue)
err := row.Scan(&rowResult.Id, &rowResult.Name, &rowResult.Password, &rowResult.Permissions, &rowResult.UserLevel, &rowResult.LastOnline, &rowResult.ResetPassword)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.User{}, false
}
helper.Check(err)
return models.User{}, false
}
user := rowResult.ToUser()
return user, true
}
// GetUser returns a models.User if valid or false if the ID is not valid
func (p DatabaseProvider) GetUser(id int) (models.User, bool) {
return p.getUserWithConstraint(false, id)
}
// GetUserByName returns a models.User if valid or false if the name is not valid
func (p DatabaseProvider) GetUserByName(username string) (models.User, bool) {
return p.getUserWithConstraint(true, username)
}
// SaveUser saves a user to the database. If isNewUser is true, a new Id will be generated
func (p DatabaseProvider) SaveUser(user models.User, isNewUser bool) {
resetpw := 0
if user.ResetPassword {
resetpw = 1
}
if isNewUser {
_, err := p.sqliteDb.Exec("INSERT INTO Users (Name, Password, Permissions, Userlevel, LastOnline, ResetPassword) VALUES (?, ?, ?, ?, ?, ?)",
user.Name, user.Password, user.Permissions, user.UserLevel, user.LastOnline, resetpw)
helper.Check(err)
} else {
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Users (Id, Name, Password, Permissions, Userlevel, LastOnline, ResetPassword) VALUES (?, ?, ?, ?, ?, ?, ?)",
user.Id, user.Name, user.Password, user.Permissions, user.UserLevel, user.LastOnline, resetpw)
helper.Check(err)
}
}
// UpdateUserLastOnline writes the last online time to the database
func (p DatabaseProvider) UpdateUserLastOnline(id int) {
timeNow := time.Now().Unix()
// To reduce database writes, the entry is only updated, if the last timestamp is more than 30 seconds old
_, err := p.sqliteDb.Exec("UPDATE Users SET LastOnline= ? WHERE Id = ? AND (? - LastOnline > 30)", timeNow, id, timeNow)
helper.Check(err)
}
// DeleteUser deletes a user with the given ID
func (p DatabaseProvider) DeleteUser(id int) {
_, err := p.sqliteDb.Exec("DELETE FROM Users WHERE Id = ?", id)
helper.Check(err)
}
@@ -1,8 +1,9 @@
// Code generated by updateProtectedUrls.go - DO NOT EDIT.
package setup
// Do not modify: This is an automatically generated File.
// Do not modify: This is an automatically generated file created by updateProtectedUrls.go
// It contains all URLs that need to be protected when using an external authentication.
// protectedUrls contains a list of URLs that need to be protected if authentication is disabled.
// This list will be displayed during the setup
var protectedUrls = []string{"/admin", "/apiKeys", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadComplete", "/uploadStatus"}
var protectedUrls = []string{"/admin", "/apiKeys", "/changePassword", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadStatus", "/users"}
+63 -50
View File
@@ -49,8 +49,8 @@ var templateFolderEmbedded embed.FS
var srv http.Server
var isInitialSetup = true
var username string
var password string
var credentialUsername string
var credentialPassword string
// debugDisableAuth can be set to true for testing purposes. It will disable the
// password requirement for accessing the setup page
@@ -67,13 +67,13 @@ func RunIfFirstStart() {
// RunConfigModification starts a blocking webserver for reconfiguration setup
func RunConfigModification() {
isInitialSetup = false
username = helper.GenerateRandomString(6)
password = helper.GenerateRandomString(10)
credentialUsername = helper.GenerateRandomString(6)
credentialPassword = helper.GenerateRandomString(10)
fmt.Println()
fmt.Println("###################################################################")
fmt.Println("Use the following credentials for modifying the configuration:")
fmt.Println("Username: " + username)
fmt.Println("Password: " + password)
fmt.Println("Username: " + credentialUsername)
fmt.Println("Password: " + credentialPassword)
fmt.Println("###################################################################")
fmt.Println()
startSetupWebserver()
@@ -90,8 +90,8 @@ func basicAuth(next http.HandlerFunc) http.HandlerFunc {
enteredUser, enteredPw, ok := r.BasicAuth()
if ok {
usernameMatch := authentication.IsEqualStringConstantTime(enteredUser, username)
passwordMatch := authentication.IsEqualStringConstantTime(enteredPw, password)
usernameMatch := authentication.IsEqualStringConstantTime(enteredUser, credentialUsername)
passwordMatch := authentication.IsEqualStringConstantTime(enteredPw, credentialPassword)
if usernameMatch && passwordMatch {
next.ServeHTTP(w, r)
return
@@ -224,7 +224,16 @@ func getFormValueInt(formObjects *[]jsonFormObject, key string) (int, error) {
return result, nil
}
func toConfiguration(formObjects *[]jsonFormObject) (models.Configuration, *cloudconfig.CloudConfig, configuration.End2EndReconfigParameters, error) {
type authSettings struct {
UserInternalAuth string
UserOAuth string
UserHeader string
PasswordInternalAuth string
OnlyRegisteredUsersOAuth bool
OnlyRegisteredUsersHeader bool
}
func toConfiguration(formObjects *[]jsonFormObject) (models.Configuration, *cloudconfig.CloudConfig, configuration.End2EndReconfigParameters, authSettings, error) {
var err error
var e2eConfig configuration.End2EndReconfigParameters
parsedEnv := environment.New()
@@ -239,6 +248,7 @@ func toConfiguration(formObjects *[]jsonFormObject) (models.Configuration, *clou
ConfigVersion: configupgrade.CurrentConfigVersion,
Authentication: models.AuthenticationConfig{},
}
authInfo := authSettings{}
if isInitialSetup {
result.Authentication.SaltFiles = helper.GenerateRandomString(30)
@@ -249,41 +259,54 @@ func toConfiguration(formObjects *[]jsonFormObject) (models.Configuration, *clou
err = parseDatabaseSettings(&result, formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
err = parseBasicAuthSettings(&result, formObjects)
err = parseBasicAuthSettings(&result, &authInfo, formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
err = parseOAuthSettings(&result, formObjects)
err = parseOAuthSettings(&result, &authInfo, formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
err = parseHeaderAuthSettings(&result, formObjects)
err = parseHeaderAuthSettings(&result, &authInfo, formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
err = parseServerSettings(&result, formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
e2eConfig, err = parseEncryptionAndDelete(&result, formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
var cloudSettings *cloudconfig.CloudConfig
cloudSettings, err = parseCloudSettings(formObjects)
if err != nil {
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, err
return models.Configuration{}, nil, configuration.End2EndReconfigParameters{}, authSettings{}, err
}
return result, cloudSettings, e2eConfig, nil
switch result.Authentication.Method {
case models.AuthenticationInternal:
result.Authentication.Username = authInfo.UserInternalAuth
case models.AuthenticationOAuth2:
result.Authentication.Username = authInfo.UserOAuth
result.Authentication.OnlyRegisteredUsers = authInfo.OnlyRegisteredUsersOAuth
case models.AuthenticationHeader:
result.Authentication.Username = authInfo.UserHeader
result.Authentication.OnlyRegisteredUsers = authInfo.OnlyRegisteredUsersHeader
case models.AuthenticationDisabled:
result.Authentication.Username = "admin@gokapi"
}
return result, cloudSettings, e2eConfig, authInfo, nil
}
func parseDatabaseSettings(result *models.Configuration, formObjects *[]jsonFormObject) error {
@@ -360,12 +383,12 @@ func checkForAllDbValues(formObjects *[]jsonFormObject) error {
return err
}
func parseBasicAuthSettings(result *models.Configuration, formObjects *[]jsonFormObject) error {
var err error
result.Authentication.Username, err = getFormValueString(formObjects, "auth_username")
func parseBasicAuthSettings(result *models.Configuration, authInfo *authSettings, formObjects *[]jsonFormObject) error {
username, err := getFormValueString(formObjects, "auth_username")
if err != nil {
return err
}
authInfo.UserInternalAuth = username
pw, err := getFormValueString(formObjects, "auth_pw")
if err != nil {
@@ -373,14 +396,16 @@ func parseBasicAuthSettings(result *models.Configuration, formObjects *[]jsonFor
}
// Password is not displayed in reconf setup, but a placeholder "unc". If this is submitted as a password, the
// old password is kept
if isInitialSetup || pw != "unc" {
if isInitialSetup {
result.Authentication.SaltAdmin = helper.GenerateRandomString(30)
result.Authentication.Password = configuration.HashPasswordCustomSalt(pw, result.Authentication.SaltAdmin)
}
if isInitialSetup || pw != "unc" {
authInfo.PasswordInternalAuth = configuration.HashPasswordCustomSalt(pw, result.Authentication.SaltAdmin)
}
return nil
}
func parseOAuthSettings(result *models.Configuration, formObjects *[]jsonFormObject) error {
func parseOAuthSettings(result *models.Configuration, authInfo *authSettings, formObjects *[]jsonFormObject) error {
var err error
result.Authentication.OAuthProvider, err = getFormValueString(formObjects, "oauth_provider")
if err != nil {
@@ -397,32 +422,21 @@ func parseOAuthSettings(result *models.Configuration, formObjects *[]jsonFormObj
return err
}
restrictUsers, err := getFormValueBool(formObjects, "oauth_restrict_users")
if err != nil {
return err
}
result.Authentication.OAuthRecheckInterval, err = getFormValueInt(formObjects, "oauth_recheck_interval")
if err != nil {
return err
}
scopeUsers, err := getFormValueString(formObjects, "oauth_scope_users")
username, err := getFormValueString(formObjects, "oauth_admin_user")
if err != nil {
return err
}
authInfo.UserOAuth = username
oauthAllowedUsers, err := getFormValueString(formObjects, "oauth_allowed_users")
authInfo.OnlyRegisteredUsersOAuth, err = getFormValueBool(formObjects, "oauth_only_registered_users")
if err != nil {
return err
}
if restrictUsers {
result.Authentication.OAuthUserScope = scopeUsers
result.Authentication.OAuthUsers = splitAndTrim(oauthAllowedUsers)
} else {
result.Authentication.OAuthUsers = []string{}
result.Authentication.OAuthUserScope = ""
}
restrictGroups, err := getFormValueBool(formObjects, "oauth_restrict_groups")
if err != nil {
@@ -447,18 +461,21 @@ func parseOAuthSettings(result *models.Configuration, formObjects *[]jsonFormObj
return nil
}
func parseHeaderAuthSettings(result *models.Configuration, formObjects *[]jsonFormObject) error {
var err error
func parseHeaderAuthSettings(result *models.Configuration, authInfo *authSettings, formObjects *[]jsonFormObject) error {
username, err := getFormValueString(formObjects, "auth_header_admin")
if err != nil {
return err
}
authInfo.UserHeader = username
result.Authentication.HeaderKey, err = getFormValueString(formObjects, "auth_headerkey")
if err != nil {
return err
}
headerAllowedUsers, err := getFormValueString(formObjects, "auth_header_users")
authInfo.OnlyRegisteredUsersHeader, err = getFormValueBool(formObjects, "auth_header_only_registered_users")
if err != nil {
return err
}
result.Authentication.HeaderUsers = splitAndTrim(headerAllowedUsers)
return nil
}
@@ -681,9 +698,7 @@ type setupView struct {
IsDataNotMounted bool
IsConfigNotMounted bool
Port int
OAuthUsers string
OAuthGroups string
HeaderUsers string
Auth models.AuthenticationConfig
Settings models.Configuration
CloudSettings cloudconfig.CloudConfig
@@ -708,9 +723,7 @@ func (v *setupView) loadFromConfig() {
v.Settings = *settings
v.Auth = settings.Authentication
v.CloudSettings, _ = cloudconfig.Load()
v.OAuthUsers = strings.Join(settings.Authentication.OAuthUsers, ";")
v.OAuthGroups = strings.Join(settings.Authentication.OAuthGroups, ";")
v.HeaderUsers = strings.Join(settings.Authentication.HeaderUsers, ";")
if strings.Contains(settings.Port, "localhost") || strings.Contains(settings.Port, "127.0.0.1") {
v.LocalhostOnly = true
@@ -771,12 +784,12 @@ func handleResult(w http.ResponseWriter, r *http.Request) {
return
}
newConfig, cloudSettings, e2eConfig, err := toConfiguration(&setupResult)
newConfig, cloudSettings, e2eConfig, authInfo, err := toConfiguration(&setupResult)
if err != nil {
outputError(w, err)
return
}
configuration.LoadFromSetup(newConfig, cloudSettings, e2eConfig)
configuration.LoadFromSetup(newConfig, cloudSettings, e2eConfig, authInfo.PasswordInternalAuth)
w.WriteHeader(200)
_, _ = w.Write([]byte("{ \"result\": \"OK\"}"))
go func() {
+74 -85
View File
@@ -13,7 +13,6 @@ import (
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/test"
"github.com/forceu/gokapi/internal/test/testconfiguration"
"github.com/forceu/gokapi/internal/webserver/authentication"
"log"
"net"
"net/http"
@@ -64,7 +63,7 @@ func TestMissingSetupValues(t *testing.T) {
for _, invalid := range invalidInputs {
formObjects, err := invalid.toFormObject()
test.IsNil(t, err)
_, _, _, err = toConfiguration(&formObjects)
_, _, _, _, err = toConfiguration(&formObjects)
test.IsNotNilWithMessage(t, err, invalid.toJson())
}
}
@@ -75,7 +74,7 @@ func TestEncryptionSetup(t *testing.T) {
input.EncryptionLevel.Value = "1"
formObjects, err := input.toFormObject()
test.IsNil(t, err)
config, _, e2eConfig, err = toConfiguration(&formObjects)
config, _, e2eConfig, _, err = toConfiguration(&formObjects)
test.IsNil(t, err)
test.IsEqualInt(t, len(config.Encryption.Cipher), 32)
test.IsEqualString(t, config.Encryption.Checksum, "")
@@ -86,7 +85,7 @@ func TestEncryptionSetup(t *testing.T) {
input.EncryptionPassword.Value = "testpw12"
formObjects, err = input.toFormObject()
test.IsNil(t, err)
config, _, _, err = toConfiguration(&formObjects)
config, _, _, _, err = toConfiguration(&formObjects)
test.IsNil(t, err)
test.IsEqualString(t, string(config.Encryption.Cipher), "")
test.IsEqualInt(t, len(config.Encryption.Checksum), 64)
@@ -103,7 +102,7 @@ func TestEncryptionSetup(t *testing.T) {
test.IsEqualBool(t, file.UnlimitedTime, true)
formObjects, err = input.toFormObject()
test.IsNil(t, err)
config, _, e2eConfig, err = toConfiguration(&formObjects)
config, _, e2eConfig, _, err = toConfiguration(&formObjects)
test.IsNil(t, err)
test.IsEqualBool(t, e2eConfig.DeleteEncryptedStorage, true)
test.IsEqualBool(t, e2eConfig.DeleteEnd2EndEncryption, false)
@@ -115,7 +114,7 @@ func TestEncryptionSetup(t *testing.T) {
test.IsEqualBool(t, ok, true)
formObjects, err = input.toFormObject()
test.IsNil(t, err)
config, _, _, err = toConfiguration(&formObjects)
config, _, _, _, err = toConfiguration(&formObjects)
test.IsNil(t, err)
file, ok = database.GetMetaDataById(id)
test.IsEqualBool(t, ok, true)
@@ -130,7 +129,7 @@ func TestEncryptionSetup(t *testing.T) {
test.IsEqualBool(t, ok, true)
formObjects, err = input.toFormObject()
test.IsNil(t, err)
config, _, e2eConfig, err = toConfiguration(&formObjects)
config, _, e2eConfig, _, err = toConfiguration(&formObjects)
test.IsNil(t, err)
test.IsEqualBool(t, e2eConfig.DeleteEncryptedStorage, true)
test.IsEqualBool(t, e2eConfig.DeleteEnd2EndEncryption, false)
@@ -155,13 +154,13 @@ var config = models.Configuration{
}
func TestToConfiguration(t *testing.T) {
output, cloudConfig, _, err := toConfiguration(&jsonForms)
output, cloudConfig, _, authsettings, err := toConfiguration(&jsonForms)
test.IsNil(t, err)
test.IsEqualInt(t, output.Authentication.Method, authentication.Internal)
test.IsEqualInt(t, output.Authentication.Method, models.AuthenticationInternal)
test.IsEqualString(t, cloudConfig.Aws.KeyId, "testapi")
test.IsEqualString(t, output.Authentication.Username, "admin")
test.IsNotEqualString(t, output.Authentication.Password, "adminadmin")
test.IsNotEqualString(t, output.Authentication.Password, "")
test.IsNotEqualString(t, authsettings.PasswordInternalAuth, "adminadmin")
test.IsNotEqualString(t, authsettings.PasswordInternalAuth, "")
test.IsEqualString(t, output.RedirectUrl, "https://github.com/Forceu/Gokapi/")
}
@@ -196,8 +195,8 @@ func TestBasicAuth(t *testing.T) {
isAuth = false
isInitialSetup = false
username = "test"
password = "testpw"
credentialUsername = "test"
credentialPassword = "testpw"
basicAuth(continueFunc).ServeHTTP(w, r)
test.IsEqualBool(t, isAuth, false)
@@ -295,8 +294,8 @@ func TestParseDatabaseSettings(t *testing.T) {
func TestRunConfigModification(t *testing.T) {
testconfiguration.Create(false)
username = ""
password = ""
credentialUsername = ""
credentialPassword = ""
finish := make(chan bool)
go func() {
waitForServer(t, true)
@@ -312,8 +311,8 @@ func TestRunConfigModification(t *testing.T) {
finish <- true
}()
RunConfigModification()
test.IsEqualInt(t, len(username), 6)
test.IsEqualInt(t, len(password), 10)
test.IsEqualInt(t, len(credentialUsername), 6)
test.IsEqualInt(t, len(credentialPassword), 10)
isInitialSetup = true
<-finish
}
@@ -375,7 +374,7 @@ func TestIntegration(t *testing.T) {
ResultCode: 500,
})
setupValues := createInputInternalAuth()
setupVals := createInputInternalAuth()
test.HttpPageResultJson(t, test.HttpTestConfig{
Url: "http://localhost:53842/setup/setupResult",
RequiredContent: []string{"\"result\": \"OK\""},
@@ -383,7 +382,7 @@ func TestIntegration(t *testing.T) {
IsHtml: false,
Method: "POST",
ResultCode: 200,
Body: strings.NewReader(setupValues.toJson()),
Body: strings.NewReader(setupVals.toJson()),
})
waitForServer(t, false)
@@ -395,9 +394,8 @@ func TestIntegration(t *testing.T) {
test.IsEqualString(t, settings.Authentication.OAuthProvider, "")
test.IsEqualString(t, settings.Authentication.OAuthClientId, "")
test.IsEqualString(t, settings.Authentication.OAuthClientSecret, "")
test.IsEqualInt(t, len(settings.Authentication.OAuthUsers), 0)
test.IsEqualBool(t, settings.Authentication.OnlyRegisteredUsers, false)
test.IsEqualString(t, settings.Authentication.HeaderKey, "")
test.IsEqualInt(t, len(settings.Authentication.HeaderUsers), 0)
test.IsEqualBool(t, strings.Contains(settings.Port, "127.0.0.1"), true)
test.IsEqualBool(t, strings.Contains(settings.Port, ":53842"), true)
test.IsEqualBool(t, settings.UseSsl, false)
@@ -419,8 +417,8 @@ func TestIntegration(t *testing.T) {
go RunConfigModification()
waitForServer(t, true)
username = "test"
password = "testpw"
credentialUsername = "test"
credentialPassword = "testpw"
setupInput := createInputHeaderAuth()
test.HttpPageResultJson(t, test.HttpTestConfig{
@@ -468,18 +466,12 @@ func TestIntegration(t *testing.T) {
settings = configuration.Get()
test.IsEqualBool(t, settings.IncludeFilename, false)
test.IsEqualInt(t, settings.Authentication.Method, 2)
test.IsEqualString(t, settings.Authentication.Username, "")
test.IsEqualString(t, settings.Authentication.Username, "test1")
test.IsEqualString(t, settings.Authentication.OAuthProvider, "")
test.IsEqualString(t, settings.Authentication.OAuthClientId, "")
test.IsEqualString(t, settings.Authentication.OAuthClientSecret, "")
test.IsEqualInt(t, len(settings.Authentication.OAuthUsers), 0)
test.IsEqualString(t, settings.Authentication.HeaderKey, "testkey")
headerUsers := len(settings.Authentication.HeaderUsers)
test.IsEqualInt(t, headerUsers, 2)
if headerUsers == 2 {
test.IsEqualString(t, settings.Authentication.HeaderUsers[0], "test1")
test.IsEqualString(t, settings.Authentication.HeaderUsers[1], "test2")
}
test.IsEqualBool(t, settings.Authentication.OnlyRegisteredUsers, true)
test.IsEqualBool(t, strings.Contains(settings.Port, "127.0.0.1"), false)
test.IsEqualBool(t, strings.Contains(settings.Port, ":53842"), true)
test.IsEqualBool(t, settings.UseSsl, true)
@@ -496,8 +488,8 @@ func TestIntegration(t *testing.T) {
go RunConfigModification()
waitForServer(t, true)
username = "test"
password = "testpw"
credentialUsername = "test"
credentialPassword = "testpw"
setupInput = createInputOAuth()
test.HttpPageResultJson(t, test.HttpTestConfig{
@@ -518,55 +510,51 @@ func TestIntegration(t *testing.T) {
test.IsEqualString(t, settings.Authentication.OAuthProvider, "provider")
test.IsEqualString(t, settings.Authentication.OAuthClientId, "id")
test.IsEqualString(t, settings.Authentication.OAuthClientSecret, "secret")
oauthUsers := len(settings.Authentication.OAuthUsers)
test.IsEqualInt(t, oauthUsers, 2)
if oauthUsers == 2 {
test.IsEqualString(t, settings.Authentication.OAuthUsers[0], "oatest1")
test.IsEqualString(t, settings.Authentication.OAuthUsers[1], "oatest2")
}
test.IsEqualString(t, settings.Authentication.Username, "oatest1")
test.IsEqualBool(t, settings.Authentication.OnlyRegisteredUsers, true)
}
type setupValues struct {
BindLocalhost setupEntry `form:"localhost_sel" isBool:"true"`
UseSsl setupEntry `form:"ssl_sel" isBool:"true"`
SaveIp setupEntry `form:"logip_sel" isBool:"true"`
Port setupEntry `form:"port" isInt:"true"`
PublicName setupEntry `form:"public_name"`
ExtUrl setupEntry `form:"url"`
RedirectUrl setupEntry `form:"url_redirection"`
IncludeFilename setupEntry `form:"showfilename_sel" isBool:"true"`
AuthenticationMode setupEntry `form:"authentication_sel" isInt:"true"`
AuthUsername setupEntry `form:"auth_username"`
AuthPassword setupEntry `form:"auth_pw"`
OAuthProvider setupEntry `form:"oauth_provider"`
OAuthClientId setupEntry `form:"oauth_id"`
OAuthClientSecret setupEntry `form:"oauth_secret"`
OAuthAuthorisedUsers setupEntry `form:"oauth_allowed_users"`
OAuthAuthorisedGroups setupEntry `form:"oauth_allowed_groups"`
OAuthScopeUser setupEntry `form:"oauth_scope_users"`
OAuthScopeGroup setupEntry `form:"oauth_scope_groups"`
OAuthRestrictUser setupEntry `form:"oauth_restrict_users" isBool:"true"`
OAuthRestrictGroups setupEntry `form:"oauth_restrict_groups" isBool:"true"`
OAuthRecheckInterval setupEntry `form:"oauth_recheck_interval" isInt:"true"`
AuthHeaderKey setupEntry `form:"auth_headerkey"`
AuthHeaderUsers setupEntry `form:"auth_header_users"`
StorageSelection setupEntry `form:"storage_sel"`
PicturesAlwaysLocal setupEntry `form:"storage_sel_image"`
ProxyDownloads setupEntry `form:"storage_sel_proxy"`
S3Bucket setupEntry `form:"s3_bucket"`
S3Region setupEntry `form:"s3_region"`
S3ApiKey setupEntry `form:"s3_api"`
S3ApiSecret setupEntry `form:"s3_secret"`
S3Endpoint setupEntry `form:"s3_endpoint"`
EncryptionLevel setupEntry `form:"encrypt_sel" isInt:"true"`
EncryptionPassword setupEntry `form:"enc_pw"`
DatabaseType setupEntry `form:"dbtype_sel" isInt:"true"`
SqliteLocation setupEntry `form:"sqlite_location"`
RedisLocation setupEntry `form:"redis_location"`
RedisPrefix setupEntry `form:"redis_prefix"`
RedisUser setupEntry `form:"redis_user"`
RedisPw setupEntry `form:"redis_password"`
RedisUseSsl setupEntry `form:"redis_ssl_sel" isBool:"true"`
BindLocalhost setupEntry `form:"localhost_sel" isBool:"true"`
UseSsl setupEntry `form:"ssl_sel" isBool:"true"`
SaveIp setupEntry `form:"logip_sel" isBool:"true"`
Port setupEntry `form:"port" isInt:"true"`
PublicName setupEntry `form:"public_name"`
ExtUrl setupEntry `form:"url"`
RedirectUrl setupEntry `form:"url_redirection"`
IncludeFilename setupEntry `form:"showfilename_sel" isBool:"true"`
AuthenticationMode setupEntry `form:"authentication_sel" isInt:"true"`
AuthUsername setupEntry `form:"auth_username"`
AuthPassword setupEntry `form:"auth_pw"`
OAuthProvider setupEntry `form:"oauth_provider"`
OAuthClientId setupEntry `form:"oauth_id"`
OAuthClientSecret setupEntry `form:"oauth_secret"`
OAuthAuthorisedGroups setupEntry `form:"oauth_allowed_groups"`
OAuthAdminUser setupEntry `form:"oauth_admin_user"`
OAuthScopeGroup setupEntry `form:"oauth_scope_groups"`
OAuthRestrictGroups setupEntry `form:"oauth_restrict_groups" isBool:"true"`
OAuthRecheckInterval setupEntry `form:"oauth_recheck_interval" isInt:"true"`
OAuthOnlyRegisteredUsers setupEntry `form:"oauth_only_registered_users" isBool:"true"`
AuthHeaderKey setupEntry `form:"auth_headerkey"`
AuthHeaderAdmin setupEntry `form:"auth_header_admin"`
AuthHeaderOnlyRegisteredUsers setupEntry `form:"auth_header_only_registered_users" isBool:"true"`
StorageSelection setupEntry `form:"storage_sel"`
PicturesAlwaysLocal setupEntry `form:"storage_sel_image"`
ProxyDownloads setupEntry `form:"storage_sel_proxy"`
S3Bucket setupEntry `form:"s3_bucket"`
S3Region setupEntry `form:"s3_region"`
S3ApiKey setupEntry `form:"s3_api"`
S3ApiSecret setupEntry `form:"s3_secret"`
S3Endpoint setupEntry `form:"s3_endpoint"`
EncryptionLevel setupEntry `form:"encrypt_sel" isInt:"true"`
EncryptionPassword setupEntry `form:"enc_pw"`
DatabaseType setupEntry `form:"dbtype_sel" isInt:"true"`
SqliteLocation setupEntry `form:"sqlite_location"`
RedisLocation setupEntry `form:"redis_location"`
RedisPrefix setupEntry `form:"redis_prefix"`
RedisUser setupEntry `form:"redis_user"`
RedisPw setupEntry `form:"redis_password"`
RedisUseSsl setupEntry `form:"redis_ssl_sel" isBool:"true"`
}
func (s *setupValues) init() {
@@ -680,9 +668,10 @@ func createInputInternalAuth() setupValues {
values.EncryptionLevel.Value = "0"
values.PicturesAlwaysLocal.Value = "nochange"
values.SaveIp.Value = "0"
values.OAuthRestrictUser.Value = "false"
values.OAuthOnlyRegisteredUsers.Value = "false"
values.OAuthRestrictGroups.Value = "false"
values.OAuthRecheckInterval.Value = "12"
values.AuthHeaderOnlyRegisteredUsers.Value = "false"
values.DatabaseType.Value = "0"
values.SqliteLocation.Value = "./test/gokapi.sqlite"
values.RedisUseSsl.Value = "0"
@@ -702,11 +691,12 @@ func createInputHeaderAuth() setupValues {
values.RedirectUrl.Value = "https://test.com"
values.AuthenticationMode.Value = "2"
values.AuthHeaderKey.Value = "testkey"
values.AuthHeaderUsers.Value = "test1 ;test2"
values.AuthHeaderAdmin.Value = "test1"
values.AuthHeaderOnlyRegisteredUsers.Value = "true"
values.StorageSelection.Value = "local"
values.EncryptionLevel.Value = "0"
values.SaveIp.Value = "1"
values.OAuthRestrictUser.Value = "false"
values.OAuthOnlyRegisteredUsers.Value = "false"
values.OAuthRestrictGroups.Value = "false"
values.OAuthRecheckInterval.Value = "12"
values.IncludeFilename.Value = "0"
@@ -723,10 +713,9 @@ func createInputOAuth() setupValues {
values.OAuthProvider.Value = "provider"
values.OAuthClientId.Value = "id"
values.OAuthClientSecret.Value = "secret"
values.OAuthRestrictUser.Value = "true"
values.OAuthOnlyRegisteredUsers.Value = "true"
values.OAuthRestrictGroups.Value = "true"
values.OAuthScopeUser.Value = "email"
values.OAuthAuthorisedUsers.Value = "oatest1; oatest2"
values.OAuthAdminUser.Value = "oatest1"
values.OAuthScopeGroup.Value = "groups"
values.OAuthAuthorisedGroups.Value = "group1; group2"
values.StorageSelection.Value = "local"
@@ -57,6 +57,7 @@
#label_ousers, #label_ogroups {
margin-right: 10px;
}
</style>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
@@ -105,15 +106,21 @@
</div>
<!-- Step 1 DB Location -->
<div class="wizard-card wizard-card-overlay" data-cardname="dblocation">
<h3>Database</h3>
<h3 style="display:none">Database</h3>
<div class="wizard-input-section">
<div class="form-group">
{{ if not .IsInitialSetup }}
<p>
<span style="color:red"><b>Warning:</b> When changing database parameters, ensure you first migrate the database to the new location using <code>gokapi --migrate-database</code> before finalising the setup.
</p><br>
{{ end }}
<label for="dbtype_sel">Type of database</label>
<select name="dbtype_sel" id="dbtype_sel" style="width:350px;" class="select form-control" onChange="dbChanged()">
<option value="0" selected>SQLite</option>
<option value="1">Redis (experimental)</option>
<option value="1">Redis</option>
</select><br>
<div id="divsqlite">
@@ -142,7 +149,7 @@
<!-- Step 2 Webserver -->
<div class="wizard-card wizard-card-overlay" data-cardname="webserver1">
<h3>Webserver 1/2</h3>
<h3 style="display:none">Webserver 1/2</h3>
<div class="wizard-input-section">
<div class="form-group">
@@ -182,37 +189,29 @@
<!-- Step 3 Webserver 2 -->
<div class="wizard-card wizard-card-overlay" data-cardname="webserver2">
<h3>Webserver 2/2</h3>
<h3 style="display:none">Webserver 2/2</h3>
<div class="wizard-input-section">
<div class="form-group">
<div class="col-sm-8">
<label for="public_name">Public Name:</label>
<input type="text" class="form-control" id="public_name" name="public_name" value="Gokapi" placeholder="Public Name" required>
</div><br><br><br>
<div class="col-sm-8">
<label for="port">Port:</label>
<br><label for="port">Port:</label>
{{ if .IsDocker }}
<input type="number" class="form-control" id="port" value="53842" placeholder="Port number" disabled>
<input type="hidden" class="form-control" name="port" value="53842">
{{ else }}
<input type="number" class="form-control" id="port" name="port" value="53842" min="1" max="65535" placeholder="Port number" data-min="1" data-validate="validateMinLength" required onChange="urlParamChanged()">
{{ end }}
</div><br><br><br>
<div class="col-sm-8">
<label for="url">Public facing URL:</label>
<br><label for="url">Public facing URL:</label>
<input type="text" class="form-control" id="url" name="url" value="http://127.0.0.1:53842/" onfocusout="extUrlChanged(this)" placeholder="Public URL" data-min="8" required data-validate="validateUrl">
</div><br><br><br>
<div class="col-sm-8">
<label for="url_redirection">Redirection URL for the index:</label>
<br><label for="url_redirection">Redirection URL for the index:</label>
<input type="text" class="form-control" id="url_redirection" name="url_redirection" value="https://github.com/Forceu/Gokapi/" placeholder="Redirection URL" required data-min="1" data-validate="validateMinLength">
</div>
</div>
</div>
</div>
@@ -222,7 +221,7 @@
<!-- Step 4 Auth -->
<div class="wizard-card wizard-card-overlay" data-cardname="authentication">
<h3>Authentication</h3>
<h3 style="display:none">Authentication</h3>
<div class="wizard-input-section">
<div class="form-group">
@@ -238,7 +237,7 @@
</optgroup>
<optgroup label="Reverse Proxy">
<option value="2">Header Authentication</option>
<option value="2">Trusted Header Authentication</option>
<option value="3">Disabled / Access Restriction</option>
</optgroup>
@@ -249,7 +248,7 @@
<!-- Step 5a Credentials PW -->
<div class="wizard-card wizard-card-overlay" data-cardname="credentials">
<h3>Credentials</h3>
<h3 style="display:none">Credentials</h3>
<div class="wizard-input-section">
@@ -275,7 +274,7 @@
<!-- Step 5b Credentials Oauth -->
<div class="wizard-card wizard-card-overlay" data-cardname="credentials-oauth">
<h3>Credentials</h3>
<h3 style="display:none">Credentials</h3>
<div class="wizard-input-section">
@@ -298,6 +297,10 @@
<label for="oauth_secret">Client Secret:</label>
<input type="text" class="form-control" id="oauth_secret" name="oauth_secret" placeholder="Client Secret" data-min="1" required data-validate="validateMinLength">
</div>
<div class="col-sm-8" style="width:90%">
<label for="oauth_admin_user">Admin email address:</label>
<input type="text" class="form-control" id="oauth_admin_user" name="oauth_admin_user" placeholder="Admin email address" data-min="3" required data-validate="validateMinLength">
</div>
<div class="col-sm-8" style="width:90%">
Recheck identity every
<select name="oauth_recheck_interval" id="oauth_recheck_interval" style="width:350px;" class="select form-control">
@@ -313,14 +316,6 @@
<div class="col-sm-8" style="width:90%">
<br>Restrict to:
<div class="oauthscopecontainer">
<div class="oauthscopes">
<input type="hidden" name="oauth_restrict_users.unchecked" value="false"/>
<input type="checkbox" id="oauth_restrict_users" name="oauth_restrict_users" onchange="handleOauthCheckboxChange(this)" value="true">
<label id="label_ousers" for="oauth_restrict_users" class="checkboxLabel">Users &nbsp;</label>
<input type="text" id="oauth_scope_users" name="oauth_scope_users" class="input-field" placeholder="Scope identifier" data-min="1" data-validate="validateMinLength" disabled>&nbsp;
<input type="text" id="oauth_allowed_users" name="oauth_allowed_users" class="input-field" placeholder="Authorised users" data-min="1" data-validate="validateMinLength" disabled>
</div>
<div class="oauthscopes">
<input type="hidden" name="oauth_restrict_groups.unchecked" value="false"/>
<input type="checkbox" id="oauth_restrict_groups" name="oauth_restrict_groups" onchange="handleOauthCheckboxChange(this)" value="true">
@@ -328,14 +323,20 @@
<input type="text" id="oauth_scope_groups" name="oauth_scope_groups" class="input-field" placeholder="Scope identifier" data-min="1" data-validate="validateMinLength" disabled>&nbsp;
<input type="text" id="oauth_allowed_groups" name="oauth_allowed_groups" class="input-field" placeholder="Authorised groups" data-min="1" data-validate="validateMinLength" disabled>
</div>
<div><br>
<input type="hidden" name="oauth_only_registered_users.unchecked" value="false"/>
<input id="oauth_only_registered_users" name="oauth_only_registered_users" type="checkbox" value="true"/>
<span>&nbsp;Only allow already existing users to log in</span>
</div>
</div>
</div>
<div class="col-sm-8" style="width:90%">
<label for="oauth_redir"><br>Redirection URL:</label>
{{ if .IsInitialSetup }}
<input type="text" class="form-control" id="oauth_redir" name="oauth_redir" disabled value="http://127.0.0.1:53842/oauth-callback">
<input type="text" style="cursor:default" class="form-control" id="oauth_redir" name="oauth_redir" disabled value="http://127.0.0.1:53842/oauth-callback">
{{ else }}
<input type="text" class="form-control" id="oauth_redir" name="oauth_redir" disabled value="{{ .Settings.ServerUrl }}oauth-callback">
<input type="text" style="cursor:default" class="form-control" id="oauth_redir" name="oauth_redir" disabled value="{{ .Settings.ServerUrl }}oauth-callback">
{{ end }}
</div><br><br>
@@ -346,30 +347,35 @@
<!-- Step 5c Credentials Header -->
<div class="wizard-card wizard-card-overlay" data-cardname="credentials-header">
<h3>Credentials</h3>
<h3 style="display:none">Credentials</h3>
<div class="wizard-input-section">
<div class="form-group">
<p>
Enter the key of the header that will be provided from your reverse proxy containing the username.<br>Add the allowed users to the list below (seperated by semicolon) or leave it blank if access is granted to all authenticated users.<br><br>
Enter the key of the header that will be provided from your reverse proxy containing the username.<br><br>
</p>
<div class="col-sm-8">
<p>
<label for="auth_headerkey">Header Key:</label>
<input type="text" class="form-control" id="auth_headerkey" name="auth_headerkey" required placeholder="Header Key" data-validate="validateMinLength">
</div><br><br>
<input type="text" class="form-control" id="auth_headerkey" name="auth_headerkey" data-min="1" required placeholder="Header Key" data-validate="validateMinLength">
</p>
<div class="col-sm-8">
<label for="auth_header_users">Authorised users:</label>
<input type="text" class="form-control" id="auth_header_users" name="auth_header_users" placeholder="Authorised users">
</div>
<p>
<label for="auth_header_admin">Admin user name:</label>
<input type="text" class="form-control" id="auth_header_admin" name="auth_header_admin" data-min="3" data-validate="validateMinLength" required placeholder="Admin user name">
</p>
<br>
<input type="hidden" name="auth_header_only_registered_users.unchecked" value="false"/>
<input id="auth_header_only_registered_users" name="auth_header_only_registered_users" type="checkbox" value="true"/>
<span>&nbsp;Only allow already existing users to log in</span>
</div>
</div>
</div>
<!-- Step 5d Credentials Disbabled -->
<div class="wizard-card wizard-card-overlay" data-cardname="credentials-disabled">
<h3>Credentials</h3>
<h3 style="display:none">Credentials</h3>
<div class="wizard-input-section">
@@ -392,7 +398,7 @@
<!-- Step 6 Storage -->
<div class="wizard-card wizard-card-overlay" data-cardname="storage">
<h3>Storage</h3>
<h3 style="display:none">Storage</h3>
<div class="wizard-input-section">
<p>
@@ -437,7 +443,7 @@
<!-- Step 7 S3 Credentials -->
<div class="wizard-card wizard-card-overlay" data-cardname="s3credentials">
<h3>S3 Credentials</h3>
<h3 style="display:none">S3 Credentials</h3>
<div class="wizard-input-section">
@@ -525,7 +531,7 @@ function TestAWS(button, isManual) {
<!-- Step 7 Encryption -->
<div class="wizard-card wizard-card-overlay" data-cardname="encryption">
<h3>Encryption Level</h3>
<h3 style="display:none">Encryption Level</h3>
<div class="wizard-input-section">
<div class="form-group">
@@ -655,7 +661,7 @@ function TestAWS(button, isManual) {
<!-- Step 8 Encryption PW -->
<div class="wizard-card wizard-card-overlay" data-cardname="encryptionpw">
<h3>Encryption Master Password</h3>
<h3 style="display:none">Encryption Master Password</h3>
<div class="wizard-input-section">
<div class="form-group">
@@ -798,14 +804,9 @@ function TestAWS(button, isManual) {
document.getElementById("oauth_provider").value = "{{ .Auth.OAuthProvider }}";
document.getElementById("oauth_id").value = "{{ .Auth.OAuthClientId }}";
document.getElementById("oauth_secret").value = "{{ .Auth.OAuthClientSecret }}";
document.getElementById("oauth_admin_user").value = "{{ .Auth.Username }}";
document.getElementById("oauth_recheck_interval").value = "{{ .Auth.OAuthRecheckInterval }}";
{{ if ne .OAuthUsers "" }}
document.getElementById("oauth_restrict_users").checked = true;
document.getElementById("oauth_scope_users").disabled = false;
document.getElementById("oauth_scope_users").value = "{{ .Auth.OAuthUserScope }}";
document.getElementById("oauth_allowed_users").disabled = false;
document.getElementById("oauth_allowed_users").value = "{{ .OAuthUsers }}";
{{ end }}
document.getElementById("oauth_only_registered_users").checked = {{.Auth.OnlyRegisteredUsers}};
{{ if ne .OAuthGroups "" }}
document.getElementById("oauth_restrict_groups").checked = true;
document.getElementById("oauth_scope_groups").disabled = false;
@@ -816,7 +817,8 @@ function TestAWS(button, isManual) {
break;
case 2:
document.getElementById("auth_headerkey").value = "{{ .Auth.HeaderKey }}";
document.getElementById("auth_header_users").value = "{{ .HeaderUsers }}";
document.getElementById("auth_header_admin").value = "{{ .Auth.Username }}";
document.getElementById("auth_header_only_registered_users").checked = {{.Auth.OnlyRegisteredUsers}};
break;
}
@@ -850,8 +852,6 @@ function TestAWS(button, isManual) {
/* enable inputs again, otherwise they will not be submitted */
document.getElementById("oauth_scope_groups").disabled = false;
document.getElementById("oauth_scope_users").disabled = false;
document.getElementById("oauth_allowed_users").disabled = false;
document.getElementById("oauth_allowed_groups").disabled = false;
$.ajax({
+2 -6
View File
@@ -46,13 +46,9 @@ func Init(config models.Configuration) {
switch config.Encryption.Level {
case NoEncryption:
return
case LocalEncryptionStored:
fallthrough
case FullEncryptionStored:
case LocalEncryptionStored, FullEncryptionStored:
initWithCipher(config.Encryption.Cipher)
case LocalEncryptionInput:
fallthrough
case FullEncryptionInput:
case LocalEncryptionInput, FullEncryptionInput:
initWithPassword(config.Encryption.Salt, config.Encryption.Checksum, config.Encryption.ChecksumSalt)
case EndToEndEncryption:
return
+3 -6
View File
@@ -34,9 +34,6 @@ type Environment struct {
AwsKeySecret string `env:"AWS_KEY_SECRET"`
AwsEndpoint string `env:"AWS_ENDPOINT"`
AwsProxyDownload bool `env:"AWS_PROXY_DOWNLOAD" envDefault:"false"`
// deprecated
// Will be removed with version 1.10.0
DatabaseName string `env:"DB_NAME" envDefault:"gokapi.sqlite"`
}
// New parses the env variables
@@ -101,10 +98,10 @@ func (e *Environment) IsAwsProvided() bool {
// GetConfigPaths returns the config paths to config files and the directory containing the files. The following results are returned:
// Path to config file, Path to directory containing config file, Name of config file, Path to AWS config file
func GetConfigPaths() (string, string, string, string) {
func GetConfigPaths() (pathConfigFile, pathConfigDir, nameConfigFile, pathAwsConfig string) {
env := New()
awsConfigPAth := env.ConfigDir + "/cloudconfig.yml"
return env.ConfigPath, env.ConfigDir, env.ConfigFile, awsConfigPAth
pathAwsConfig = env.ConfigDir + "/cloudconfig.yml"
return env.ConfigPath, env.ConfigDir, env.ConfigFile, pathAwsConfig
}
var osExit = os.Exit
@@ -1,10 +1,12 @@
package models
import "time"
import (
"time"
)
const (
// ApiPermView is the permission for viewing metadata of all uploaded files
ApiPermView = 1 << iota
ApiPermView ApiPermission = 1 << iota
// ApiPermUpload is the permission for creating new files
ApiPermUpload
// ApiPermDelete is the permission for deleting files
@@ -15,28 +17,34 @@ const (
ApiPermEdit
// ApiPermReplace is the permission for replacing the content of uploaded files
ApiPermReplace
// ApiPermManageUsers is the permission for managing users
ApiPermManageUsers
)
// ApiPermNone means no permission granted
const ApiPermNone = 0
const ApiPermNone ApiPermission = 0
// ApiPermAll means all permission granted
const ApiPermAll = 63
const ApiPermAll ApiPermission = 127
// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod and ApiPermReplace
// ApiPermDefault means all permission granted, except ApiPermApiMod, ApiPermManageUsers and ApiPermReplace
// This is the default for new API keys that are created from the UI
const ApiPermAllNoApiMod = ApiPermAll - ApiPermApiMod - ApiPermReplace
const ApiPermDefault = ApiPermAll - ApiPermApiMod - ApiPermManageUsers - ApiPermReplace
// ApiKey contains data of a single api key
type ApiKey struct {
Id string `json:"Id" redis:"Id"`
FriendlyName string `json:"FriendlyName" redis:"FriendlyName"`
LastUsed int64 `json:"LastUsed" redis:"LastUsed"`
Permissions uint8 `json:"Permissions" redis:"Permissions"`
Expiry int64 `json:"Expiry" redis:"Expiry"` // Does not expire if 0
IsSystemKey bool `json:"IsSystemKey" redis:"IsSystemKey"`
Id string `json:"Id" redis:"Id"`
PublicId string `json:"PublicId" redis:"PublicId"`
FriendlyName string `json:"FriendlyName" redis:"FriendlyName"`
LastUsed int64 `json:"LastUsed" redis:"LastUsed"`
Permissions ApiPermission `json:"Permissions" redis:"Permissions"`
Expiry int64 `json:"Expiry" redis:"Expiry"` // Does not expire if 0
IsSystemKey bool `json:"IsSystemKey" redis:"IsSystemKey"`
UserId int `json:"UserId" redis:"UserId"`
}
type ApiPermission uint8
// GetReadableDate returns the date as YYYY-MM-DD HH:MM:SS
func (key *ApiKey) GetReadableDate() string {
if key.LastUsed == 0 {
@@ -45,18 +53,23 @@ func (key *ApiKey) GetReadableDate() string {
return time.Unix(key.LastUsed, 0).Format("2006-01-02 15:04:05")
}
// SetPermission grants one or more permissions
func (key *ApiKey) SetPermission(permission uint8) {
// GetRedactedId returns a redacted version of the API key
func (key *ApiKey) GetRedactedId() string {
return key.Id[0:2] + "**************************" + key.Id[len(key.Id)-2:]
}
// GrantPermission sets one or more permissions
func (key *ApiKey) GrantPermission(permission ApiPermission) {
key.Permissions |= permission
}
// RemovePermission revokes one or more permissions
func (key *ApiKey) RemovePermission(permission uint8) {
func (key *ApiKey) RemovePermission(permission ApiPermission) {
key.Permissions &^= permission
}
// HasPermission returns true if the key has the permission(s)
func (key *ApiKey) HasPermission(permission uint8) bool {
func (key *ApiKey) HasPermission(permission ApiPermission) bool {
if permission == ApiPermNone {
return true
}
@@ -93,8 +106,14 @@ func (key *ApiKey) HasPermissionReplace() bool {
return key.HasPermission(ApiPermReplace)
}
// HasPermissionManageUsers returns true if ApiPermManageUsers is granted
func (key *ApiKey) HasPermissionManageUsers() bool {
return key.HasPermission(ApiPermManageUsers)
}
// ApiKeyOutput is the output that is used after a new key is created
type ApiKeyOutput struct {
Result string
Id string
Result string
Id string
PublicId string
}
@@ -2,21 +2,30 @@ package models
import (
"github.com/forceu/gokapi/internal/test"
"os"
"testing"
"time"
)
func TestApiKey_GetReadableDate(t *testing.T) {
key := &ApiKey{}
test.IsEqualString(t, key.GetReadableDate(), "Never")
now := time.Now()
key.LastUsed = now.Unix()
test.IsEqualString(t, key.GetReadableDate(), now.Format("2006-01-02 15:04:05"))
key.LastUsed = 1736276120
lastTz := os.Getenv("TZ")
err := os.Setenv("TZ", "Europe/Berlin")
test.IsNil(t, err)
test.IsEqualString(t, key.GetReadableDate(), "2025-01-07 19:55:20")
err = os.Setenv("TZ", lastTz)
test.IsNil(t, err)
}
func TestApiKey_GetRedactedId(t *testing.T) {
key := &ApiKey{Id: "eivahB9imahj3fiquoh6DieNgeeThe"}
test.IsEqualString(t, key.GetRedactedId(), "ei**************************he")
}
func TestSetPermission(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermView)
key.GrantPermission(ApiPermView)
if !key.HasPermission(ApiPermView) {
t.Errorf("expected permission %d to be set", ApiPermView)
}
@@ -27,7 +36,7 @@ func TestSetPermission(t *testing.T) {
func TestRemovePermission(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermView)
key.GrantPermission(ApiPermView)
if !key.HasPermission(ApiPermView) {
t.Errorf("expected permission %d to be set", ApiPermView)
}
@@ -45,7 +54,7 @@ func TestHasPermission(t *testing.T) {
if key.HasPermission(ApiPermUpload) {
t.Errorf("expected permission %d not to be set", ApiPermUpload)
}
key.SetPermission(ApiPermUpload)
key.GrantPermission(ApiPermUpload)
if !key.HasPermission(ApiPermUpload) {
t.Errorf("expected permission %d to be set", ApiPermUpload)
}
@@ -59,7 +68,7 @@ func TestHasPermissionView(t *testing.T) {
if key.HasPermissionView() {
t.Errorf("expected view permission to be not set")
}
key.SetPermission(ApiPermView)
key.GrantPermission(ApiPermView)
if !key.HasPermissionView() {
t.Errorf("expected view permission to be set")
}
@@ -67,7 +76,7 @@ func TestHasPermissionView(t *testing.T) {
func TestHasPermissionUpload(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermUpload)
key.GrantPermission(ApiPermUpload)
if !key.HasPermissionUpload() {
t.Errorf("expected upload permission to be set")
}
@@ -75,7 +84,7 @@ func TestHasPermissionUpload(t *testing.T) {
func TestHasPermissionDelete(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermDelete)
key.GrantPermission(ApiPermDelete)
if !key.HasPermissionDelete() {
t.Errorf("expected delete permission to be set")
}
@@ -83,7 +92,7 @@ func TestHasPermissionDelete(t *testing.T) {
func TestHasPermissionApiMod(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermApiMod)
key.GrantPermission(ApiPermApiMod)
if !key.HasPermissionApiMod() {
t.Errorf("expected ApiMod permission to be set")
}
@@ -91,15 +100,30 @@ func TestHasPermissionApiMod(t *testing.T) {
func TestHasPermissionEdit(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermEdit)
key.GrantPermission(ApiPermEdit)
if !key.HasPermissionEdit() {
t.Errorf("expected edit permission to be set")
}
}
func TestHasPermissionReplace(t *testing.T) {
key := &ApiKey{}
key.GrantPermission(ApiPermReplace)
if !key.HasPermissionReplace() {
t.Errorf("expected edit permission to be set")
}
}
func TestHasPermissionManageUsers(t *testing.T) {
key := &ApiKey{}
key.GrantPermission(ApiPermManageUsers)
if !key.HasPermissionManageUsers() {
t.Errorf("expected edit permission to be set")
}
}
func TestApiPermAllNoApiMod(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermAllNoApiMod)
key.GrantPermission(ApiPermDefault)
if !key.HasPermission(ApiPermView) || !key.HasPermission(ApiPermUpload) || !key.HasPermission(ApiPermDelete) || !key.HasPermission(ApiPermEdit) {
t.Errorf("expected all permissions except ApiMod to be set")
}
@@ -110,16 +134,22 @@ func TestApiPermAllNoApiMod(t *testing.T) {
func TestApiPermAll(t *testing.T) {
key := &ApiKey{}
key.SetPermission(ApiPermAll)
if !key.HasPermission(ApiPermView) || !key.HasPermission(ApiPermUpload) || !key.HasPermission(ApiPermDelete) || !key.HasPermission(ApiPermApiMod) || !key.HasPermission(ApiPermEdit) {
key.GrantPermission(ApiPermAll)
if !key.HasPermission(ApiPermView) ||
!key.HasPermission(ApiPermUpload) ||
!key.HasPermission(ApiPermDelete) ||
!key.HasPermission(ApiPermApiMod) ||
!key.HasPermission(ApiPermEdit) ||
!key.HasPermission(ApiPermReplace) ||
!key.HasPermission(ApiPermManageUsers) {
t.Errorf("expected all permissions to be set")
}
}
// Helper function to check only one permission is set
func checkOnlyPermissionSet(t *testing.T, key *ApiKey, perm uint8, permName string) {
func checkOnlyPermissionSet(t *testing.T, key *ApiKey, perm ApiPermission) {
allPermissions := []struct {
perm uint8
perm ApiPermission
permName string
}{
{ApiPermView, "ApiPermView"},
@@ -127,6 +157,8 @@ func checkOnlyPermissionSet(t *testing.T, key *ApiKey, perm uint8, permName stri
{ApiPermDelete, "ApiPermDelete"},
{ApiPermApiMod, "ApiPermApiMod"},
{ApiPermEdit, "ApiPermEdit"},
{ApiPermReplace, "ApiPermReplace"},
{ApiPermManageUsers, "ApiPermManageUsers"},
}
for _, p := range allPermissions {
@@ -147,7 +179,7 @@ func TestSetIndividualPermissions(t *testing.T) {
// Test each individual permission
permissions := []struct {
perm uint8
perm ApiPermission
permName string
}{
{ApiPermView, "ApiPermView"},
@@ -155,17 +187,19 @@ func TestSetIndividualPermissions(t *testing.T) {
{ApiPermDelete, "ApiPermDelete"},
{ApiPermApiMod, "ApiPermApiMod"},
{ApiPermEdit, "ApiPermEdit"},
{ApiPermReplace, "ApiPermReplace"},
{ApiPermManageUsers, "ApiPermManageUsers"},
}
for _, p := range permissions {
key.Permissions = ApiPermNone // reset permissions
key.SetPermission(p.perm)
checkOnlyPermissionSet(t, key, p.perm, p.permName)
key.GrantPermission(p.perm)
checkOnlyPermissionSet(t, key, p.perm)
}
}
// Helper function to check combined permissions are set
func checkCombinedPermissions(t *testing.T, key *ApiKey, perms []uint8) {
func checkCombinedPermissions(t *testing.T, key *ApiKey, perms []ApiPermission) {
for _, perm := range perms {
if !key.HasPermission(perm) {
t.Errorf("expected permission %d to be set", perm)
@@ -175,19 +209,21 @@ func checkCombinedPermissions(t *testing.T, key *ApiKey, perms []uint8) {
func TestSetCombinedPermissions(t *testing.T) {
key := &ApiKey{}
allPermissions := []uint8{
allPermissions := []ApiPermission{
ApiPermView,
ApiPermUpload,
ApiPermDelete,
ApiPermApiMod,
ApiPermEdit,
ApiPermReplace,
ApiPermManageUsers,
}
// Test setting permissions in combination
for i := 0; i < len(allPermissions); i++ {
key.Permissions = ApiPermNone // reset permissions
for j := 0; j <= i; j++ {
key.SetPermission(allPermissions[j])
key.GrantPermission(allPermissions[j])
}
checkCombinedPermissions(t, key, allPermissions[:i+1])
}
+15 -4
View File
@@ -6,15 +6,26 @@ type AuthenticationConfig struct {
SaltAdmin string `json:"SaltAdmin"`
SaltFiles string `json:"SaltFiles"`
Username string `json:"Username"`
Password string `json:"Password"`
HeaderKey string `json:"HeaderKey"`
OAuthProvider string `json:"OauthProvider"`
OAuthClientId string `json:"OAuthClientId"`
OAuthClientSecret string `json:"OAuthClientSecret"`
OAuthUserScope string `json:"OauthUserScope"`
OAuthGroupScope string `json:"OauthGroupScope"`
OAuthRecheckInterval int `json:"OAuthRecheckInterval"`
HeaderUsers []string `json:"HeaderUsers"`
OAuthGroups []string `json:"OAuthGroups"`
OAuthUsers []string `json:"OauthUsers"`
OnlyRegisteredUsers bool `json:"OnlyRegisteredUsers"`
}
const (
// AuthenticationInternal authentication method uses a user / password combination handled by Gokapi
AuthenticationInternal = iota
// AuthenticationOAuth2 authentication retrieves the users email with Open Connect ID
AuthenticationOAuth2
// AuthenticationHeader authentication relies on a header from a reverse proxy to parse the username
AuthenticationHeader
// AuthenticationDisabled authentication ignores all internal authentication procedures. A reverse proxy needs to restrict access
AuthenticationDisabled
)
+1 -4
View File
@@ -12,13 +12,10 @@ var testConfig = Configuration{
SaltAdmin: "saltadmin",
SaltFiles: "saltfiles",
Username: "admin",
Password: "adminpwhashed",
HeaderKey: "",
OAuthProvider: "",
OAuthClientId: "",
OAuthClientSecret: "",
HeaderUsers: nil,
OAuthUsers: nil,
},
Port: ":12345",
ServerUrl: "https://testserver.com/",
@@ -49,4 +46,4 @@ func TestConfiguration_ToString(t *testing.T) {
test.IsEqualString(t, testConfig.ToString(), exptectedUnidentedOutput)
}
const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","Password":"adminpwhashed","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","OauthUserScope":"","OauthGroupScope":"","OAuthRecheckInterval":0,"HeaderUsers":null,"OAuthGroups":null,"OauthUsers":null},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","PublicName":"public-name","DataDir":"test","DatabaseUrl":"sqlite://./test/gokapitest.sqlite","ConfigVersion":14,"LengthId":5,"MaxFileSizeMB":20,"MaxMemory":50,"ChunkSize":0,"MaxParallelUploads":0,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"UseSsl":true,"PicturesAlwaysLocal":true,"SaveIp":false,"IncludeFilename":false}`
const exptectedUnidentedOutput = `{"Authentication":{"Method":0,"SaltAdmin":"saltadmin","SaltFiles":"saltfiles","Username":"admin","HeaderKey":"","OauthProvider":"","OAuthClientId":"","OAuthClientSecret":"","OauthGroupScope":"","OAuthRecheckInterval":0,"OAuthGroups":null,"OnlyRegisteredUsers":false},"Port":":12345","ServerUrl":"https://testserver.com/","RedirectUrl":"https://test.com","PublicName":"public-name","DataDir":"test","DatabaseUrl":"sqlite://./test/gokapitest.sqlite","ConfigVersion":14,"LengthId":5,"MaxFileSizeMB":20,"MaxMemory":50,"ChunkSize":0,"MaxParallelUploads":0,"Encryption":{"Level":1,"Cipher":"AA==","Salt":"encsalt","Checksum":"encsum","ChecksumSalt":"encsumsalt"},"UseSsl":true,"PicturesAlwaysLocal":true,"SaveIp":false,"IncludeFilename":false}`
+3
View File
@@ -22,6 +22,7 @@ type File struct {
SizeBytes int64 `json:"SizeBytes" redis:"SizeBytes"` // Filesize in bytes
DownloadsRemaining int `json:"DownloadsRemaining" redis:"DownloadsRemaining"` // The remaining downloads for this file
DownloadCount int `json:"DownloadCount" redis:"DownloadCount"` // The amount of times the file has been downloaded
UserId int `json:"UserId" redis:"UserId"` // The user ID of the uploader
Encryption EncryptionInfo `json:"Encryption" redis:"-"` // If the file is encrypted, this stores all info for decrypting
UnlimitedDownloads bool `json:"UnlimitedDownloads" redis:"UnlimitedDownloads"` // True if the uploader did not limit the downloads
UnlimitedTime bool `json:"UnlimitedTime" redis:"UnlimitedTime"` // True if the uploader did not limit the time
@@ -49,6 +50,7 @@ type FileApiOutput struct {
IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted
IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file
IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage
UploaderId int `json:"UploaderId"` // The user ID of the uploader
}
// EncryptionInfo holds information about the encryption used on the file
@@ -80,6 +82,7 @@ func (f *File) ToFileApiOutput(serverUrl string, useFilenameInUrl bool) (FileApi
result.IsEndToEndEncrypted = f.Encryption.IsEndToEndEncrypted
result.UrlHotlink = getHotlinkUrl(result, serverUrl, useFilenameInUrl)
result.UrlDownload = getDownloadUrl(result, serverUrl, useFilenameInUrl)
result.UploaderId = f.UserId
return result, nil
}
+3 -2
View File
@@ -20,6 +20,7 @@ func TestToJsonResult(t *testing.T) {
HotlinkId: "hotlinkid",
ContentType: "text/html",
AwsBucket: "test",
UserId: 2,
DownloadCount: 3,
Encryption: EncryptionInfo{
IsEncrypted: true,
@@ -29,8 +30,8 @@ func TestToJsonResult(t *testing.T) {
UnlimitedDownloads: true,
UnlimitedTime: true,
}
test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"future","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false},"IncludeFilename":false}`)
test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"future","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false},"IncludeFilename":true}`)
test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"future","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"UploaderId":2},"IncludeFilename":false}`)
test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"future","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"UploaderId":2},"IncludeFilename":true}`)
}
func TestIsLocalStorage(t *testing.T) {
+2 -1
View File
@@ -2,14 +2,15 @@ package models
// UploadRequest is used to set an upload request
type UploadRequest struct {
UserId int
AllowedDownloads int
Expiry int
ExpiryTimestamp int64
Password string
ExternalUrl string
MaxMemory int
UnlimitedDownload bool
UnlimitedTime bool
IsEndToEndEncrypted bool
ExpiryTimestamp int64
RealSize int64
}
+1
View File
@@ -4,4 +4,5 @@ package models
type Session struct {
RenewAt int64 `redis:"renew_at"`
ValidUntil int64 `redis:"valid_until"`
UserId int `redis:"user_id"`
}
+135
View File
@@ -0,0 +1,135 @@
package models
import (
"encoding/json"
"github.com/forceu/gokapi/internal/helper"
"time"
)
type UserPermission uint16
type User struct {
Id int `json:"id" redis:"id"`
Name string `json:"name" redis:"Name"`
Permissions UserPermission `json:"permissions" redis:"Permissions"`
UserLevel UserRank `json:"userLevel" redis:"UserLevel"`
LastOnline int64 `json:"lastOnline" redis:"LastOnline"`
Password string `json:"-" redis:"Password"`
ResetPassword bool `json:"resetPassword" redis:"ResetPassword"`
}
// GetReadableDate returns the date as YYYY-MM-DD HH:MM
func (u *User) GetReadableDate() string {
if u.LastOnline == 0 {
return "Never"
}
if time.Now().Unix()-u.LastOnline < 120 {
return "Online"
}
return time.Unix(u.LastOnline, 0).Format("2006-01-02 15:04")
}
// GetReadableUserLevel returns the userlevel as a group name
func (u *User) GetReadableUserLevel() string {
switch u.UserLevel {
case UserLevelSuperAdmin:
return "Super Admin"
case UserLevelAdmin:
return "Admin"
case UserLevelUser:
return "User"
default:
return "Invalid"
}
}
// ToJson returns the user as a JSon object
func (u *User) ToJson() string {
result, err := json.Marshal(u)
helper.Check(err)
return string(result)
}
const UserLevelSuperAdmin UserRank = 0
const UserLevelAdmin UserRank = 1
const UserLevelUser UserRank = 2
type UserRank uint8
func (u *User) IsSuperAdmin() bool {
return u.UserLevel == UserLevelSuperAdmin
}
func (u *User) IsSameUser(userId int) bool {
return u.Id == userId
}
const (
UserPermReplaceUploads UserPermission = 1 << iota
UserPermListOtherUploads
UserPermEditOtherUploads
UserPermReplaceOtherUploads
UserPermDeleteOtherUploads
UserPermManageLogs
UserPermManageApiKeys
UserPermManageUsers
)
const UserPermissionNone UserPermission = 0
const UserPermissionAll UserPermission = 255
// GrantPermission grants one or more permissions
func (u *User) GrantPermission(permission UserPermission) {
u.Permissions |= permission
}
// RemovePermission revokes one or more permissions
func (u *User) RemovePermission(permission UserPermission) {
u.Permissions &^= permission
}
// HasPermission returns true if the key has the permission(s)
func (u *User) HasPermission(permission UserPermission) bool {
if permission == UserPermissionNone {
return true
}
return (u.Permissions & permission) == permission
}
// HasPermissionReplace returns true if the user has the permission UserPermReplaceUploads
func (u *User) HasPermissionReplace() bool {
return u.HasPermission(UserPermReplaceUploads)
}
// HasPermissionListOtherUploads returns true if the user has the permission UserPermListOtherUploads
func (u *User) HasPermissionListOtherUploads() bool {
return u.HasPermission(UserPermListOtherUploads)
}
// HasPermissionEditOtherUploads returns true if the user has the permission UserPermEditOtherUploads
func (u *User) HasPermissionEditOtherUploads() bool {
return u.HasPermission(UserPermEditOtherUploads)
}
// HasPermissionReplaceOtherUploads returns true if the user has the permission UserPermReplaceOtherUploads
func (u *User) HasPermissionReplaceOtherUploads() bool {
return u.HasPermission(UserPermReplaceOtherUploads)
}
// HasPermissionDeleteOtherUploads returns true if the user has the permission UserPermDeleteOtherUploads
func (u *User) HasPermissionDeleteOtherUploads() bool {
return u.HasPermission(UserPermDeleteOtherUploads)
}
// HasPermissionManageLogs returns true if the user has the permission UserPermManageLogs
func (u *User) HasPermissionManageLogs() bool {
return u.HasPermission(UserPermManageLogs)
}
// HasPermissionManageApi returns true if the user has the permission UserPermManageApiKeys
func (u *User) HasPermissionManageApi() bool {
return u.HasPermission(UserPermManageApiKeys)
}
// HasPermissionManageUsers returns true if the user has the permission UserPermManageUsers
func (u *User) HasPermissionManageUsers() bool {
return u.HasPermission(UserPermManageUsers)
}
+280
View File
@@ -0,0 +1,280 @@
package models
import (
"github.com/forceu/gokapi/internal/test"
"os"
"testing"
"time"
)
func TestUser_GetReadableDate(t *testing.T) {
u := User{
Id: 50,
Name: "Admin",
Permissions: UserPermissionAll,
UserLevel: UserLevelSuperAdmin,
LastOnline: 0,
Password: "1234",
ResetPassword: false,
}
date := u.GetReadableDate()
test.IsEqualString(t, date, "Never")
u.LastOnline = time.Now().Unix() - 10
date = u.GetReadableDate()
test.IsEqualString(t, date, "Online")
u.LastOnline = 1736276120
lastTz := os.Getenv("TZ")
err := os.Setenv("TZ", "Europe/Berlin")
test.IsNil(t, err)
date = u.GetReadableDate()
test.IsEqualString(t, date, "2025-01-07 19:55")
err = os.Setenv("TZ", lastTz)
test.IsNil(t, err)
}
func TestUserPermAll(t *testing.T) {
user := &User{}
user.GrantPermission(UserPermissionAll)
if !user.HasPermission(UserPermReplaceUploads) ||
!user.HasPermission(UserPermListOtherUploads) ||
!user.HasPermission(UserPermEditOtherUploads) ||
!user.HasPermission(UserPermDeleteOtherUploads) ||
!user.HasPermission(UserPermReplaceOtherUploads) ||
!user.HasPermission(UserPermManageLogs) ||
!user.HasPermission(UserPermManageApiKeys) ||
!user.HasPermission(UserPermManageUsers) {
t.Errorf("expected all permissions to be set")
}
}
// Helper function to check only one permission is set
func checkOnlyUserPermissionSet(t *testing.T, user *User, perm UserPermission) {
allPermissions := []struct {
perm UserPermission
permName string
}{
{UserPermReplaceUploads, "UserPermReplaceUploads"},
{UserPermListOtherUploads, "UserPermListOtherUploads"},
{UserPermEditOtherUploads, "UserPermEditOtherUploads"},
{UserPermDeleteOtherUploads, "UserPermDeleteOtherUploads"},
{UserPermReplaceOtherUploads, "UserPermReplaceOtherUploads"},
{UserPermManageLogs, "UserPermManageLogs"},
{UserPermManageApiKeys, "UserPermManageApiKeys"},
{UserPermManageUsers, "UserPermManageUsers"},
}
for _, p := range allPermissions {
if p.perm == perm {
if !user.HasPermission(p.perm) {
t.Errorf("expected permission %s to be set", p.permName)
}
} else {
if user.HasPermission(p.perm) {
t.Errorf("expected permission %s not to be set", p.permName)
}
}
}
}
func TestSetIndividualUserPermissions(t *testing.T) {
user := &User{}
// Test each individual permission
permissions := []struct {
perm UserPermission
permName string
}{
{UserPermReplaceUploads, "UserPermReplaceUploads"},
{UserPermListOtherUploads, "UserPermListOtherUploads"},
{UserPermEditOtherUploads, "UserPermEditOtherUploads"},
{UserPermDeleteOtherUploads, "UserPermDeleteOtherUploads"},
{UserPermReplaceOtherUploads, "UserPermReplaceOtherUploads"},
{UserPermManageLogs, "UserPermManageLogs"},
{UserPermManageApiKeys, "UserPermManageApiKeys"},
{UserPermManageUsers, "UserPermManageUsers"},
}
for _, p := range permissions {
user.Permissions = UserPermissionNone // reset permissions
user.GrantPermission(p.perm)
checkOnlyUserPermissionSet(t, user, p.perm)
}
}
// Helper function to check combined permissions are set
func checkCombinedUserPermissions(t *testing.T, user *User, perms []UserPermission) {
for _, perm := range perms {
if !user.HasPermission(perm) {
t.Errorf("expected permission %d to be set", perm)
}
}
}
func TestSetCombinedUserPermissions(t *testing.T) {
user := &User{}
allPermissions := []UserPermission{
UserPermReplaceUploads,
UserPermListOtherUploads,
UserPermEditOtherUploads,
UserPermDeleteOtherUploads,
UserPermReplaceOtherUploads,
UserPermManageLogs,
UserPermManageApiKeys,
UserPermManageUsers,
}
// Test setting permissions in combination
for i := 0; i < len(allPermissions); i++ {
user.Permissions = UserPermissionNone // reset permissions
for j := 0; j <= i; j++ {
user.GrantPermission(allPermissions[j])
}
checkCombinedUserPermissions(t, user, allPermissions[:i+1])
}
}
func TestUser_GetReadableUserLevel(t *testing.T) {
user := &User{
UserLevel: UserLevelSuperAdmin,
}
test.IsEqualString(t, user.GetReadableUserLevel(), "Super Admin")
user.UserLevel = UserLevelAdmin
test.IsEqualString(t, user.GetReadableUserLevel(), "Admin")
user.UserLevel = UserLevelUser
test.IsEqualString(t, user.GetReadableUserLevel(), "User")
user.UserLevel = 4
test.IsEqualString(t, user.GetReadableUserLevel(), "Invalid")
}
func TestUser_IsSuperAdmin(t *testing.T) {
user := &User{
UserLevel: UserLevelSuperAdmin,
}
test.IsEqualBool(t, user.IsSuperAdmin(), true)
user.UserLevel = UserLevelAdmin
test.IsEqualBool(t, user.IsSuperAdmin(), false)
user.UserLevel = UserLevelUser
test.IsEqualBool(t, user.IsSuperAdmin(), false)
user.UserLevel = 4
test.IsEqualBool(t, user.IsSuperAdmin(), false)
}
func TestUser_IsSameUser(t *testing.T) {
user := &User{
Id: 5,
}
test.IsEqualBool(t, user.IsSameUser(5), true)
test.IsEqualBool(t, user.IsSameUser(0), false)
}
func TestSetUserPermission(t *testing.T) {
user := &User{}
user.GrantPermission(UserPermListOtherUploads)
if !user.HasPermission(UserPermListOtherUploads) {
t.Errorf("expected permission %d to be set", UserPermListOtherUploads)
}
if user.HasPermission(UserPermReplaceOtherUploads) {
t.Errorf("expected permission %d to be not set", UserPermReplaceOtherUploads)
}
}
func TestRemoveUserPermission(t *testing.T) {
user := &User{}
user.GrantPermission(UserPermManageUsers)
if !user.HasPermission(UserPermManageUsers) {
t.Errorf("expected permission %d to be set", UserPermManageUsers)
}
user.RemovePermission(UserPermManageUsers)
if user.HasPermission(UserPermManageUsers) {
t.Errorf("expected permission %d to be removed", UserPermManageUsers)
}
}
func TestHasUserPermission(t *testing.T) {
user := &User{}
if !user.HasPermission(UserPermissionNone) {
t.Errorf("expected ApiPermNone to always return true")
}
if user.HasPermission(UserPermManageUsers) {
t.Errorf("expected permission %d not to be set", UserPermManageUsers)
}
user.GrantPermission(UserPermManageUsers)
if !user.HasPermission(UserPermManageUsers) {
t.Errorf("expected permission %d to be set", UserPermManageUsers)
}
if user.HasPermission(UserPermReplaceOtherUploads) {
t.Errorf("expected permission %d not to be set", UserPermReplaceOtherUploads)
}
}
func TestUser_HasPermissionReplace(t *testing.T) {
user := &User{}
if user.HasPermissionReplace() {
t.Errorf("expected replace permission to be not set")
}
user.GrantPermission(UserPermReplaceUploads)
if !user.HasPermissionReplace() {
t.Errorf("expected replace permission to be set")
}
}
func TestUser_HasPermissionListOtherUploads(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionListOtherUploads(), false)
user.GrantPermission(UserPermListOtherUploads)
test.IsEqualBool(t, user.HasPermissionListOtherUploads(), true)
}
func TestUser_HasPermissionEditOtherUploads(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionEditOtherUploads(), false)
user.GrantPermission(UserPermEditOtherUploads)
test.IsEqualBool(t, user.HasPermissionEditOtherUploads(), true)
}
func TestUser_HasPermissionDeleteOtherUploads(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionDeleteOtherUploads(), false)
user.GrantPermission(UserPermDeleteOtherUploads)
test.IsEqualBool(t, user.HasPermissionDeleteOtherUploads(), true)
}
func TestUser_HasPermissionReplaceOtherUploads(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionReplaceOtherUploads(), false)
user.GrantPermission(UserPermReplaceOtherUploads)
test.IsEqualBool(t, user.HasPermissionReplaceOtherUploads(), true)
}
func TestUser_HasPermissionManageLogs(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionManageLogs(), false)
user.GrantPermission(UserPermManageLogs)
test.IsEqualBool(t, user.HasPermissionManageLogs(), true)
}
func TestUser_HasPermissionManageApi(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionManageApi(), false)
user.GrantPermission(UserPermManageApiKeys)
test.IsEqualBool(t, user.HasPermissionManageApi(), true)
}
func TestUser_HasPermissionManageUsers(t *testing.T) {
user := &User{}
test.IsEqualBool(t, user.HasPermissionManageUsers(), false)
user.GrantPermission(UserPermManageUsers)
test.IsEqualBool(t, user.HasPermissionManageUsers(), true)
}
func TestUser_ToJson(t *testing.T) {
user := &User{
Id: 4,
Name: "Test User",
Permissions: UserPermissionAll,
UserLevel: UserLevelAdmin,
LastOnline: 1337,
Password: "1234",
ResetPassword: true,
}
test.IsEqualString(t, user.ToJson(), `{"id":4,"name":"Test User","permissions":255,"userLevel":1,"lastOnline":1337,"resetPassword":true}`)
}
+35 -15
View File
@@ -47,7 +47,7 @@ var ErrorFileNotFound = errors.New("file not found")
// NewFile creates a new file in the system. Called after an upload from the API has been completed. If a file with the same sha1 hash
// already exists, it is deduplicated. This function gathers information about the file, creates an ID and saves
// it into the global configuration. It is now only used by the API, the web UI uses NewFileFromChunk
func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, uploadRequest models.UploadRequest) (models.File, error) {
func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, userId int, uploadRequest models.UploadRequest) (models.File, error) {
if !isAllowedFileSize(fileHeader.Size) {
return models.File{}, ErrorFileTooLarge
}
@@ -58,7 +58,7 @@ func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, uploadRequ
if err != nil {
return models.File{}, err
}
file := createNewMetaData(hex.EncodeToString(hash), header, uploadRequest)
file := createNewMetaData(hex.EncodeToString(hash), header, userId, uploadRequest)
file.Encryption = encInfo
filename := configuration.Get().DataDir + "/" + file.SHA1
dataDir := configuration.Get().DataDir
@@ -132,10 +132,23 @@ func validateChunkInfo(file *os.File, fileHeader chunking.FileHeader) error {
return nil
}
// GetUploadCounts returns the currently uploaded files per user
func GetUploadCounts() map[int]int {
result := make(map[int]int)
timeNow := time.Now().Unix()
files := database.GetAllMetadata()
for _, file := range files {
if !IsExpiredFile(file, timeNow) {
result[file.UserId] = result[file.UserId] + 1
}
}
return result
}
// NewFileFromChunk creates a new file in the system after a chunk upload has fully completed. If a file with the same sha1 hash
// already exists, it is deduplicated. This function gathers information about the file, creates an ID and saves
// it into the global configuration.
func NewFileFromChunk(chunkId string, fileHeader chunking.FileHeader, uploadRequest models.UploadRequest) (models.File, error) {
func NewFileFromChunk(chunkId string, fileHeader chunking.FileHeader, userId int, uploadRequest models.UploadRequest) (models.File, error) {
file, err := chunking.GetFileByChunkId(chunkId)
if err != nil {
return models.File{}, err
@@ -151,7 +164,7 @@ func NewFileFromChunk(chunkId string, fileHeader chunking.FileHeader, uploadRequ
if err != nil {
return models.File{}, err
}
metaData := createNewMetaData(hash, fileHeader, uploadRequest)
metaData := createNewMetaData(hash, fileHeader, userId, uploadRequest)
fileExists := FileExists(metaData, configuration.Get().DataDir)
if fileExists {
fileExists = copyEncryptionInfo(&metaData)
@@ -273,7 +286,7 @@ func FormatTimestamp(timestamp int64) string {
return time.Unix(timestamp, 0).Format("2006-01-02 15:04")
}
func createNewMetaData(hash string, fileHeader chunking.FileHeader, uploadRequest models.UploadRequest) models.File {
func createNewMetaData(hash string, fileHeader chunking.FileHeader, userId int, uploadRequest models.UploadRequest) models.File {
file := models.File{
Id: createNewId(),
Name: fileHeader.Filename,
@@ -287,6 +300,7 @@ func createNewMetaData(hash string, fileHeader chunking.FileHeader, uploadReques
UnlimitedTime: uploadRequest.UnlimitedTime,
UnlimitedDownloads: uploadRequest.UnlimitedDownload,
PasswordHash: configuration.HashPassword(uploadRequest.Password, true),
UserId: userId,
}
if uploadRequest.IsEndToEndEncrypted {
file.Encryption = models.EncryptionInfo{IsEndToEndEncrypted: true, IsEncrypted: true}
@@ -372,18 +386,26 @@ func ReplaceFile(fileId, newFileContentId string, delete bool) (models.File, err
return file, nil
}
func isChangeRequested(parametersToChange, parameter int) bool {
return parametersToChange&parameter != 0
}
// DuplicateFile creates a copy of an existing file with new parameters
func DuplicateFile(file models.File, parametersToChange int, newFileName string, fileParameters models.UploadRequest) (models.File, error) {
// apiDuplicateFile expects fileParameters.IsEndToEndEncrypted and fileParameters.RealSize not to be used,
// change in apiDuplicateFile if using in this function!
var newFile models.File
err := copier.Copy(&newFile, &file)
if err != nil {
return models.File{}, err
}
changeExpiry := parametersToChange&ParamExpiry != 0
changeDownloads := parametersToChange&ParamDownloads != 0
changePassword := parametersToChange&ParamPassword != 0
changeName := parametersToChange&ParamName != 0
changeExpiry := isChangeRequested(parametersToChange, ParamExpiry)
changeDownloads := isChangeRequested(parametersToChange, ParamDownloads)
changePassword := isChangeRequested(parametersToChange, ParamPassword)
changeName := isChangeRequested(parametersToChange, ParamName)
if changeExpiry {
newFile.ExpireAt = fileParameters.ExpiryTimestamp
@@ -467,13 +489,9 @@ func isEncryptionRequested() bool {
switch configuration.Get().Encryption.Level {
case encryption.NoEncryption:
return false
case encryption.LocalEncryptionStored:
fallthrough
case encryption.LocalEncryptionInput:
case encryption.LocalEncryptionStored, encryption.LocalEncryptionInput:
return !aws.IsAvailable()
case encryption.FullEncryptionStored:
fallthrough
case encryption.FullEncryptionInput:
case encryption.FullEncryptionStored, encryption.FullEncryptionInput:
return true
case encryption.EndToEndEncryption:
return false
@@ -740,6 +758,8 @@ func deleteSource(file models.File, dataDir string) {
// DeleteFile is called when an admin requests deletion of a file
// Returns true if file was deleted or false if ID did not exist
// deleteSource forces a clean-up and will delete the source if it is not
// used by a different file
func DeleteFile(keyId string, deleteSource bool) bool {
if keyId == "" {
return false
+25 -18
View File
@@ -2,6 +2,7 @@ package storage
import (
"bytes"
"fmt"
"github.com/forceu/gokapi/internal/configuration"
"github.com/forceu/gokapi/internal/configuration/cloudconfig"
"github.com/forceu/gokapi/internal/configuration/database"
@@ -43,7 +44,10 @@ var idNewFile string
func TestGetFile(t *testing.T) {
_, result := GetFile("invalid")
test.IsEqualBool(t, result, false)
file, result := GetFile("Wzol7LyY2QVczXynJtVo")
fmt.Println(configuration.Get().DataDir)
fmt.Println(configuration.Get().DatabaseUrl)
test.IsEqualBool(t, result, true)
test.IsEqualString(t, file.Id, "Wzol7LyY2QVczXynJtVo")
test.IsEqualString(t, file.Name, "smallfile2")
@@ -131,6 +135,7 @@ type testFile struct {
File models.File
Request models.UploadRequest
Header multipart.FileHeader
UserId int
Content []byte
}
@@ -156,12 +161,13 @@ func createRawTestFile(content []byte) (multipart.FileHeader, models.UploadReque
func createTestFile() (testFile, error) {
content := []byte("This is a file for testing purposes")
header, request := createRawTestFile(content)
file, err := NewFile(bytes.NewReader(content), &header, request)
file, err := NewFile(bytes.NewReader(content), &header, 63, request)
return testFile{
File: file,
Request: request,
Header: header,
Content: content,
UserId: 63,
}, err
}
@@ -205,18 +211,19 @@ func TestNewFile(t *testing.T) {
idNewFile = file.Id
request.UnlimitedDownload = true
file, err = NewFile(bytes.NewReader(content), &header, request)
file, err = NewFile(bytes.NewReader(content), &header, 99, request)
test.IsNil(t, err)
test.IsEqualInt(t, file.UserId, 99)
test.IsEqualBool(t, file.UnlimitedTime, false)
test.IsEqualBool(t, file.UnlimitedDownloads, true)
request.UnlimitedDownload = false
request.UnlimitedTime = true
file, err = NewFile(bytes.NewReader(content), &header, request)
file, err = NewFile(bytes.NewReader(content), &header, 99, request)
test.IsNil(t, err)
test.IsEqualBool(t, file.UnlimitedTime, true)
test.IsEqualBool(t, file.UnlimitedDownloads, false)
request.UnlimitedDownload = true
file, err = NewFile(bytes.NewReader(content), &header, request)
file, err = NewFile(bytes.NewReader(content), &header, 99, request)
test.IsNil(t, err)
test.IsEqualBool(t, file.UnlimitedTime, true)
test.IsEqualBool(t, file.UnlimitedDownloads, true)
@@ -238,7 +245,7 @@ func TestNewFile(t *testing.T) {
MaxMemory: 10,
}
// Also testing renaming of temp file
file, err = NewFile(bigFile, &header, request)
file, err = NewFile(bigFile, &header, 99, request)
test.IsNil(t, err)
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, true)
@@ -270,7 +277,7 @@ func TestNewFile(t *testing.T) {
ExpiryTimestamp: 2147483600,
MaxMemory: 10,
}
file, err = NewFile(bigFile, &header, request)
file, err = NewFile(bigFile, &header, 99, request)
test.IsNotNil(t, err)
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, false)
@@ -296,7 +303,7 @@ func TestNewFile(t *testing.T) {
createBigFile("bigfile", 20)
header.Size = int64(20) * 1024 * 1024
bigFile, _ = os.Open("bigfile")
file, err = NewFile(bigFile, &header, request)
file, err = NewFile(bigFile, &header, 99, request)
test.IsNil(t, err)
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, true)
@@ -307,7 +314,7 @@ func TestNewFile(t *testing.T) {
database.DeleteMetaData(retrievedFile.Id)
bigFile, _ = os.Open("bigfile")
file, err = NewFile(bigFile, &header, request)
file, err = NewFile(bigFile, &header, 99, request)
test.IsNil(t, err)
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, true)
@@ -333,7 +340,7 @@ func TestNewFile(t *testing.T) {
test.IsEqualBool(t, ok, true)
ok = aws.Init(config.Aws)
test.IsEqualBool(t, ok, true)
file, err = NewFile(bytes.NewReader(content), &header, request)
file, err = NewFile(bytes.NewReader(content), &header, 99, request)
test.IsNil(t, err)
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, true)
@@ -348,7 +355,7 @@ func TestNewFileFromChunk(t *testing.T) {
test.FileDoesNotExist(t, "test/data/6cca7a6905774e6d61a77dca3ad7a1f44581d6ab")
id, header, request, err := createTestChunk()
test.IsNil(t, err)
file, err := NewFileFromChunk(id, header, request)
file, err := NewFileFromChunk(id, header, 99, request)
test.IsNil(t, err)
test.IsEqualString(t, file.Name, "test.dat")
test.IsEqualString(t, file.Size, "41 B")
@@ -366,14 +373,14 @@ func TestNewFileFromChunk(t *testing.T) {
test.FileDoesNotExist(t, "test/data/chunk-"+id)
retrievedFile, ok := database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, true)
test.IsEqualStruct(t, file, retrievedFile)
test.IsEqual(t, file, retrievedFile)
id, header, request, err = createTestChunk()
header.Filename = "newfile"
request.UnlimitedTime = true
request.UnlimitedDownload = true
test.IsNil(t, err)
file, err = NewFileFromChunk(id, header, request)
file, err = NewFileFromChunk(id, header, 99, request)
test.IsNil(t, err)
test.IsEqualString(t, file.Name, "newfile")
test.IsEqualString(t, file.Size, "41 B")
@@ -391,19 +398,19 @@ func TestNewFileFromChunk(t *testing.T) {
test.FileDoesNotExist(t, "test/data/chunk-"+id)
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualBool(t, ok, true)
test.IsEqualStruct(t, file, retrievedFile)
test.IsEqual(t, file, retrievedFile)
err = os.Remove("test/data/6cca7a6905774e6d61a77dca3ad7a1f44581d6ab")
test.IsNil(t, err)
_, err = NewFileFromChunk("invalid", header, request)
_, err = NewFileFromChunk("invalid", header, 99, request)
test.IsNotNil(t, err)
id, header, request, err = createTestChunk()
test.IsNil(t, err)
header.Size = 100000
file, err = NewFileFromChunk(id, header, request)
file, err = NewFileFromChunk(id, header, 99, request)
test.IsNotNil(t, err)
_, err = NewFileFromChunk("", header, request)
_, err = NewFileFromChunk("", header, 99, request)
test.IsNotNil(t, err)
if aws.IsIncludedInBuild {
@@ -414,12 +421,12 @@ func TestNewFileFromChunk(t *testing.T) {
test.IsEqualBool(t, ok, true)
id, header, request, err = createTestChunk()
test.IsNil(t, err)
file, err = NewFileFromChunk(id, header, request)
file, err = NewFileFromChunk(id, header, 99, request)
test.IsNil(t, err)
test.IsEqualBool(t, file.AwsBucket != "", true)
test.IsEqualString(t, file.SHA1, "6cca7a6905774e6d61a77dca3ad7a1f44581d6ab")
retrievedFile, ok = database.GetMetaDataById(file.Id)
test.IsEqualStruct(t, file, retrievedFile)
test.IsEqual(t, file, retrievedFile)
test.IsEqualBool(t, ok, true)
testconfiguration.DisableS3()
}
+2 -6
View File
@@ -114,9 +114,7 @@ func parseContentType(r *http.Request) string {
}
fileExt := strings.ToLower(filepath.Ext(r.Form.Get("filename")))
switch fileExt {
case ".jpeg":
fallthrough
case ".jpg":
case ".jpg", ".jpeg":
contentType = "image/jpeg"
case ".png":
contentType = "image/png"
@@ -128,9 +126,7 @@ func parseContentType(r *http.Request) string {
contentType = "image/bmp"
case ".svg":
contentType = "image/svg+xml"
case ".tiff":
fallthrough
case ".tif":
case ".tif", ".tiff":
contentType = "image/tiff"
case ".ico":
contentType = "image/vnd.microsoft.icon"
@@ -8,6 +8,7 @@ import (
)
func TestSetStatus(t *testing.T) {
isGbStarted = true
const id = "testchunk"
status, ok := getStatus(id)
test.IsEqualBool(t, ok, false)
+6 -10
View File
@@ -43,7 +43,7 @@ func ResponseBodyContains(t MockT, got *httptest.ResponseRecorder, want string)
result, err := io.ReadAll(got.Result().Body)
IsNil(t, err)
if !strings.Contains(string(result), want) {
t.Errorf("Assertion failed, got: %s, want: %s.", got, want)
t.Errorf("Assertion failed, got: %v \n want: %s.\n\n", got, want)
}
}
@@ -63,14 +63,6 @@ func IsEqualBool(t MockT, got, want bool) {
}
}
// IsEqualStruct fails test if got and want are not identical
func IsEqualStruct(t MockT, got, want any) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("Assertion failed, got: %+v, want: %+v.", got, want)
}
}
// IsEqualInt fails test if got and want are not identical
func IsEqualInt(t MockT, got, want int) {
t.Helper()
@@ -172,6 +164,7 @@ func IsNotNil(t MockT, got any) {
}
}
// IsEqual fails test if got and want are not identical
func IsEqual(t MockT, got, expected any) {
t.Helper()
if !reflect.DeepEqual(got, expected) {
@@ -293,7 +286,7 @@ func checkResponse(t MockT, response *http.Response, config HttpTestConfig) {
t.Helper()
IsEqualBool(t, response != nil, true)
if response.StatusCode != config.ResultCode {
t.Errorf("Status Code - Got: %d Want: %d", config.ResultCode, response.StatusCode)
t.Errorf("Status Code - Got: %d Want: %d", response.StatusCode, config.ResultCode)
}
content, err := io.ReadAll(response.Body)
@@ -422,6 +415,9 @@ func HttpPostRequest(t MockT, config HttpTestConfig) []*http.Cookie {
})
}
r.Header.Set("Content-type", "application/x-www-form-urlencoded")
for _, header := range config.Headers {
r.Header.Set(header.Name, header.Value)
}
client := &http.Client{}
response, err := client.Do(r)
IsNil(t, err)
+19 -2
View File
@@ -70,12 +70,14 @@ func TestFunctions(t *testing.T) {
mockT.WantNoFail()
IsNil(mockT, nil)
mockT.WantNoFail()
IsEqualByteSlice(mockT, []byte("test"), []byte("test"))
mockT.WantNoFail()
FileDoesNotExist(mockT, "testfile")
os.WriteFile("testfile", []byte("content"), 0777)
mockT.WantNoFail()
FileExists(mockT, "testfile")
mockT.WantNoFail()
IsEqualStruct(mockT, testStruct{Value1: 1337}, testStruct{Value1: 1337})
IsEqual(mockT, testStruct{Value1: 1337}, testStruct{Value1: 1337})
mockT.WantNoFail()
IsNotNil(mockT, errors.New("hello"))
@@ -86,6 +88,8 @@ func TestFunctions(t *testing.T) {
mockT.WantFail()
IsNotEqualString(mockT, "test", "test")
mockT.WantFail()
IsEqualByteSlice(mockT, []byte("test1"), []byte("test2"))
mockT.WantFail()
IsEqualBool(mockT, true, false)
mockT.WantFail()
IsEqualInt(mockT, 1, 2)
@@ -111,7 +115,7 @@ func TestFunctions(t *testing.T) {
mockT.WantFail()
FileExists(mockT, "testfile")
mockT.WantFail()
IsEqualStruct(mockT, testStruct{Value1: 1337}, testStruct{Value1: 1338})
IsEqual(mockT, testStruct{Value1: 1337}, testStruct{Value1: 1338})
mockT.Check()
}
@@ -130,6 +134,19 @@ func TestMockInputStdin(t *testing.T) {
IsEqualString(t, result, "test input")
}
func TestFolderExists(t *testing.T) {
mockT := MockTest{reference: t}
mockT.WantFail()
FolderExists(mockT, "testfolder")
os.Mkdir("testfolder", 0777)
mockT.WantNoFail()
FolderExists(mockT, "testfolder")
mockT.WantFail()
os.RemoveAll("testfolder")
FolderExists(mockT, "testfolder")
mockT.Check()
}
func TestHttpPageResult(t *testing.T) {
startTestServer()
HttpPageResult(t, HttpTestConfig{
@@ -3,6 +3,9 @@
package testconfiguration
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"github.com/forceu/gokapi/internal/configuration/database"
"github.com/forceu/gokapi/internal/helper"
@@ -22,6 +25,8 @@ const (
baseDir = "test"
dataDir = baseDir + "/data"
configFile = baseDir + "/config.json"
SqliteUrl = "sqlite://" + dataDir + "/gokapi.sqlite"
SaltAdmin = "LW6fW4Pjv8GtdWVLSZD66gYEev6NAaXxOVBw7C"
)
func SetDirEnv() {
@@ -37,10 +42,6 @@ func SetDirEnv() {
}
}
func GetSqliteUrl() string {
return "sqlite://" + dataDir + "/gokapi.sqlite"
}
// Create creates a configuration for unit testing. If initFiles is set, test metaData and content is created
func Create(initFiles bool) {
SetDirEnv()
@@ -48,11 +49,12 @@ func Create(initFiles bool) {
if err != nil {
panic(err)
}
config, err := database.ParseUrl(GetSqliteUrl(), false)
config, err := database.ParseUrl(SqliteUrl, false)
if err != nil {
panic(err)
}
database.Connect(config)
writeUsers()
writeTestSessions()
writeTestFiles()
database.SaveHotlink(models.File{Id: "n1tSTAGj8zan9KaT4u6p", HotlinkId: "PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", ExpireAt: time.Now().Add(time.Hour).Unix()})
@@ -71,6 +73,43 @@ func Create(initFiles bool) {
}
}
func writeUsers() {
admin := models.User{
Id: 5,
Name: "Test",
Permissions: models.UserPermissionAll,
UserLevel: models.UserLevelSuperAdmin,
LastOnline: 0,
Password: hashSalt("adminadmin", "LW6fW4Pjv8GtdWVLSZD66gYEev6NAaXxOVBw7C"),
ResetPassword: false,
}
user := models.User{
Id: 7,
Name: "User",
Permissions: models.UserPermissionNone,
UserLevel: models.UserLevelUser,
LastOnline: 0,
Password: hashSalt("useruser", "LW6fW4Pjv8GtdWVLSZD66gYEev6NAaXxOVBw7C"),
ResetPassword: false,
}
database.SaveUser(admin, false)
database.SaveUser(user, false)
}
// Copied from configuration
func hashSalt(password, salt string) string {
if password == "" {
return ""
}
if salt == "" {
panic(errors.New("no salt provided"))
}
pwBytes := []byte(password + salt)
hash := sha1.New()
hash.Write(pwBytes)
return hex.EncodeToString(hash.Sum(nil))
}
// WriteEncryptedFile writes metadata for an encrypted file and returns the id
func WriteEncryptedFile() string {
name := helper.GenerateRandomString(10)
@@ -167,18 +206,37 @@ func writeTestSessions() {
database.SaveSession("validsession", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483646,
UserId: 7,
})
database.SaveSession("logoutsession", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483646,
UserId: 7,
})
database.SaveSession("logoutsession2", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483646,
UserId: 7,
})
database.SaveSession("needsRenewal", models.Session{
RenewAt: 0,
ValidUntil: 2147483646,
UserId: 7,
})
database.SaveSession("expiredsession", models.Session{
RenewAt: 0,
ValidUntil: 0,
UserId: 7,
})
database.SaveSession("validSessionInvalidUser", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 5000,
})
database.SaveSession("validSessionInvalidUser", models.Session{
RenewAt: 2147483645,
ValidUntil: 2147483645,
UserId: 5000,
})
}
func writeTestUploadStatus() {
@@ -197,22 +255,37 @@ func writeApiKeys() {
Id: "validkey",
FriendlyName: "First Key",
Permissions: models.ApiPermAll, // TODO
UserId: 5,
PublicId: "taiyeo6uLie6nu6eip0ieweiM5mahv",
})
database.SaveApiKey(models.ApiKey{
Id: "validkeyid7",
FriendlyName: "Key for uid 7",
Permissions: models.ApiPermAll, // TODO
UserId: 7,
PublicId: "vu0eemi8eehaisuth3pahDai2eo6ze",
})
database.SaveApiKey(models.ApiKey{
Id: "GAh1IhXDvYnqfYLazWBqMB9HSFmNPO",
FriendlyName: "Second Key",
LastUsed: 1620671580,
Permissions: models.ApiPermAll, // TODO
UserId: 5,
PublicId: "yaeVohng1ohNohsh1vailizeil5ka5",
})
database.SaveApiKey(models.ApiKey{
Id: "jiREglQJW0bOqJakfjdVfe8T1EM8n8",
FriendlyName: "Unnamed Key",
Permissions: models.ApiPermAll, // TODO
UserId: 5,
PublicId: "ahYie4ophoo5OoGhahCe1neic6thah",
})
database.SaveApiKey(models.ApiKey{
Id: "okeCMWqhVMZSpt5c1qpCWhKvJJPifb",
FriendlyName: "Unnamed Key",
Permissions: models.ApiPermAll, // TODO
UserId: 5,
PublicId: "ugoo0roowoanahthei7ohSail5OChu",
})
}
@@ -226,6 +299,7 @@ func writeTestFiles() {
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 1,
ContentType: "text/html",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "e4TjE7CokWK0giiLNxDL",
@@ -236,6 +310,7 @@ func writeTestFiles() {
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 2,
ContentType: "text/html",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "wefffewhtrhhtrhtrhtr",
@@ -246,6 +321,7 @@ func writeTestFiles() {
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 1,
ContentType: "text/html",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "deletedfile123456789",
@@ -256,6 +332,7 @@ func writeTestFiles() {
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 2,
ContentType: "text/html",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "jpLXGJKigM4hjtA6T6sN",
@@ -267,6 +344,7 @@ func writeTestFiles() {
DownloadsRemaining: 1,
ContentType: "text/html",
PasswordHash: "7b30508aa9b233ab4b8a11b2af5816bdb58ca3e7",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "jpLXGJKigM4hjtA6T6sN2",
@@ -278,6 +356,7 @@ func writeTestFiles() {
DownloadsRemaining: 1,
ContentType: "text/html",
PasswordHash: "7b30508aa9b233ab4b8a11b2af5816bdb58ca3e7",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "n1tSTAGj8zan9KaT4u6p",
@@ -289,6 +368,7 @@ func writeTestFiles() {
DownloadsRemaining: 1,
ContentType: "text/html",
HotlinkId: "PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "cleanuptest123456789",
@@ -299,6 +379,7 @@ func writeTestFiles() {
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 0,
ContentType: "text/html",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "awsTest1234567890123",
@@ -310,6 +391,7 @@ func writeTestFiles() {
DownloadsRemaining: 4,
ContentType: "application/octet-stream",
AwsBucket: "gokapi-test",
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "unlimitedDownload",
@@ -321,6 +403,7 @@ func writeTestFiles() {
DownloadsRemaining: 0,
ContentType: "text/html",
UnlimitedDownloads: true,
UserId: 5,
})
database.SaveMetaData(models.File{
Id: "unlimitedTime",
@@ -332,21 +415,20 @@ func writeTestFiles() {
DownloadsRemaining: 1,
ContentType: "text/html",
UnlimitedTime: true,
UserId: 5,
})
}
var configTestFile = []byte(`{
"Authentication": {
"Method": 0,
"SaltAdmin": "LW6fW4Pjv8GtdWVLSZD66gYEev6NAaXxOVBw7C",
"SaltAdmin": "` + SaltAdmin + `",
"SaltFiles": "lL5wMTtnVCn5TPbpRaSe4vAQodWW0hgk00WCZE",
"Username": "test",
"Password": "10340aece68aa4fb14507ae45b05506026f276cf",
"HeaderKey": "",
"OauthProvider": "",
"OAuthClientId": "",
"OAuthClientSecret": "",
"OauthUserScope": "",
"OauthGroupScope": "",
"OAuthRecheckInterval": 12,
"HeaderUsers": null,
@@ -357,12 +439,20 @@ var configTestFile = []byte(`{
"ServerUrl": "http://127.0.0.1:53843/",
"RedirectUrl": "https://test.com/",
"PublicName": "Gokapi Test Version",
"ConfigVersion": 20,
"DataDir": "` + dataDir + `",
"DatabaseUrl": "` + SqliteUrl + `",
"ConfigVersion": 22,
"LengthId": 20,
"DataDir": "test/data",
"MaxFileSizeMB": 25,
"MaxMemory": 10,
"ChunkSize": 45,
"Encryption": {
"Level": 0,
"Cipher": null,
"Salt": "",
"Checksum": "",
"ChecksumSalt": ""
},
"MaxParallelUploads": 4,
"UseSsl": false,
"PicturesAlwaysLocal": false,
+242 -71
View File
@@ -20,7 +20,6 @@ import (
"github.com/forceu/gokapi/internal/logging"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/storage"
"github.com/forceu/gokapi/internal/storage/processingstatus"
"github.com/forceu/gokapi/internal/webserver/api"
"github.com/forceu/gokapi/internal/webserver/authentication"
"github.com/forceu/gokapi/internal/webserver/authentication/oauth"
@@ -91,32 +90,34 @@ func Start() {
}
loadExpiryImage()
mux.HandleFunc("/admin", requireLogin(showAdminMenu, false))
mux.HandleFunc("/admin", requireLogin(showAdminMenu, true, false))
mux.HandleFunc("/api/", processApi)
mux.HandleFunc("/apiKeys", requireLogin(showApiAdmin, false))
mux.HandleFunc("/apiKeys", requireLogin(showApiAdmin, true, false))
mux.HandleFunc("/changePassword", requireLogin(changePassword, true, true))
mux.HandleFunc("/d", showDownload)
mux.HandleFunc("/downloadFile", downloadFile)
mux.HandleFunc("/e2eInfo", requireLogin(e2eInfo, true))
mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, false))
mux.HandleFunc("/e2eInfo", requireLogin(e2eInfo, false, false))
mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, true, false))
mux.HandleFunc("/error", showError)
mux.HandleFunc("/error-auth", showErrorAuth)
mux.HandleFunc("/error-header", showErrorHeader)
mux.HandleFunc("/error-oauth", showErrorIntOAuth)
mux.HandleFunc("/forgotpw", forgotPassword)
mux.HandleFunc("/hotlink/", showHotlink)
mux.HandleFunc("/index", showIndex)
mux.HandleFunc("/login", showLogin)
mux.HandleFunc("/logs", requireLogin(showLogs, false))
mux.HandleFunc("/logs", requireLogin(showLogs, true, false))
mux.HandleFunc("/logout", doLogout)
mux.HandleFunc("/uploadChunk", requireLogin(uploadChunk, true))
mux.HandleFunc("/uploadComplete", requireLogin(uploadComplete, true))
mux.HandleFunc("/uploadStatus", requireLogin(sse.GetStatusSSE, true))
mux.HandleFunc("/uploadChunk", requireLogin(uploadChunk, false, false))
mux.HandleFunc("/uploadStatus", requireLogin(sse.GetStatusSSE, false, false))
mux.HandleFunc("/users", requireLogin(showUserAdmin, true, false))
mux.Handle("/main.wasm", gziphandler.GzipHandler(http.HandlerFunc(serveDownloadWasm)))
mux.Handle("/e2e.wasm", gziphandler.GzipHandler(http.HandlerFunc(serveE2EWasm)))
mux.HandleFunc("/d/{id}/{filename}", redirectFromFilename)
mux.HandleFunc("/dh/{id}/{filename}", downloadFileWithNameInUrl)
if configuration.Get().Authentication.Method == authentication.OAuth2 {
if configuration.Get().Authentication.Method == models.AuthenticationOAuth2 {
oauth.Init(configuration.Get().ServerUrl, configuration.Get().Authentication)
mux.HandleFunc("/oauth-login", oauth.HandlerLogin)
mux.HandleFunc("/oauth-callback", oauth.HandlerCallback)
@@ -157,8 +158,9 @@ func loadExpiryImage() {
svgTemplate, err := templatetext.ParseFS(templateFolderEmbedded, "web/templates/expired_file_svg.tmpl")
helper.Check(err)
var buf bytes.Buffer
view := UploadView{}
err = svgTemplate.Execute(&buf, view.convertGlobalConfig(ViewMain))
err = svgTemplate.Execute(&buf, struct {
PublicName string
}{PublicName: configuration.Get().PublicName})
helper.Check(err)
imageExpiredPicture = buf.Bytes()
}
@@ -177,12 +179,16 @@ func Shutdown() {
// Otherwise, templateFolderEmbedded will be used.
func initTemplates(templateFolderEmbedded embed.FS) {
var err error
funcMap := template.FuncMap{
"newAdminButtonContext": newAdminButtonContext,
}
if helper.FolderExists("templates") {
fmt.Println("Found folder 'templates', using local folder instead of internal template folder")
templateFolder, err = template.ParseGlob("templates/*.tmpl")
templateFolder, err = template.New("").Funcs(funcMap).ParseGlob("templates/*.tmpl")
helper.Check(err)
} else {
templateFolder, err = template.ParseFS(templateFolderEmbedded, "web/templates/*.tmpl")
templateFolder, err = template.New("").Funcs(funcMap).ParseFS(templateFolderEmbedded, "web/templates/*.tmpl")
helper.Check(err)
}
}
@@ -253,6 +259,57 @@ func showIndex(w http.ResponseWriter, r *http.Request) {
helper.CheckIgnoreTimeout(err)
}
// Handling of /changePassword
func changePassword(w http.ResponseWriter, r *http.Request) {
var errMessage string
user, err := authentication.GetUserFromRequest(r)
if err != nil {
panic(err)
}
if !user.ResetPassword {
redirect(w, "admin")
return
}
err = r.ParseForm()
if err != nil {
fmt.Println("Invalid form data sent to server for /changePassword")
fmt.Println(err)
errMessage = "Invalid form data sent"
} else {
var ok bool
var pwHash string
pw := r.Form.Get("newpw")
errMessage, pwHash, ok = validateNewPassword(pw, user)
if ok {
user.Password = pwHash
user.ResetPassword = false
database.SaveUser(user, false)
redirect(w, "admin")
return
}
}
err = templateFolder.ExecuteTemplate(w, "changepw",
genericView{PublicName: configuration.Get().PublicName,
MinPasswordLength: configuration.MinLengthPassword,
ErrorMessage: errMessage})
helper.CheckIgnoreTimeout(err)
}
func validateNewPassword(newPassword string, user models.User) (string, string, bool) {
if len(newPassword) == 0 {
return "", user.Password, false
}
if len(newPassword) < configuration.MinLengthPassword {
return "Password is too short", user.Password, false
}
newPasswordHash := configuration.HashPassword(newPassword, false)
if user.Password == newPasswordHash {
return "New password has to be different from the old password", user.Password, false
}
return "", newPasswordHash, true
}
// Handling of /error
func showError(w http.ResponseWriter, r *http.Request) {
const invalidFile = 0
@@ -276,6 +333,12 @@ func showErrorAuth(w http.ResponseWriter, r *http.Request) {
helper.CheckIgnoreTimeout(err)
}
// Handling of /error-header
func showErrorHeader(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "error_auth_header", genericView{PublicName: configuration.Get().PublicName})
helper.CheckIgnoreTimeout(err)
}
// Handling of /error-oauth
func showErrorIntOAuth(w http.ResponseWriter, r *http.Request) {
view := oauthErrorView{PublicName: configuration.Get().PublicName}
@@ -296,24 +359,50 @@ func forgotPassword(w http.ResponseWriter, r *http.Request) {
// Handling of /api
// If user is authenticated, this menu lists all uploads and enables uploading new files
func showApiAdmin(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "api", (&UploadView{}).convertGlobalConfig(ViewAPI))
userId, err := authentication.GetUserFromRequest(r)
if err != nil {
panic(err)
}
view := (&UploadView{}).convertGlobalConfig(ViewAPI, userId)
err = templateFolder.ExecuteTemplate(w, "api", view)
helper.CheckIgnoreTimeout(err)
}
// Handling of /users
// If user is authenticated, this menu lists all users
func showUserAdmin(w http.ResponseWriter, r *http.Request) {
userId, err := authentication.GetUserFromRequest(r)
if err != nil {
panic(err)
}
view := (&UploadView{}).convertGlobalConfig(ViewUsers, userId)
if !view.ActiveUser.HasPermissionManageUsers() || configuration.Get().Authentication.Method == models.AuthenticationDisabled {
redirect(w, "admin")
return
}
err = templateFolder.ExecuteTemplate(w, "users", view)
helper.CheckIgnoreTimeout(err)
}
// Handling of /api/
func processApi(w http.ResponseWriter, r *http.Request) {
api.Process(w, r, configuration.Get().MaxMemory)
api.Process(w, r)
}
// Handling of /login
// Shows a login form. If not authenticated, client needs to wait for three seconds.
// If correct, a new session is created and the user is redirected to the admin menu
func showLogin(w http.ResponseWriter, r *http.Request) {
if authentication.IsAuthenticated(w, r) {
_, ok := authentication.IsAuthenticated(w, r)
if ok {
redirect(w, "admin")
return
}
if configuration.Get().Authentication.Method == authentication.OAuth2 {
if configuration.Get().Authentication.Method == models.AuthenticationHeader {
redirect(w, "error-header")
return
}
if configuration.Get().Authentication.Method == models.AuthenticationOAuth2 {
// If user clicked logout, force consent
if r.URL.Query().Has("consent") {
redirect(w, "oauth-login?consent=true")
@@ -332,10 +421,9 @@ func showLogin(w http.ResponseWriter, r *http.Request) {
pw := r.Form.Get("password")
failedLogin := false
if pw != "" && user != "" {
if authentication.IsCorrectUsernameAndPassword(user, pw) {
isOauth := configuration.Get().Authentication.Method == authentication.OAuth2
interval := configuration.Get().Authentication.OAuthRecheckInterval
sessionmanager.CreateSession(w, isOauth, interval)
retrievedUser, validCredentials := authentication.IsCorrectUsernameAndPassword(user, pw)
if validCredentials {
sessionmanager.CreateSession(w, false, 0, retrievedUser.Id)
redirect(w, "admin")
return
}
@@ -445,17 +533,23 @@ func e2eInfo(w http.ResponseWriter, r *http.Request) {
responseError(w, errors.New("invalid action specified"))
return
}
user, err := authentication.GetUserFromRequest(r)
if err != nil {
responseError(w, err)
return
}
switch action[0] {
case "get":
getE2eInfo(w)
getE2eInfo(w, user.Id)
case "store":
storeE2eInfo(w, r)
storeE2eInfo(w, r, user.Id)
default:
responseError(w, errors.New("invalid action specified"))
}
}
func storeE2eInfo(w http.ResponseWriter, r *http.Request) {
func storeE2eInfo(w http.ResponseWriter, r *http.Request, userId int) {
err := r.ParseForm()
if err != nil {
responseError(w, err)
@@ -477,12 +571,12 @@ func storeE2eInfo(w http.ResponseWriter, r *http.Request) {
responseError(w, err)
return
}
database.SaveEnd2EndInfo(info)
database.SaveEnd2EndInfo(info, userId)
_, _ = w.Write([]byte("\"result\":\"OK\""))
}
func getE2eInfo(w http.ResponseWriter) {
info := database.GetEnd2EndInfo()
func getE2eInfo(w http.ResponseWriter, userId int) {
info := database.GetEnd2EndInfo(userId)
bytesE2e, err := json.Marshal(info)
helper.Check(err)
_, _ = w.Write(bytesE2e)
@@ -505,21 +599,36 @@ func queryUrl(w http.ResponseWriter, r *http.Request, redirectUrl string) string
// Handling of /admin
// If user is authenticated, this menu lists all uploads and enables uploading new files
func showAdminMenu(w http.ResponseWriter, r *http.Request) {
user, err := authentication.GetUserFromRequest(r)
if err != nil {
panic(err)
}
if configuration.Get().Encryption.Level == encryption.EndToEndEncryption {
e2einfo := database.GetEnd2EndInfo()
e2einfo := database.GetEnd2EndInfo(user.Id)
if !e2einfo.HasBeenSetUp() {
redirect(w, "e2eSetup")
return
}
}
err := templateFolder.ExecuteTemplate(w, "admin", (&UploadView{}).convertGlobalConfig(ViewMain))
err = templateFolder.ExecuteTemplate(w, "admin", (&UploadView{}).convertGlobalConfig(ViewMain, user))
helper.CheckIgnoreTimeout(err)
}
// Handling of /logs
// If user is authenticated, this menu shows the stored logs
func showLogs(w http.ResponseWriter, r *http.Request) {
err := templateFolder.ExecuteTemplate(w, "logs", (&UploadView{}).convertGlobalConfig(ViewLogs))
userId, err := authentication.GetUserFromRequest(r)
if err != nil {
panic(err)
}
view := (&UploadView{}).convertGlobalConfig(ViewLogs, userId)
if !view.ActiveUser.HasPermissionManageLogs() {
redirect(w, "admin")
return
}
err = templateFolder.ExecuteTemplate(w, "logs", view)
helper.CheckIgnoreTimeout(err)
}
@@ -528,8 +637,13 @@ func showE2ESetup(w http.ResponseWriter, r *http.Request) {
redirect(w, "admin")
return
}
e2einfo := database.GetEnd2EndInfo()
err := templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{HasBeenSetup: e2einfo.HasBeenSetUp(), PublicName: configuration.Get().PublicName})
user, err := authentication.GetUserFromRequest(r)
if err != nil {
panic(err)
}
e2einfo := database.GetEnd2EndInfo(user.Id)
err = templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{HasBeenSetup: e2einfo.HasBeenSetUp(), PublicName: configuration.Get().PublicName})
helper.CheckIgnoreTimeout(err)
}
@@ -561,6 +675,9 @@ type e2ESetupView struct {
type UploadView struct {
Items []models.FileApiOutput
ApiKeys []models.ApiKey
Users []userInfo
ActiveUser models.User
UserMap map[int]*models.User
ServerUrl string
Logs string
PublicName string
@@ -569,8 +686,10 @@ type UploadView struct {
IsDownloadView bool
IsApiView bool
IsLogoutAvailable bool
IsUserTabAvailable bool
EndToEndEncryption bool
IncludeFilename bool
IsInternalAuth bool
MaxFileSize int
ActiveView int
ChunkSize int
@@ -578,25 +697,44 @@ type UploadView struct {
TimeNow int64
}
// ViewMain is the identifier for the main menu
const ViewMain = 0
// getUserMap needs to return the map with pointers, otherwise template cannot call
// functions associated with it
func getUserMap() map[int]*models.User {
result := make(map[int]*models.User)
users := database.GetAllUsers()
for _, user := range users {
result[user.Id] = &user
}
return result
}
// ViewLogs is the identifier for the log viewer menu
const ViewLogs = 1
// ViewAPI is the identifier for the API menu
const ViewAPI = 2
const (
// ViewMain is the identifier for the main menu
ViewMain = iota
// ViewLogs is the identifier for the log viewer menu
ViewLogs
// ViewAPI is the identifier for the API menu
ViewAPI
// ViewUsers is the identifier for the user management menu
ViewUsers
)
// Converts the globalConfig variable to an UploadView struct to pass the infos to
// the admin template
func (u *UploadView) convertGlobalConfig(view int) *UploadView {
func (u *UploadView) convertGlobalConfig(view int, user models.User) *UploadView {
var result []models.FileApiOutput
var resultApi []models.ApiKey
config := configuration.Get()
u.IsInternalAuth = config.Authentication.Method == models.AuthenticationInternal
u.ActiveUser = user
u.UserMap = getUserMap()
switch view {
case ViewMain:
for _, element := range database.GetAllMetadata() {
if element.UserId != user.Id && !user.HasPermissionListOtherUploads() {
continue
}
fileInfo, err := element.ToFileApiOutput(config.ServerUrl, config.IncludeFilename)
helper.Check(err)
result = append(result, fileInfo)
@@ -608,8 +746,19 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
return result[i].ExpireAt > result[j].ExpireAt
})
case ViewAPI:
for _, element := range database.GetAllApiKeys() {
resultApi = append(resultApi, element)
for _, apiKey := range database.GetAllApiKeys() {
// Double-checking if user of API key exists
// If the user was manually deleted from the database, this could lead to a crash
// in the API view
_, ok := u.UserMap[apiKey.UserId]
if !ok {
continue
}
if !apiKey.IsSystemKey {
if apiKey.UserId == user.Id || user.HasPermissionManageApi() {
resultApi = append(resultApi, apiKey)
}
}
}
sort.Slice(resultApi[:], func(i, j int) bool {
if resultApi[i].LastUsed == resultApi[j].LastUsed {
@@ -625,6 +774,20 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
} else {
u.Logs = "Warning: Log file not found!"
}
case ViewUsers:
uploadCounts := storage.GetUploadCounts()
u.Users = make([]userInfo, 0)
for _, userEntry := range database.GetAllUsers() {
userWithUploads := userInfo{
UploadCount: uploadCounts[userEntry.Id],
User: userEntry,
}
// Otherwise the user is not shown as online, if /users is opened as first page
if userEntry.Id == user.Id {
userWithUploads.User.LastOnline = time.Now().Unix()
}
u.Users = append(u.Users, userWithUploads)
}
}
u.ServerUrl = config.ServerUrl
@@ -636,14 +799,20 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
u.ActiveView = view
u.MaxFileSize = config.MaxFileSizeMB
u.IsLogoutAvailable = authentication.IsLogoutAvailable()
u.IsUserTabAvailable = config.Authentication.Method != models.AuthenticationDisabled
u.EndToEndEncryption = config.Encryption.Level == encryption.EndToEndEncryption
u.MaxParallelUploads = config.MaxParallelUploads
u.ChunkSize = config.ChunkSize
u.IncludeFilename = config.IncludeFilename
u.SystemKey = api.GetSystemKey()
u.SystemKey = api.GetSystemKey(user.Id)
return u
}
type userInfo struct {
UploadCount int
User models.User
}
// Handling of /uploadChunk
// If the user is authenticated, this parses the uploaded chunk and stores it
func uploadChunk(w http.ResponseWriter, r *http.Request) {
@@ -657,25 +826,6 @@ func uploadChunk(w http.ResponseWriter, r *http.Request) {
responseError(w, err)
}
// Handling of /uploadComplete
// If the user is authenticated, this parses the uploaded chunk and stores it
func uploadComplete(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
chunkId, header, config, err := fileupload.ParseFileHeader(r)
if err != nil {
responseError(w, err)
return
}
go func() {
_, err = fileupload.CompleteChunk(chunkId, header, config)
if err != nil {
processingstatus.Set(chunkId, processingstatus.StatusError, models.File{}, err)
fmt.Println(err)
}
}()
_, _ = io.WriteString(w, "{\"result\":\"OK\"}")
}
// Outputs an error in json format if err!=nil
func responseError(w http.ResponseWriter, err error) {
if err != nil {
@@ -725,14 +875,23 @@ func serveFile(id string, isRootUrl bool, w http.ResponseWriter, r *http.Request
storage.ServeFile(savedFile, w, r, true)
}
func requireLogin(next http.HandlerFunc, isUpload bool) http.HandlerFunc {
func requireLogin(next http.HandlerFunc, isUiCall, isPwChangeView bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
addNoCacheHeader(w)
if authentication.IsAuthenticated(w, r) {
user, isLoggedIn := authentication.IsAuthenticated(w, r)
if isLoggedIn {
if user.ResetPassword && isUiCall && configuration.Get().Authentication.Method == models.AuthenticationInternal {
if !isPwChangeView {
redirect(w, "changePassword")
return
}
}
c := context.WithValue(r.Context(), "user", user)
r = r.WithContext(c)
next.ServeHTTP(w, r)
return
}
if isUpload {
if !isUiCall {
w.WriteHeader(http.StatusUnauthorized)
_, _ = io.WriteString(w, "{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}")
return
@@ -741,6 +900,16 @@ func requireLogin(next http.HandlerFunc, isUpload bool) http.HandlerFunc {
}
}
type adminButtonContext struct {
CurrentFile models.FileApiOutput
ActiveUser *models.User
}
// Used internally in templates, to create buttons with user context
func newAdminButtonContext(file models.FileApiOutput, user models.User) adminButtonContext {
return adminButtonContext{CurrentFile: file, ActiveUser: &user}
}
// Write a cookie if the user has entered a correct password for a password-protected file
func writeFilePwCookie(w http.ResponseWriter, file models.File) {
http.SetCookie(w, &http.Cookie{
@@ -775,11 +944,13 @@ func addNoCacheHeader(w http.ResponseWriter) {
// A view containing parameters for a generic template
type genericView struct {
IsAdminView bool
IsDownloadView bool
PublicName string
RedirectUrl string
ErrorId int
IsAdminView bool
IsDownloadView bool
PublicName string
RedirectUrl string
ErrorMessage string
ErrorId int
MinPasswordLength int
}
// A view containing parameters for an oauth error
+23 -28
View File
@@ -8,6 +8,7 @@ import (
"errors"
"github.com/forceu/gokapi/internal/configuration"
"github.com/forceu/gokapi/internal/configuration/database"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/storage/processingstatus"
"github.com/forceu/gokapi/internal/test"
"github.com/forceu/gokapi/internal/test/testconfiguration"
@@ -24,6 +25,7 @@ func TestMain(m *testing.M) {
testconfiguration.Create(true)
configuration.Load()
configuration.ConnectDatabase()
authentication.Init(configuration.Get().Authentication)
go Start()
time.Sleep(1 * time.Second)
exitVal := m.Run()
@@ -32,9 +34,13 @@ func TestMain(m *testing.M) {
}
func TestEmbedFs(t *testing.T) {
templates, err := template.ParseFS(templateFolderEmbedded, "web/templates/*.tmpl")
funcMap := template.FuncMap{
"newAdminButtonContext": newAdminButtonContext,
}
templates, err := template.New("").Funcs(funcMap).ParseFS(templateFolderEmbedded, "web/templates/*.tmpl")
if err != nil {
t.Error("Unable to read templates")
return
}
if !strings.Contains(templates.DefinedTemplates(), "header") {
t.Error("Unable to parse templates")
@@ -88,6 +94,7 @@ func TestLogin(t *testing.T) {
ResultCode: 200,
}
test.HttpPostRequest(t, config)
config.PostValues = []test.PostBody{
{
Key: "username",
@@ -100,7 +107,7 @@ func TestLogin(t *testing.T) {
test.HttpPostRequest(t, config)
oauthConfig := configuration.Get()
oauthConfig.Authentication.Method = authentication.OAuth2
oauthConfig.Authentication.Method = models.AuthenticationOAuth2
oauthConfig.Authentication.OAuthProvider = "http://test.com"
oauthConfig.Authentication.OAuthClientSecret = "secret"
oauthConfig.Authentication.OAuthClientId = "client"
@@ -108,7 +115,7 @@ func TestLogin(t *testing.T) {
config.RequiredContent = []string{"\"Refresh\" content=\"0; URL=./oauth-login\""}
config.PostValues = []test.PostBody{}
test.HttpPageResult(t, config)
configuration.Get().Authentication.Method = authentication.Internal
configuration.Get().Authentication.Method = models.AuthenticationInternal
authentication.Init(configuration.Get().Authentication)
buf := config.RequiredContent
@@ -120,7 +127,7 @@ func TestLogin(t *testing.T) {
Value: "test",
}, {
Key: "password",
Value: "testtest",
Value: "adminadmin",
},
}
cookies := test.HttpPostRequest(t, config)
@@ -260,7 +267,7 @@ func TestLoginCorrect(t *testing.T) {
RequiredContent: []string{"URL=./admin\""},
IsHtml: true,
Method: "POST",
PostValues: []test.PostBody{{"username", "test"}, {"password", "testtest"}},
PostValues: []test.PostBody{{"username", "test"}, {"password", "adminadmin"}},
})
}
@@ -442,13 +449,6 @@ func TestPostUploadNoAuth(t *testing.T) {
RequiredContent: []string{"{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}"},
})
test.HttpPostUploadRequest(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/uploadComplete",
UploadFileName: "test/fileupload.jpg",
UploadFieldName: "file",
ResultCode: http.StatusUnauthorized,
RequiredContent: []string{"{\"Result\":\"error\",\"ErrorMessage\":\"Not authenticated\"}"},
})
}
func TestPostUpload(t *testing.T) {
@@ -500,20 +500,15 @@ func TestPostUpload(t *testing.T) {
go func() {
time.Sleep(200 * time.Millisecond)
test.HttpPostRequest(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/uploadComplete",
PostValues: []test.PostBody{{
Key: "chunkid",
Value: "eeng4ier3Taen7a",
}, {
Key: "filename",
Value: "fileupload.jpg",
}, {
Key: "filecontenttype",
Value: "test-content",
}, {
Key: "filesize",
Value: "50",
}},
Url: "http://127.0.0.1:53843/api/chunk/complete",
Headers: []test.Header{
{"apikey", "validkeyid7"},
{"uuid", "eeng4ier3Taen7a"},
{"filename", "fileupload.jpg"},
{"filecontenttype", "test-content"},
{"filesize", "50"},
{"nonblocking", "true"},
},
RequiredContent: []string{"{\"result\":\"OK\"}"},
Cookies: []test.Cookie{{
Name: "session_token",
@@ -628,7 +623,7 @@ func TestDisableLogin(t *testing.T) {
Value: "invalid",
}},
})
configuration.Get().Authentication.Method = authentication.Disabled
configuration.Get().Authentication.Method = models.AuthenticationDisabled
authentication.Init(configuration.Get().Authentication)
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://localhost:53843/admin",
@@ -639,7 +634,7 @@ func TestDisableLogin(t *testing.T) {
Value: "invalid",
}},
})
configuration.Get().Authentication.Method = authentication.Internal
configuration.Get().Authentication.Method = models.AuthenticationInternal
authentication.Init(configuration.Get().Authentication)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+482
View File
@@ -0,0 +1,482 @@
package api
import (
"errors"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/storage"
"github.com/forceu/gokapi/internal/storage/chunking"
"net/http"
"strconv"
"strings"
)
type apiRoute struct {
Url string
HasWildcard bool
ApiPerm models.ApiPermission
RequestParser requestParser
execution apiFunc
}
func (r apiRoute) Continue(w http.ResponseWriter, request requestParser, user models.User) {
r.execution(w, request, user)
}
type apiFunc func(w http.ResponseWriter, request requestParser, user models.User)
var routes = []apiRoute{
{
Url: "/files/list",
ApiPerm: models.ApiPermView,
execution: apiList,
RequestParser: nil,
},
{
Url: "/files/list/",
ApiPerm: models.ApiPermView,
execution: apiListSingle,
HasWildcard: true,
RequestParser: &paramFilesListSingle{},
},
{
Url: "/chunk/add",
ApiPerm: models.ApiPermUpload,
execution: apiChunkAdd,
RequestParser: &paramChunkAdd{},
},
{
Url: "/chunk/complete",
ApiPerm: models.ApiPermUpload,
execution: apiChunkComplete,
RequestParser: &paramChunkComplete{},
},
{
Url: "/files/add",
ApiPerm: models.ApiPermUpload,
execution: apiUploadFile,
RequestParser: &paramFilesAdd{},
},
{
Url: "/files/delete",
ApiPerm: models.ApiPermDelete,
execution: apiDeleteFile,
RequestParser: &paramFilesDelete{},
},
{
Url: "/files/duplicate",
ApiPerm: models.ApiPermUpload,
execution: apiDuplicateFile,
RequestParser: &paramFilesDuplicate{},
},
{
Url: "/files/modify",
ApiPerm: models.ApiPermEdit,
execution: apiEditFile,
RequestParser: &paramFilesModify{},
},
{
Url: "/files/replace",
ApiPerm: models.ApiPermReplace,
execution: apiReplaceFile,
RequestParser: &paramFilesReplace{},
},
{
Url: "/auth/create",
ApiPerm: models.ApiPermApiMod,
execution: apiCreateApiKey,
RequestParser: &paramAuthCreate{},
},
{
Url: "/auth/friendlyname",
ApiPerm: models.ApiPermApiMod,
execution: apiChangeFriendlyName,
RequestParser: &paramAuthFriendlyName{},
},
{
Url: "/auth/modify",
ApiPerm: models.ApiPermApiMod,
execution: apiModifyApiKey,
RequestParser: &paramAuthModify{},
},
{
Url: "/auth/delete",
ApiPerm: models.ApiPermApiMod,
execution: apiDeleteKey,
RequestParser: &paramAuthDelete{},
},
{
Url: "/user/create",
ApiPerm: models.ApiPermManageUsers,
execution: apiCreateUser,
RequestParser: &paramUserCreate{},
},
{
Url: "/user/changeRank",
ApiPerm: models.ApiPermManageUsers,
execution: apiChangeUserRank,
RequestParser: &paramUserChangeRank{},
},
{
Url: "/user/delete",
ApiPerm: models.ApiPermManageUsers,
execution: apiDeleteUser,
RequestParser: &paramUserDelete{},
},
{
Url: "/user/modify",
ApiPerm: models.ApiPermManageUsers,
execution: apiModifyUser,
RequestParser: &paramUserModify{},
},
{
Url: "/user/resetPassword",
ApiPerm: models.ApiPermManageUsers,
execution: apiResetPassword,
RequestParser: &paramUserResetPw{},
},
}
func getRouting(requestUrl string) (apiRoute, bool) {
for _, route := range routes {
if (!route.HasWildcard && requestUrl == route.Url) ||
(route.HasWildcard && strings.HasPrefix(requestUrl, route.Url)) {
return route, true
}
}
return apiRoute{}, false
}
type requestParser interface {
// ParseRequest reads the supplied headers, stores them and afterwards calls ProcessParameter()
ParseRequest(r *http.Request) error
// ProcessParameter goes through the submitted parameters, checks them and converts them to expected values
ProcessParameter(r *http.Request) error
// New returns an empty struct of the type
New() requestParser
}
type paramFilesListSingle struct {
RequestUrl string
}
func (p *paramFilesListSingle) ProcessParameter(r *http.Request) error {
p.RequestUrl = parseRequestUrl(r)
return nil
}
type paramFilesAdd struct {
Request *http.Request
}
func (p *paramFilesAdd) ProcessParameter(r *http.Request) error {
p.Request = r
return nil
}
type paramFilesDuplicate struct {
Id string `header:"id" required:"true"`
AllowedDownloads int `header:"allowedDownloads"`
ExpiryDays int `header:"expiryDays"`
Password string `header:"password"`
KeepPassword bool `header:"originalPassword"`
FileName string `header:"filename"`
UnlimitedDownloads bool
UnlimitedTime bool
RequestedChanges int
foundHeaders map[string]bool
}
func (p *paramFilesDuplicate) ProcessParameter(r *http.Request) error {
if p.foundHeaders["allowedDownloads"] {
p.RequestedChanges |= storage.ParamDownloads
if p.AllowedDownloads == 0 {
p.UnlimitedDownloads = true
}
}
if p.foundHeaders["expiryDays"] {
p.RequestedChanges |= storage.ParamExpiry
if p.ExpiryDays == 0 {
p.UnlimitedTime = true
}
}
if !p.KeepPassword {
if p.foundHeaders["password"] {
p.RequestedChanges |= storage.ParamPassword
}
}
if p.foundHeaders["filename"] {
p.RequestedChanges |= storage.ParamName
}
return nil
}
type paramFilesModify struct {
Id string `header:"id" required:"true"`
AllowedDownloads int `header:"allowedDownloads"`
ExpiryTimestamp int64 `header:"expiryTimestamp"`
Password string `header:"password"`
KeepPassword bool `header:"originalPassword"`
UnlimitedDownloads bool
UnlimitedExpiry bool
IsPasswordSet bool
foundHeaders map[string]bool
}
func (p *paramFilesModify) ProcessParameter(_ *http.Request) error {
if p.foundHeaders["allowedDownloads"] && p.AllowedDownloads == 0 {
p.UnlimitedDownloads = true
}
if p.foundHeaders["expiryTimestamp"] && p.ExpiryTimestamp == 0 {
p.UnlimitedExpiry = true
}
p.IsPasswordSet = p.foundHeaders["password"]
return nil
}
type paramFilesReplace struct {
Id string `header:"id" required:"true"`
IdNewContent string `header:"idNewContent" required:"true"`
Delete bool `header:"deleteNewFile"`
foundHeaders map[string]bool
}
func (p *paramFilesReplace) ProcessParameter(_ *http.Request) error { return nil }
type paramFilesDelete struct {
Id string `header:"id" required:"true"`
foundHeaders map[string]bool
}
func (p *paramFilesDelete) ProcessParameter(_ *http.Request) error { return nil }
type paramAuthCreate struct {
FriendlyName string `header:"friendlyName"`
BasicPermissions bool `header:"basicPermissions"`
foundHeaders map[string]bool
}
func (p *paramAuthCreate) ProcessParameter(_ *http.Request) error { return nil }
type paramAuthFriendlyName struct {
KeyId string `header:"targetKey" required:"true"`
FriendlyName string `header:"friendlyName" required:"true"`
foundHeaders map[string]bool
}
func (p *paramAuthFriendlyName) ProcessParameter(_ *http.Request) error { return nil }
type paramAuthModify struct {
KeyId string `header:"targetKey" required:"true"`
permissionRaw string `header:"permission" required:"true"`
permissionModifier string `header:"permissionModifier" required:"true"`
Permission models.ApiPermission
GrantPermission bool
foundHeaders map[string]bool
}
func (p *paramAuthModify) ProcessParameter(_ *http.Request) error {
switch strings.ToUpper(p.permissionRaw) {
case "PERM_VIEW":
p.Permission = models.ApiPermView
case "PERM_UPLOAD":
p.Permission = models.ApiPermUpload
case "PERM_DELETE":
p.Permission = models.ApiPermDelete
case "PERM_API_MOD":
p.Permission = models.ApiPermApiMod
case "PERM_EDIT":
p.Permission = models.ApiPermEdit
case "PERM_REPLACE":
p.Permission = models.ApiPermReplace
case "PERM_MANAGE_USERS":
p.Permission = models.ApiPermManageUsers
default:
return errors.New("invalid permission")
}
switch strings.ToUpper(p.permissionModifier) {
case "GRANT":
p.GrantPermission = true
case "REVOKE":
p.GrantPermission = false
default:
return errors.New("invalid permission modifier")
}
return nil
}
type paramAuthDelete struct {
KeyId string `header:"targetKey" required:"true"`
foundHeaders map[string]bool
}
func (p *paramAuthDelete) ProcessParameter(_ *http.Request) error { return nil }
type paramUserCreate struct {
Username string `header:"username" required:"true"`
foundHeaders map[string]bool
}
func (p *paramUserCreate) ProcessParameter(_ *http.Request) error { return nil }
type paramUserChangeRank struct {
Id int `header:"userid" required:"true"`
newRankRaw string `header:"newRank" required:"true"`
NewRank models.UserRank
foundHeaders map[string]bool
}
func (p *paramUserChangeRank) ProcessParameter(_ *http.Request) error {
switch strings.ToLower(p.newRankRaw) {
case "admin":
p.NewRank = models.UserLevelAdmin
case "user":
p.NewRank = models.UserLevelUser
default:
return errors.New("invalid rank")
}
return nil
}
type paramUserDelete struct {
Id int `header:"userid" required:"true"`
DeleteFiles bool `header:"deleteFiles"`
foundHeaders map[string]bool
}
func (p *paramUserDelete) ProcessParameter(_ *http.Request) error { return nil }
type paramUserModify struct {
Id int `header:"userid" required:"true"`
Permission models.UserPermission
permissionRaw string `header:"userpermission" required:"true"`
permissionModifier string `header:"permissionModifier" required:"true"`
GrantPermission bool
foundHeaders map[string]bool
}
func (p *paramUserModify) ProcessParameter(_ *http.Request) error {
switch strings.ToUpper(p.permissionRaw) {
case "PERM_REPLACE":
p.Permission = models.UserPermReplaceUploads
case "PERM_LIST":
p.Permission = models.UserPermListOtherUploads
case "PERM_EDIT":
p.Permission = models.UserPermEditOtherUploads
case "PERM_REPLACE_OTHER":
p.Permission = models.UserPermReplaceOtherUploads
case "PERM_DELETE":
p.Permission = models.UserPermDeleteOtherUploads
case "PERM_LOGS":
p.Permission = models.UserPermManageLogs
case "PERM_API":
p.Permission = models.UserPermManageApiKeys
case "PERM_USERS":
p.Permission = models.UserPermManageUsers
default:
return errors.New("invalid permission")
}
switch strings.ToUpper(p.permissionModifier) {
case "GRANT":
p.GrantPermission = true
case "REVOKE":
p.GrantPermission = false
default:
return errors.New("invalid permission modifier")
}
return nil
}
type paramUserResetPw struct {
Id int `header:"userid" required:"true"`
NewPassword bool `header:"generateNewPassword"`
foundHeaders map[string]bool
}
func (p *paramUserResetPw) ProcessParameter(_ *http.Request) error { return nil }
type paramChunkAdd struct {
Request *http.Request
}
func (p *paramChunkAdd) ProcessParameter(r *http.Request) error {
p.Request = r
return nil
}
type paramChunkComplete struct {
Uuid string `header:"uuid" required:"true"`
FileName string `header:"filename" required:"true"`
FileSize int64 `header:"filesize" required:"true"`
RealSize int64 `header:"realsize"`
ContentType string `header:"contenttype"`
AllowedDownloads int `header:"allowedDownloads"`
ExpiryDays int `header:"expiryDays"`
Password string `header:"password"`
IsE2E bool `header:"isE2E"`
IsNonBlocking bool `header:"nonblocking"`
UnlimitedDownloads bool
UnlimitedTime bool
FileHeader chunking.FileHeader
foundHeaders map[string]bool
}
func (p *paramChunkComplete) ProcessParameter(_ *http.Request) error {
if !p.foundHeaders["realsize"] {
if !p.IsE2E {
p.RealSize = p.FileSize
} else {
return errors.New("e2e set, but realsize not submitted")
}
}
if p.foundHeaders["allowedDownloads"] && p.AllowedDownloads == 0 {
p.UnlimitedDownloads = true
}
if p.foundHeaders["expiryDays"] && p.ExpiryDays == 0 {
p.UnlimitedTime = true
}
p.FileHeader = chunking.FileHeader{
Filename: p.FileName,
ContentType: p.ContentType,
Size: p.FileSize,
}
return nil
}
func checkHeaderExists(r *http.Request, key string, isRequired, isString bool) (bool, error) {
if r.Header.Get(key) != "" {
return true, nil
}
if isRequired {
return false, errors.New("header " + key + " is required")
}
if isString {
return len(r.Header.Values(key)) > 0, nil
}
return false, nil
}
func parseHeaderBool(r *http.Request, key string) (bool, error) {
value, err := strconv.ParseBool(r.Header.Get(key))
if err != nil {
return false, err
}
return value, nil
}
func parseHeaderInt64(r *http.Request, key string) (int64, error) {
value, err := strconv.ParseInt(r.Header.Get(key), 10, 64)
if err != nil {
return 0, err
}
return value, nil
}
func parseHeaderInt(r *http.Request, key string) (int, error) {
value, err := strconv.Atoi(r.Header.Get(key))
if err != nil {
return 0, err
}
return value, nil
}
+743
View File
@@ -0,0 +1,743 @@
// Code generated by updateApiRouting.go - DO NOT EDIT.
package api
import (
"fmt"
"net/http"
)
// Do not modify: This is an automatically generated file created by updateApiRouting.go
// It contains the code that is used to parse the headers submitted in an API request
// ParseRequest parses the header file. As paramFilesListSingle has no fields with the
// tag header, this method does nothing, except calling ProcessParameter()
func (p *paramFilesListSingle) ParseRequest(r *http.Request) error {
return p.ProcessParameter(r)
}
// New returns a new instance of paramFilesListSingle struct
func (p *paramFilesListSingle) New() requestParser {
return &paramFilesListSingle{}
}
// ParseRequest parses the header file. As paramFilesAdd has no fields with the
// tag header, this method does nothing, except calling ProcessParameter()
func (p *paramFilesAdd) ParseRequest(r *http.Request) error {
return p.ProcessParameter(r)
}
// New returns a new instance of paramFilesAdd struct
func (p *paramFilesAdd) New() requestParser {
return &paramFilesAdd{}
}
// ParseRequest reads r and saves the passed header values in the paramFilesDuplicate struct
// In the end, ProcessParameter() is called
func (p *paramFilesDuplicate) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "id", required: true
exists, err = checkHeaderExists(r, "id", true, true)
if err != nil {
return err
}
p.foundHeaders["id"] = exists
if exists {
p.Id = r.Header.Get("id")
}
// RequestParser header value "allowedDownloads", required: false
exists, err = checkHeaderExists(r, "allowedDownloads", false, false)
if err != nil {
return err
}
p.foundHeaders["allowedDownloads"] = exists
if exists {
p.AllowedDownloads, err = parseHeaderInt(r, "allowedDownloads")
if err != nil {
return fmt.Errorf("invalid value in header allowedDownloads supplied")
}
}
// RequestParser header value "expiryDays", required: false
exists, err = checkHeaderExists(r, "expiryDays", false, false)
if err != nil {
return err
}
p.foundHeaders["expiryDays"] = exists
if exists {
p.ExpiryDays, err = parseHeaderInt(r, "expiryDays")
if err != nil {
return fmt.Errorf("invalid value in header expiryDays supplied")
}
}
// RequestParser header value "password", required: false
exists, err = checkHeaderExists(r, "password", false, true)
if err != nil {
return err
}
p.foundHeaders["password"] = exists
if exists {
p.Password = r.Header.Get("password")
}
// RequestParser header value "originalPassword", required: false
exists, err = checkHeaderExists(r, "originalPassword", false, false)
if err != nil {
return err
}
p.foundHeaders["originalPassword"] = exists
if exists {
p.KeepPassword, err = parseHeaderBool(r, "originalPassword")
if err != nil {
return fmt.Errorf("invalid value in header originalPassword supplied")
}
}
// RequestParser header value "filename", required: false
exists, err = checkHeaderExists(r, "filename", false, true)
if err != nil {
return err
}
p.foundHeaders["filename"] = exists
if exists {
p.FileName = r.Header.Get("filename")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramFilesDuplicate struct
func (p *paramFilesDuplicate) New() requestParser {
return &paramFilesDuplicate{}
}
// ParseRequest reads r and saves the passed header values in the paramFilesModify struct
// In the end, ProcessParameter() is called
func (p *paramFilesModify) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "id", required: true
exists, err = checkHeaderExists(r, "id", true, true)
if err != nil {
return err
}
p.foundHeaders["id"] = exists
if exists {
p.Id = r.Header.Get("id")
}
// RequestParser header value "allowedDownloads", required: false
exists, err = checkHeaderExists(r, "allowedDownloads", false, false)
if err != nil {
return err
}
p.foundHeaders["allowedDownloads"] = exists
if exists {
p.AllowedDownloads, err = parseHeaderInt(r, "allowedDownloads")
if err != nil {
return fmt.Errorf("invalid value in header allowedDownloads supplied")
}
}
// RequestParser header value "expiryTimestamp", required: false
exists, err = checkHeaderExists(r, "expiryTimestamp", false, false)
if err != nil {
return err
}
p.foundHeaders["expiryTimestamp"] = exists
if exists {
p.ExpiryTimestamp, err = parseHeaderInt64(r, "expiryTimestamp")
if err != nil {
return fmt.Errorf("invalid value in header expiryTimestamp supplied")
}
}
// RequestParser header value "password", required: false
exists, err = checkHeaderExists(r, "password", false, true)
if err != nil {
return err
}
p.foundHeaders["password"] = exists
if exists {
p.Password = r.Header.Get("password")
}
// RequestParser header value "originalPassword", required: false
exists, err = checkHeaderExists(r, "originalPassword", false, false)
if err != nil {
return err
}
p.foundHeaders["originalPassword"] = exists
if exists {
p.KeepPassword, err = parseHeaderBool(r, "originalPassword")
if err != nil {
return fmt.Errorf("invalid value in header originalPassword supplied")
}
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramFilesModify struct
func (p *paramFilesModify) New() requestParser {
return &paramFilesModify{}
}
// ParseRequest reads r and saves the passed header values in the paramFilesReplace struct
// In the end, ProcessParameter() is called
func (p *paramFilesReplace) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "id", required: true
exists, err = checkHeaderExists(r, "id", true, true)
if err != nil {
return err
}
p.foundHeaders["id"] = exists
if exists {
p.Id = r.Header.Get("id")
}
// RequestParser header value "idNewContent", required: true
exists, err = checkHeaderExists(r, "idNewContent", true, true)
if err != nil {
return err
}
p.foundHeaders["idNewContent"] = exists
if exists {
p.IdNewContent = r.Header.Get("idNewContent")
}
// RequestParser header value "deleteNewFile", required: false
exists, err = checkHeaderExists(r, "deleteNewFile", false, false)
if err != nil {
return err
}
p.foundHeaders["deleteNewFile"] = exists
if exists {
p.Delete, err = parseHeaderBool(r, "deleteNewFile")
if err != nil {
return fmt.Errorf("invalid value in header deleteNewFile supplied")
}
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramFilesReplace struct
func (p *paramFilesReplace) New() requestParser {
return &paramFilesReplace{}
}
// ParseRequest reads r and saves the passed header values in the paramFilesDelete struct
// In the end, ProcessParameter() is called
func (p *paramFilesDelete) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "id", required: true
exists, err = checkHeaderExists(r, "id", true, true)
if err != nil {
return err
}
p.foundHeaders["id"] = exists
if exists {
p.Id = r.Header.Get("id")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramFilesDelete struct
func (p *paramFilesDelete) New() requestParser {
return &paramFilesDelete{}
}
// ParseRequest reads r and saves the passed header values in the paramAuthCreate struct
// In the end, ProcessParameter() is called
func (p *paramAuthCreate) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "friendlyName", required: false
exists, err = checkHeaderExists(r, "friendlyName", false, true)
if err != nil {
return err
}
p.foundHeaders["friendlyName"] = exists
if exists {
p.FriendlyName = r.Header.Get("friendlyName")
}
// RequestParser header value "basicPermissions", required: false
exists, err = checkHeaderExists(r, "basicPermissions", false, false)
if err != nil {
return err
}
p.foundHeaders["basicPermissions"] = exists
if exists {
p.BasicPermissions, err = parseHeaderBool(r, "basicPermissions")
if err != nil {
return fmt.Errorf("invalid value in header basicPermissions supplied")
}
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramAuthCreate struct
func (p *paramAuthCreate) New() requestParser {
return &paramAuthCreate{}
}
// ParseRequest reads r and saves the passed header values in the paramAuthFriendlyName struct
// In the end, ProcessParameter() is called
func (p *paramAuthFriendlyName) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "targetKey", required: true
exists, err = checkHeaderExists(r, "targetKey", true, true)
if err != nil {
return err
}
p.foundHeaders["targetKey"] = exists
if exists {
p.KeyId = r.Header.Get("targetKey")
}
// RequestParser header value "friendlyName", required: true
exists, err = checkHeaderExists(r, "friendlyName", true, true)
if err != nil {
return err
}
p.foundHeaders["friendlyName"] = exists
if exists {
p.FriendlyName = r.Header.Get("friendlyName")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramAuthFriendlyName struct
func (p *paramAuthFriendlyName) New() requestParser {
return &paramAuthFriendlyName{}
}
// ParseRequest reads r and saves the passed header values in the paramAuthModify struct
// In the end, ProcessParameter() is called
func (p *paramAuthModify) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "targetKey", required: true
exists, err = checkHeaderExists(r, "targetKey", true, true)
if err != nil {
return err
}
p.foundHeaders["targetKey"] = exists
if exists {
p.KeyId = r.Header.Get("targetKey")
}
// RequestParser header value "permission", required: true
exists, err = checkHeaderExists(r, "permission", true, true)
if err != nil {
return err
}
p.foundHeaders["permission"] = exists
if exists {
p.permissionRaw = r.Header.Get("permission")
}
// RequestParser header value "permissionModifier", required: true
exists, err = checkHeaderExists(r, "permissionModifier", true, true)
if err != nil {
return err
}
p.foundHeaders["permissionModifier"] = exists
if exists {
p.permissionModifier = r.Header.Get("permissionModifier")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramAuthModify struct
func (p *paramAuthModify) New() requestParser {
return &paramAuthModify{}
}
// ParseRequest reads r and saves the passed header values in the paramAuthDelete struct
// In the end, ProcessParameter() is called
func (p *paramAuthDelete) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "targetKey", required: true
exists, err = checkHeaderExists(r, "targetKey", true, true)
if err != nil {
return err
}
p.foundHeaders["targetKey"] = exists
if exists {
p.KeyId = r.Header.Get("targetKey")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramAuthDelete struct
func (p *paramAuthDelete) New() requestParser {
return &paramAuthDelete{}
}
// ParseRequest reads r and saves the passed header values in the paramUserCreate struct
// In the end, ProcessParameter() is called
func (p *paramUserCreate) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "username", required: true
exists, err = checkHeaderExists(r, "username", true, true)
if err != nil {
return err
}
p.foundHeaders["username"] = exists
if exists {
p.Username = r.Header.Get("username")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramUserCreate struct
func (p *paramUserCreate) New() requestParser {
return &paramUserCreate{}
}
// ParseRequest reads r and saves the passed header values in the paramUserChangeRank struct
// In the end, ProcessParameter() is called
func (p *paramUserChangeRank) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "userid", required: true
exists, err = checkHeaderExists(r, "userid", true, false)
if err != nil {
return err
}
p.foundHeaders["userid"] = exists
if exists {
p.Id, err = parseHeaderInt(r, "userid")
if err != nil {
return fmt.Errorf("invalid value in header userid supplied")
}
}
// RequestParser header value "newRank", required: true
exists, err = checkHeaderExists(r, "newRank", true, true)
if err != nil {
return err
}
p.foundHeaders["newRank"] = exists
if exists {
p.newRankRaw = r.Header.Get("newRank")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramUserChangeRank struct
func (p *paramUserChangeRank) New() requestParser {
return &paramUserChangeRank{}
}
// ParseRequest reads r and saves the passed header values in the paramUserDelete struct
// In the end, ProcessParameter() is called
func (p *paramUserDelete) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "userid", required: true
exists, err = checkHeaderExists(r, "userid", true, false)
if err != nil {
return err
}
p.foundHeaders["userid"] = exists
if exists {
p.Id, err = parseHeaderInt(r, "userid")
if err != nil {
return fmt.Errorf("invalid value in header userid supplied")
}
}
// RequestParser header value "deleteFiles", required: false
exists, err = checkHeaderExists(r, "deleteFiles", false, false)
if err != nil {
return err
}
p.foundHeaders["deleteFiles"] = exists
if exists {
p.DeleteFiles, err = parseHeaderBool(r, "deleteFiles")
if err != nil {
return fmt.Errorf("invalid value in header deleteFiles supplied")
}
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramUserDelete struct
func (p *paramUserDelete) New() requestParser {
return &paramUserDelete{}
}
// ParseRequest reads r and saves the passed header values in the paramUserModify struct
// In the end, ProcessParameter() is called
func (p *paramUserModify) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "userid", required: true
exists, err = checkHeaderExists(r, "userid", true, false)
if err != nil {
return err
}
p.foundHeaders["userid"] = exists
if exists {
p.Id, err = parseHeaderInt(r, "userid")
if err != nil {
return fmt.Errorf("invalid value in header userid supplied")
}
}
// RequestParser header value "userpermission", required: true
exists, err = checkHeaderExists(r, "userpermission", true, true)
if err != nil {
return err
}
p.foundHeaders["userpermission"] = exists
if exists {
p.permissionRaw = r.Header.Get("userpermission")
}
// RequestParser header value "permissionModifier", required: true
exists, err = checkHeaderExists(r, "permissionModifier", true, true)
if err != nil {
return err
}
p.foundHeaders["permissionModifier"] = exists
if exists {
p.permissionModifier = r.Header.Get("permissionModifier")
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramUserModify struct
func (p *paramUserModify) New() requestParser {
return &paramUserModify{}
}
// ParseRequest reads r and saves the passed header values in the paramUserResetPw struct
// In the end, ProcessParameter() is called
func (p *paramUserResetPw) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "userid", required: true
exists, err = checkHeaderExists(r, "userid", true, false)
if err != nil {
return err
}
p.foundHeaders["userid"] = exists
if exists {
p.Id, err = parseHeaderInt(r, "userid")
if err != nil {
return fmt.Errorf("invalid value in header userid supplied")
}
}
// RequestParser header value "generateNewPassword", required: false
exists, err = checkHeaderExists(r, "generateNewPassword", false, false)
if err != nil {
return err
}
p.foundHeaders["generateNewPassword"] = exists
if exists {
p.NewPassword, err = parseHeaderBool(r, "generateNewPassword")
if err != nil {
return fmt.Errorf("invalid value in header generateNewPassword supplied")
}
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramUserResetPw struct
func (p *paramUserResetPw) New() requestParser {
return &paramUserResetPw{}
}
// ParseRequest parses the header file. As paramChunkAdd has no fields with the
// tag header, this method does nothing, except calling ProcessParameter()
func (p *paramChunkAdd) ParseRequest(r *http.Request) error {
return p.ProcessParameter(r)
}
// New returns a new instance of paramChunkAdd struct
func (p *paramChunkAdd) New() requestParser {
return &paramChunkAdd{}
}
// ParseRequest reads r and saves the passed header values in the paramChunkComplete struct
// In the end, ProcessParameter() is called
func (p *paramChunkComplete) ParseRequest(r *http.Request) error {
var err error
var exists bool
p.foundHeaders = make(map[string]bool)
// RequestParser header value "uuid", required: true
exists, err = checkHeaderExists(r, "uuid", true, true)
if err != nil {
return err
}
p.foundHeaders["uuid"] = exists
if exists {
p.Uuid = r.Header.Get("uuid")
}
// RequestParser header value "filename", required: true
exists, err = checkHeaderExists(r, "filename", true, true)
if err != nil {
return err
}
p.foundHeaders["filename"] = exists
if exists {
p.FileName = r.Header.Get("filename")
}
// RequestParser header value "filesize", required: true
exists, err = checkHeaderExists(r, "filesize", true, false)
if err != nil {
return err
}
p.foundHeaders["filesize"] = exists
if exists {
p.FileSize, err = parseHeaderInt64(r, "filesize")
if err != nil {
return fmt.Errorf("invalid value in header filesize supplied")
}
}
// RequestParser header value "realsize", required: false
exists, err = checkHeaderExists(r, "realsize", false, false)
if err != nil {
return err
}
p.foundHeaders["realsize"] = exists
if exists {
p.RealSize, err = parseHeaderInt64(r, "realsize")
if err != nil {
return fmt.Errorf("invalid value in header realsize supplied")
}
}
// RequestParser header value "contenttype", required: false
exists, err = checkHeaderExists(r, "contenttype", false, true)
if err != nil {
return err
}
p.foundHeaders["contenttype"] = exists
if exists {
p.ContentType = r.Header.Get("contenttype")
}
// RequestParser header value "allowedDownloads", required: false
exists, err = checkHeaderExists(r, "allowedDownloads", false, false)
if err != nil {
return err
}
p.foundHeaders["allowedDownloads"] = exists
if exists {
p.AllowedDownloads, err = parseHeaderInt(r, "allowedDownloads")
if err != nil {
return fmt.Errorf("invalid value in header allowedDownloads supplied")
}
}
// RequestParser header value "expiryDays", required: false
exists, err = checkHeaderExists(r, "expiryDays", false, false)
if err != nil {
return err
}
p.foundHeaders["expiryDays"] = exists
if exists {
p.ExpiryDays, err = parseHeaderInt(r, "expiryDays")
if err != nil {
return fmt.Errorf("invalid value in header expiryDays supplied")
}
}
// RequestParser header value "password", required: false
exists, err = checkHeaderExists(r, "password", false, true)
if err != nil {
return err
}
p.foundHeaders["password"] = exists
if exists {
p.Password = r.Header.Get("password")
}
// RequestParser header value "isE2E", required: false
exists, err = checkHeaderExists(r, "isE2E", false, false)
if err != nil {
return err
}
p.foundHeaders["isE2E"] = exists
if exists {
p.IsE2E, err = parseHeaderBool(r, "isE2E")
if err != nil {
return fmt.Errorf("invalid value in header isE2E supplied")
}
}
// RequestParser header value "nonblocking", required: false
exists, err = checkHeaderExists(r, "nonblocking", false, false)
if err != nil {
return err
}
p.foundHeaders["nonblocking"] = exists
if exists {
p.IsNonBlocking, err = parseHeaderBool(r, "nonblocking")
if err != nil {
return fmt.Errorf("invalid value in header nonblocking supplied")
}
}
return p.ProcessParameter(r)
}
// New returns a new instance of paramChunkComplete struct
func (p *paramChunkComplete) New() requestParser {
return &paramChunkComplete{}
}
@@ -6,12 +6,14 @@ import (
"errors"
"fmt"
"github.com/forceu/gokapi/internal/configuration"
"github.com/forceu/gokapi/internal/configuration/database"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/webserver/authentication/sessionmanager"
"io"
"log"
"net/http"
"os"
"regexp"
"strings"
)
@@ -19,103 +21,103 @@ import (
// CookieOauth is the cookie name used for login
const CookieOauth = "state"
// Internal authentication method uses a user / password combination handled by Gokapi
const Internal = 0
// OAuth2 authentication retrieves the users email with Open Connect ID
const OAuth2 = 1
// Header authentication relies on a header from a reverse proxy to parse the username
const Header = 2
// Disabled authentication ignores all internal authentication procedures. A reverse proxy needs to restrict access
const Disabled = 3
var authSettings models.AuthenticationConfig
// Init needs to be called first to process the authentication configuration
func Init(config models.AuthenticationConfig) {
valid, err := isValid(config)
if !valid {
err := checkAuthConfig(config)
if err != nil {
log.Println("Error while initiating authentication method:")
log.Fatal(err)
log.Println(err)
osExit(3)
return
}
authSettings = config
}
// isValid checks if the config is actually valid, and returns true or returns false and an error
func isValid(config models.AuthenticationConfig) (bool, error) {
var osExit = os.Exit
// checkAuthConfig checks if the config is actually valid, and returns an error otherwise
func checkAuthConfig(config models.AuthenticationConfig) error {
switch config.Method {
case Internal:
case models.AuthenticationInternal:
if len(config.Username) < 3 {
return false, errors.New("username too short")
return errors.New("username too short")
}
if len(config.Password) != 40 {
return false, errors.New("password does not appear to be a SHA-1 hash")
}
return true, nil
case OAuth2:
return nil
case models.AuthenticationOAuth2:
if config.OAuthProvider == "" {
return false, errors.New("oauth provider was not set")
return errors.New("oauth provider was not set")
}
if config.OAuthClientId == "" {
return false, errors.New("oauth client id was not set")
return errors.New("oauth client id was not set")
}
if config.OAuthClientSecret == "" {
return false, errors.New("oauth client secret was not set")
return errors.New("oauth client secret was not set")
}
return true, nil
case Header:
if config.OAuthRecheckInterval < 1 {
return errors.New("oauth recheck interval invalid")
}
return nil
case models.AuthenticationHeader:
if config.HeaderKey == "" {
return false, errors.New("header key is not set")
return errors.New("header key is not set")
}
return true, nil
case Disabled:
return true, nil
return nil
case models.AuthenticationDisabled:
return nil
default:
return false, errors.New("unknown authentication selected")
return errors.New("unknown authentication selected")
}
}
// IsAuthenticated returns true if the user provides a valid authentication
func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool {
switch authSettings.Method {
case Internal:
return isGrantedSession(w, r)
case OAuth2:
return isGrantedSession(w, r)
case Header:
return isGrantedHeader(r)
case Disabled:
return true
func GetUserFromRequest(r *http.Request) (models.User, error) {
c := r.Context()
user, ok := c.Value("user").(models.User)
if !ok {
return models.User{}, errors.New("user not found in context")
}
return false
return user, nil
}
// IsAuthenticated returns true and the user ID if authenticated
func IsAuthenticated(w http.ResponseWriter, r *http.Request) (models.User, bool) {
switch authSettings.Method {
case models.AuthenticationInternal:
user, ok := isGrantedSession(w, r)
if ok {
return user, true
}
case models.AuthenticationOAuth2:
user, ok := isGrantedSession(w, r)
if ok {
return user, true
}
case models.AuthenticationHeader:
user, ok := isGrantedHeader(r)
if ok {
return user, true
}
case models.AuthenticationDisabled:
adminUser, ok := database.GetSuperAdmin()
if !ok {
panic("no super admin found")
}
return adminUser, true
}
return models.User{}, false
}
// isGrantedHeader returns true if the user was authenticated by a proxy header if enabled
func isGrantedHeader(r *http.Request) bool {
func isGrantedHeader(r *http.Request) (models.User, bool) {
if authSettings.HeaderKey == "" {
return false
return models.User{}, false
}
value := r.Header.Get(authSettings.HeaderKey)
if value == "" {
return false
userName := r.Header.Get(authSettings.HeaderKey)
if userName == "" {
return models.User{}, false
}
if len(authSettings.HeaderUsers) == 0 {
return true
}
return isUserInArray(value, authSettings.HeaderUsers)
}
func isUserInArray(userEntered string, allowedUsers []string) bool {
for _, allowedUser := range allowedUsers {
matches, err := matchesWithWildcard(strings.ToLower(allowedUser), strings.ToLower(userEntered))
helper.Check(err)
if matches {
return true
}
}
return false
return getOrCreateUser(userName)
}
func matchesWithWildcard(pattern, input string) (bool, error) {
@@ -185,33 +187,6 @@ func extractOauthGroups(userInfo OAuthUserClaims, groupScope string) ([]string,
return groups, nil
}
func extractFieldValue(userInfo OAuthUserClaims, fieldName string) (string, error) {
var claims json.RawMessage
err := userInfo.Claims(&claims)
if err != nil {
return "", err
}
var fieldMap map[string]interface{}
err = json.Unmarshal(claims, &fieldMap)
if err != nil {
return "", err
}
// Extract the field value based on the provided fieldName
fieldValue, ok := fieldMap[fieldName]
if !ok {
return "", fmt.Errorf("%s scope not found in reply", fieldName)
}
strValue, ok := fieldValue.(string)
if !ok {
return "", fmt.Errorf("value of %s scope is not a string", fieldName)
}
return strValue, nil
}
// OAuthUserInfo is used to make testing easier. This results in an additional parameter for the subject unfortunately
type OAuthUserInfo struct {
Subject string
@@ -226,59 +201,74 @@ type OAuthUserClaims interface {
// CheckOauthUserAndRedirect checks if the user is allowed to use the Gokapi instance
func CheckOauthUserAndRedirect(userInfo OAuthUserInfo, w http.ResponseWriter) error {
var username string
var groups []string
var err error
if authSettings.OAuthUserScope != "" {
if authSettings.OAuthUserScope == "email" {
username = userInfo.Email
} else {
username, err = extractFieldValue(userInfo.ClaimsSent, authSettings.OAuthUserScope)
if err != nil {
return err
}
}
}
if authSettings.OAuthGroupScope != "" {
groups, err = extractOauthGroups(userInfo.ClaimsSent, authSettings.OAuthGroupScope)
if err != nil {
return err
}
}
if isValidOauthUser(userInfo, username, groups) {
sessionmanager.CreateSession(w, authSettings.Method == OAuth2, authSettings.OAuthRecheckInterval)
redirect(w, "admin")
return nil
if isValidOauthUser(userInfo, groups) {
user, ok := getOrCreateUser(userInfo.Email)
if ok {
sessionmanager.CreateSession(w, true, authSettings.OAuthRecheckInterval, user.Id)
redirect(w, "admin")
}
}
redirect(w, "error-auth")
return nil
}
func isValidOauthUser(userInfo OAuthUserInfo, username string, groups []string) bool {
func getOrCreateUser(username string) (models.User, bool) {
user, ok := database.GetUserByName(username)
if !ok {
if authSettings.OnlyRegisteredUsers {
return models.User{}, false
}
user = models.User{
Name: username,
UserLevel: models.UserLevelUser,
}
database.SaveUser(user, true)
user, ok = database.GetUserByName(username)
if !ok {
panic("unable to read new user")
}
}
return user, true
}
func isValidOauthUser(userInfo OAuthUserInfo, groups []string) bool {
if userInfo.Subject == "" {
return false
}
isValidUser := true
if len(authSettings.OAuthUsers) > 0 {
isValidUser = isUserInArray(username, authSettings.OAuthUsers)
if userInfo.Email == "" {
return false
}
isValidGroup := true
if len(authSettings.OAuthGroups) > 0 {
isValidGroup = isGroupInArray(groups, authSettings.OAuthGroups)
}
return isValidUser && isValidGroup
return isValidGroup
}
// isGrantedSession returns true if the user holds a valid internal session cookie
func isGrantedSession(w http.ResponseWriter, r *http.Request) bool {
return sessionmanager.IsValidSession(w, r, authSettings.Method == OAuth2, authSettings.OAuthRecheckInterval)
func isGrantedSession(w http.ResponseWriter, r *http.Request) (models.User, bool) {
return sessionmanager.IsValidSession(w, r, authSettings.Method == models.AuthenticationOAuth2, authSettings.OAuthRecheckInterval)
}
// IsCorrectUsernameAndPassword checks if a provided username and password is correct
func IsCorrectUsernameAndPassword(username, password string) bool {
return IsEqualStringConstantTime(username, authSettings.Username) &&
IsEqualStringConstantTime(configuration.HashPasswordCustomSalt(password, authSettings.SaltAdmin), authSettings.Password)
func IsCorrectUsernameAndPassword(username, password string) (models.User, bool) {
user, ok := database.GetUserByName(username)
if !ok {
return models.User{}, false
}
if IsEqualStringConstantTime(configuration.HashPasswordCustomSalt(password, authSettings.SaltAdmin), user.Password) {
return user, true
}
return models.User{}, false
}
// IsEqualStringConstantTime uses ConstantTimeCompare to prevent timing attack.
@@ -295,10 +285,10 @@ func redirect(w http.ResponseWriter, url string) {
// Logout logs the user out and removes the session
func Logout(w http.ResponseWriter, r *http.Request) {
if authSettings.Method == Internal || authSettings.Method == OAuth2 {
if authSettings.Method == models.AuthenticationInternal || authSettings.Method == models.AuthenticationOAuth2 {
sessionmanager.LogoutSession(w, r)
}
if authSettings.Method == OAuth2 {
if authSettings.Method == models.AuthenticationOAuth2 {
redirect(w, "login?consent=true")
} else {
redirect(w, "login")
@@ -307,5 +297,5 @@ func Logout(w http.ResponseWriter, r *http.Request) {
// IsLogoutAvailable returns true if a logout button should be shown with the current form of authentication
func IsLogoutAvailable() bool {
return authSettings.Method == Internal || authSettings.Method == OAuth2
return authSettings.Method == models.AuthenticationInternal || authSettings.Method == models.AuthenticationOAuth2
}
@@ -1,14 +1,18 @@
package authentication
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/forceu/gokapi/internal/configuration"
"github.com/forceu/gokapi/internal/configuration/database"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/test"
"github.com/forceu/gokapi/internal/test/testconfiguration"
"github.com/forceu/gokapi/internal/webserver/authentication/sessionmanager"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
@@ -26,13 +30,63 @@ func TestMain(m *testing.M) {
func TestInit(t *testing.T) {
Init(modelUserPW)
test.IsEqualInt(t, authSettings.Method, Internal)
test.IsEqualString(t, authSettings.Username, "admin")
test.IsEqualInt(t, authSettings.Method, models.AuthenticationInternal)
test.IsEqualString(t, authSettings.Username, "test")
}
func TestIsValid(t *testing.T) {
config := models.AuthenticationConfig{
Method: models.AuthenticationInternal,
SaltAdmin: "1234",
SaltFiles: "1234",
Username: "2s",
}
err := checkAuthConfig(config)
test.IsNotNil(t, err)
config.Username = "long name"
err = checkAuthConfig(config)
test.IsNil(t, err)
config.Method = models.AuthenticationHeader
err = checkAuthConfig(config)
test.IsNotNil(t, err)
config.HeaderKey = "header"
err = checkAuthConfig(config)
test.IsNil(t, err)
config.Method = models.AuthenticationOAuth2
err = checkAuthConfig(config)
test.IsNotNil(t, err)
config.OAuthProvider = "xxx"
err = checkAuthConfig(config)
test.IsNotNil(t, err)
config.OAuthClientId = "xxx"
err = checkAuthConfig(config)
test.IsNotNil(t, err)
config.OAuthClientSecret = "xxx"
err = checkAuthConfig(config)
test.IsNotNil(t, err)
config.OAuthRecheckInterval = -1
err = checkAuthConfig(config)
test.IsNotNil(t, err)
config.OAuthRecheckInterval = 1
err = checkAuthConfig(config)
test.IsNil(t, err)
}
func TestIsCorrectUsernameAndPassword(t *testing.T) {
test.IsEqualBool(t, IsCorrectUsernameAndPassword("admin", "adminadmin"), true)
test.IsEqualBool(t, IsCorrectUsernameAndPassword("admin", "wrong"), false)
user, ok := IsCorrectUsernameAndPassword("test", "adminadmin")
test.IsEqualBool(t, ok, true)
user, ok = IsCorrectUsernameAndPassword("Test", "adminadmin")
test.IsEqualBool(t, ok, true)
test.IsEqualInt(t, user.Id, 5)
user, ok = IsCorrectUsernameAndPassword("user", "useruser")
test.IsEqualBool(t, ok, true)
test.IsEqualInt(t, user.Id, 7)
_, ok = IsCorrectUsernameAndPassword("test", "wrong")
test.IsEqualBool(t, ok, false)
_, ok = IsCorrectUsernameAndPassword("invalid", "adminadmin")
test.IsEqualBool(t, ok, false)
}
func TestIsAuthenticated(t *testing.T) {
@@ -41,55 +95,86 @@ func TestIsAuthenticated(t *testing.T) {
testAuthDisabled(t)
w, r := test.GetRecorder("GET", "/", nil, nil, nil)
authSettings.Method = -1
test.IsEqualBool(t, IsAuthenticated(w, r), false)
_, ok := IsAuthenticated(w, r)
test.IsEqualBool(t, ok, false)
}
func testAuthSession(t *testing.T) {
exitCode := 0
osExit = func(code int) {
exitCode = code
}
w, r := test.GetRecorder("GET", "/", nil, nil, nil)
Init(modelUserPW)
test.IsEqualBool(t, IsAuthenticated(w, r), false)
_, ok := IsAuthenticated(w, r)
test.IsEqualBool(t, ok, false)
Init(modelOauth)
test.IsEqualBool(t, IsAuthenticated(w, r), false)
_, ok = IsAuthenticated(w, r)
test.IsEqualBool(t, ok, false)
Init(modelUserPW)
w, r = test.GetRecorder("GET", "/", []test.Cookie{{
Name: "session_token",
Value: "validsession",
}}, nil, nil)
test.IsEqualBool(t, IsAuthenticated(w, r), true)
user, ok := IsAuthenticated(w, r)
test.IsEqualBool(t, ok, true)
test.IsEqualInt(t, user.Id, 7)
test.IsEqualInt(t, exitCode, 0)
Init(models.AuthenticationConfig{
Method: 10,
})
test.IsEqualInt(t, exitCode, 3)
}
func testAuthHeader(t *testing.T) {
w, r := test.GetRecorder("GET", "/", nil, nil, nil)
Init(modelHeader)
test.IsEqualBool(t, IsAuthenticated(w, r), false)
_, ok := IsAuthenticated(w, r)
test.IsEqualBool(t, ok, false)
w, r = test.GetRecorder("GET", "/", nil, []test.Header{{
Name: "testHeader",
Value: "testUser",
}}, nil)
test.IsEqualBool(t, IsAuthenticated(w, r), true)
authSettings.HeaderUsers = []string{"testUser"}
test.IsEqualBool(t, IsAuthenticated(w, r), true)
authSettings.HeaderUsers = []string{"otherUser"}
test.IsEqualBool(t, IsAuthenticated(w, r), false)
authSettings.HeaderKey = ""
authSettings.HeaderUsers = []string{}
test.IsEqualBool(t, IsAuthenticated(w, r), false)
user, ok := IsAuthenticated(w, r)
test.IsEqualString(t, user.Name, "testuser")
test.IsEqualBool(t, ok, true)
authSettings.OnlyRegisteredUsers = true
w, r = test.GetRecorder("GET", "/", nil, []test.Header{{
Name: "testHeader",
Value: "testUser",
}}, nil)
_, ok = IsAuthenticated(w, r)
test.IsEqualBool(t, ok, true)
w, r = test.GetRecorder("GET", "/", nil, []test.Header{{
Name: "testHeader",
Value: "otherUser2",
}}, nil)
_, ok = IsAuthenticated(w, r)
test.IsEqualBool(t, ok, false)
authSettings.OnlyRegisteredUsers = false
}
func testAuthDisabled(t *testing.T) {
w, r := test.GetRecorder("GET", "/", nil, nil, nil)
Init(modelDisabled)
test.IsEqualBool(t, IsAuthenticated(w, r), true)
user, ok := IsAuthenticated(w, r)
test.IsEqualBool(t, ok, true)
test.IsEqualInt(t, user.Id, 5)
}
func TestIsLogoutAvailable(t *testing.T) {
authSettings.Method = Internal
authSettings.Method = models.AuthenticationInternal
test.IsEqualBool(t, IsLogoutAvailable(), true)
authSettings.Method = OAuth2
authSettings.Method = models.AuthenticationOAuth2
test.IsEqualBool(t, IsLogoutAvailable(), true)
authSettings.Method = Header
authSettings.Method = models.AuthenticationHeader
test.IsEqualBool(t, IsLogoutAvailable(), false)
authSettings.Method = Disabled
authSettings.Method = models.AuthenticationDisabled
test.IsEqualBool(t, IsLogoutAvailable(), false)
}
@@ -106,25 +191,54 @@ func TestRedirect(t *testing.T) {
test.IsEqualString(t, string(output), "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./test\"></head></html>")
}
func TestGetUserFromRequest(t *testing.T) {
_, r := test.GetRecorder("GET", "/", nil, nil, nil)
_, err := GetUserFromRequest(r)
test.IsNotNil(t, err)
c := context.WithValue(r.Context(), "user", "invalid")
rInvalid := r.WithContext(c)
_, err = GetUserFromRequest(rInvalid)
test.IsNotNil(t, err)
user := models.User{
Id: 1,
Name: "test",
Permissions: 1,
UserLevel: 2,
LastOnline: 3,
Password: "12345",
ResetPassword: true,
}
c = context.WithValue(r.Context(), "user", user)
rValid := r.WithContext(c)
retrievedUser, err := GetUserFromRequest(rValid)
test.IsNil(t, err)
test.IsEqual(t, retrievedUser, user)
}
func TestIsValidOauthUser(t *testing.T) {
Init(modelOauth)
info := OAuthUserInfo{Subject: "randomid"}
test.IsEqualBool(t, isValidOauthUser(info, "", []string{}), true)
test.IsEqualBool(t, isValidOauthUser(info, "test1", []string{"test2"}), true)
authSettings.OAuthUserScope = "user"
authSettings.OAuthUsers = []string{"otheruser"}
test.IsEqualBool(t, isValidOauthUser(info, "test1", []string{}), false)
test.IsEqualBool(t, isValidOauthUser(info, "otheruser", []string{}), true)
info := OAuthUserInfo{Email: "", Subject: "randomid"}
test.IsEqualBool(t, isValidOauthUser(info, []string{}), false)
info.Email = "newemail"
test.IsEqualBool(t, isValidOauthUser(info, []string{}), true)
test.IsEqualBool(t, isValidOauthUser(info, []string{"test2"}), true)
test.IsEqualBool(t, isValidOauthUser(info, []string{}), true)
authSettings.OAuthGroupScope = "group"
authSettings.OAuthGroups = []string{"othergroup"}
test.IsEqualBool(t, isValidOauthUser(info, "test1", []string{}), false)
test.IsEqualBool(t, isValidOauthUser(info, "otheruser", []string{}), false)
test.IsEqualBool(t, isValidOauthUser(info, "test1", []string{"testgroup"}), false)
test.IsEqualBool(t, isValidOauthUser(info, "test1", []string{"testgroup", "othergroup"}), false)
test.IsEqualBool(t, isValidOauthUser(info, "otheruser", []string{"othergroup"}), true)
test.IsEqualBool(t, isValidOauthUser(info, "otheruser", []string{"testgroup", "othergroup"}), true)
info.Email = "test1"
test.IsEqualBool(t, isValidOauthUser(info, []string{}), false)
info.Email = "otheruser"
test.IsEqualBool(t, isValidOauthUser(info, []string{}), false)
info.Email = "test1"
test.IsEqualBool(t, isValidOauthUser(info, []string{"testgroup"}), false)
test.IsEqualBool(t, isValidOauthUser(info, []string{"testgroup", "othergroup"}), true)
info.Email = "otheruser"
test.IsEqualBool(t, isValidOauthUser(info, []string{"othergroup"}), true)
test.IsEqualBool(t, isValidOauthUser(info, []string{"testgroup", "othergroup"}), true)
info.Subject = ""
test.IsEqualBool(t, isValidOauthUser(info, "otheruser", []string{"testgroup", "othergroup"}), false)
test.IsEqualBool(t, isValidOauthUser(info, []string{"testgroup", "othergroup"}), false)
}
func TestWildcardMatch(t *testing.T) {
@@ -194,10 +308,39 @@ func TestWildcardMatch(t *testing.T) {
}
}
func getRecorder(cookies []test.Cookie) (*httptest.ResponseRecorder, *http.Request, bool, int) {
w, r := test.GetRecorder("GET", "/", cookies, nil, nil)
return w, r, false, 1
}
func TestLogout(t *testing.T) {
Init(modelUserPW)
w, r := test.GetRecorder("GET", "/", nil, nil, nil)
w, r, _, _ := getRecorder([]test.Cookie{{
Name: "session_token",
Value: "logoutsession"},
})
_, ok := sessionmanager.IsValidSession(w, r, false, 0)
test.IsEqualBool(t, ok, true)
Logout(w, r)
_, ok = database.GetSession("logoutsession")
test.IsEqualBool(t, ok, false)
_, ok = sessionmanager.IsValidSession(w, r, false, 0)
test.IsEqualBool(t, ok, false)
test.ResponseBodyContains(t, w, "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./login\"></head></html>")
Init(modelOauth)
w, r, _, _ = getRecorder([]test.Cookie{{
Name: "session_token",
Value: "logoutsession2"},
})
_, ok = sessionmanager.IsValidSession(w, r, false, 0)
test.IsEqualBool(t, ok, true)
Logout(w, r)
_, ok = database.GetSession("logoutsession")
test.IsEqualBool(t, ok, false)
_, ok = sessionmanager.IsValidSession(w, r, false, 0)
test.IsEqualBool(t, ok, false)
test.ResponseBodyContains(t, w, "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=./login?consent=true\"></head></html>")
}
type testInfo struct {
@@ -223,29 +366,27 @@ func TestCheckOauthUser(t *testing.T) {
info.Subject = "random"
output, err = getOuthUserOutput(t, info)
test.IsNil(t, err)
test.IsEqualString(t, redirectsToSite(output), "admin")
info.Email = "test@test.com"
authSettings.OAuthUserScope = "email"
authSettings.OAuthUsers = []string{"otheruser"}
output, err = getOuthUserOutput(t, info)
test.IsNil(t, err)
test.IsEqualString(t, redirectsToSite(output), "error-auth")
authSettings.OAuthUsers = []string{"test@test.com"}
info.Email = "random"
output, err = getOuthUserOutput(t, info)
test.IsNil(t, err)
test.IsEqualString(t, redirectsToSite(output), "admin")
authSettings.OAuthUsers = []string{"otheruser@test"}
info.Email = "test@test-invalid.com"
authSettings.OnlyRegisteredUsers = true
output, err = getOuthUserOutput(t, info)
test.IsNil(t, err)
test.IsEqualString(t, redirectsToSite(output), "error-auth")
authSettings.OAuthUserScope = "invalidScope"
_, err = getOuthUserOutput(t, info)
test.IsNotNil(t, err)
info.Email = "random"
output, err = getOuthUserOutput(t, info)
test.IsNil(t, err)
test.IsEqualString(t, redirectsToSite(output), "admin")
authSettings.OnlyRegisteredUsers = false
authSettings.OAuthGroups = []string{"otheruser@test"}
authSettings.OAuthGroupScope = "groupscope"
newClaims := testInfo{Output: []byte("{invalid")}
info.ClaimsSent = newClaims
_, err = getOuthUserOutput(t, info)
@@ -274,66 +415,28 @@ func getOuthUserOutput(t *testing.T, info OAuthUserInfo) (string, error) {
}
var modelUserPW = models.AuthenticationConfig{
Method: Internal,
SaltAdmin: "1234",
SaltFiles: "1234",
Username: "admin",
Password: "7d23732d69c050bf7a2f5ab7d979f92f33bb585e",
HeaderKey: "",
OAuthProvider: "",
OAuthClientId: "",
OAuthClientSecret: "",
HeaderUsers: nil,
OAuthUsers: nil,
OAuthGroups: nil,
OAuthUserScope: "",
OAuthGroupScope: "",
Method: models.AuthenticationInternal,
SaltAdmin: testconfiguration.SaltAdmin,
SaltFiles: "1234",
Username: "test",
}
var modelOauth = models.AuthenticationConfig{
Method: OAuth2,
SaltAdmin: "1234",
SaltFiles: "1234",
Username: "",
Password: "",
HeaderKey: "",
OAuthProvider: "test",
OAuthClientId: "test",
OAuthClientSecret: "test",
HeaderUsers: nil,
OAuthUsers: nil,
OAuthGroups: nil,
OAuthUserScope: "",
OAuthGroupScope: "",
Method: models.AuthenticationOAuth2,
SaltAdmin: testconfiguration.SaltAdmin,
SaltFiles: "1234",
OAuthProvider: "test",
OAuthClientId: "test",
OAuthClientSecret: "test",
OAuthRecheckInterval: 1,
}
var modelHeader = models.AuthenticationConfig{
Method: Header,
SaltAdmin: "1234",
SaltFiles: "1234",
Username: "",
Password: "",
HeaderKey: "testHeader",
OAuthProvider: "",
OAuthClientId: "",
OAuthClientSecret: "",
HeaderUsers: nil,
OAuthUsers: nil,
OAuthGroups: nil,
OAuthUserScope: "",
OAuthGroupScope: "",
Method: models.AuthenticationHeader,
SaltAdmin: testconfiguration.SaltAdmin,
SaltFiles: "1234",
HeaderKey: "testHeader",
}
var modelDisabled = models.AuthenticationConfig{
Method: Disabled,
SaltAdmin: "1234",
SaltFiles: "1234",
Username: "",
Password: "",
HeaderKey: "",
OAuthProvider: "",
OAuthClientId: "",
OAuthClientSecret: "",
HeaderUsers: nil,
OAuthUsers: nil,
OAuthGroups: nil,
OAuthUserScope: "",
OAuthGroupScope: "",
Method: models.AuthenticationDisabled,
SaltAdmin: testconfiguration.SaltAdmin,
SaltFiles: "1234",
}
@@ -28,9 +28,6 @@ func Init(baseUrl string, credentials models.AuthenticationConfig) {
systemConfig := configuration.Get()
scopes := []string{oidc.ScopeOpenID, "profile"}
if systemConfig.Authentication.OAuthUserScope != "" {
scopes = append(scopes, systemConfig.Authentication.OAuthUserScope)
}
if systemConfig.Authentication.OAuthGroupScope != "" {
scopes = append(scopes, systemConfig.Authentication.OAuthGroupScope)
}
@@ -97,6 +94,10 @@ func HandlerCallback(w http.ResponseWriter, r *http.Request) {
showOauthErrorPage(w, r, "Failed to get userinfo: "+err.Error())
return
}
if userInfo.Email == "" {
showOauthErrorPage(w, r, "An empty email address was provided, cannot continue.")
return
}
info := authentication.OAuthUserInfo{
Subject: userInfo.Subject,
Email: userInfo.Email,
@@ -12,26 +12,29 @@ import (
"time"
)
// TODO add username to check for revocation
// If no login occurred during this time, the admin session will be deleted. Default 30 days
const cookieLifeAdmin = 30 * 24 * time.Hour
const lengthSessionId = 60
// IsValidSession checks if the user is submitting a valid session token
// If valid session is found, useSession will be called
// Returns true if authenticated, otherwise false
func IsValidSession(w http.ResponseWriter, r *http.Request, isOauth bool, OAuthRecheckInterval int) bool {
func IsValidSession(w http.ResponseWriter, r *http.Request, isOauth bool, OAuthRecheckInterval int) (models.User, bool) {
cookie, err := r.Cookie("session_token")
if err == nil {
sessionString := cookie.Value
if sessionString != "" {
session, ok := database.GetSession(sessionString)
if ok {
return useSession(w, sessionString, session, isOauth, OAuthRecheckInterval)
user, userExists := database.GetUser(session.UserId)
if !userExists {
return user, false
}
return user, useSession(w, sessionString, session, isOauth, OAuthRecheckInterval)
}
}
}
return false
return models.User{}, false
}
// useSession checks if a session is still valid. It Changes the session string
@@ -44,24 +47,26 @@ func useSession(w http.ResponseWriter, id string, session models.Session, isOaut
return false
}
if session.RenewAt < time.Now().Unix() {
CreateSession(w, isOauth, OAuthRecheckInterval)
CreateSession(w, isOauth, OAuthRecheckInterval, session.UserId)
database.DeleteSession(id)
}
go database.UpdateUserLastOnline(session.UserId)
return true
}
// CreateSession creates a new session - called after login with correct username / password
// If sessions parameter is nil, it will be loaded from config
func CreateSession(w http.ResponseWriter, isOauth bool, OAuthRecheckInterval int) {
func CreateSession(w http.ResponseWriter, isOauth bool, OAuthRecheckInterval int, userId int) {
timeExpiry := time.Now().Add(cookieLifeAdmin)
if isOauth {
timeExpiry = time.Now().Add(time.Duration(OAuthRecheckInterval) * time.Hour)
}
sessionString := helper.GenerateRandomString(60)
sessionString := helper.GenerateRandomString(lengthSessionId)
database.SaveSession(sessionString, models.Session{
RenewAt: time.Now().Add(12 * time.Hour).Unix(),
ValidUntil: timeExpiry.Unix(),
UserId: userId,
})
writeSessionCookie(w, sessionString, timeExpiry)
}
@@ -2,12 +2,15 @@ package sessionmanager
import (
"github.com/forceu/gokapi/internal/configuration"
"github.com/forceu/gokapi/internal/configuration/database"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/test"
"github.com/forceu/gokapi/internal/test/testconfiguration"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
)
var newSession string
@@ -27,24 +30,39 @@ func getRecorder(cookies []test.Cookie) (*httptest.ResponseRecorder, *http.Reque
}
func TestIsValidSession(t *testing.T) {
test.IsEqualBool(t, IsValidSession(getRecorder(nil)), false)
test.IsEqualBool(t, IsValidSession(getRecorder([]test.Cookie{{
user, ok := IsValidSession(getRecorder(nil))
test.IsEqualBool(t, ok, false)
user, ok = IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: "invalid"},
})), false)
test.IsEqualBool(t, IsValidSession(getRecorder([]test.Cookie{{
}))
test.IsEqualBool(t, ok, false)
user, ok = IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: ""},
}))
test.IsEqualBool(t, ok, false)
user, ok = IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: "expiredsession"},
})), false)
test.IsEqualBool(t, IsValidSession(getRecorder([]test.Cookie{{
}))
test.IsEqualBool(t, ok, false)
user, ok = IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: "validsession"},
})), true)
}))
test.IsEqualBool(t, ok, true)
_, ok = IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: "validSessionInvalidUser"},
}))
test.IsEqualBool(t, ok, false)
test.IsEqualInt(t, user.Id, 7)
w, r, _, _ := getRecorder([]test.Cookie{{
Name: "session_token",
Value: "needsRenewal"},
})
test.IsEqualBool(t, IsValidSession(w, r, false, 1), true)
user, ok = IsValidSession(w, r, false, 1)
cookies := w.Result().Cookies()
test.IsEqualInt(t, len(cookies), 1)
test.IsEqualString(t, cookies[0].Name, "session_token")
@@ -55,30 +73,48 @@ func TestIsValidSession(t *testing.T) {
func TestCreateSession(t *testing.T) {
w, _, _, _ := getRecorder(nil)
CreateSession(w, false, 1)
CreateSession(w, false, 1, 5)
cookies := w.Result().Cookies()
test.IsEqualInt(t, len(cookies), 1)
test.IsEqualString(t, cookies[0].Name, "session_token")
newSession = cookies[0].Value
test.IsEqualInt(t, len(newSession), 60)
test.IsEqualBool(t, IsValidSession(getRecorder([]test.Cookie{{
user, ok := IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: newSession},
})), true)
}))
test.IsEqualBool(t, ok, true)
test.IsEqualInt(t, user.Id, 5)
w, _, _, _ = getRecorder(nil)
CreateSession(w, true, 20, 50)
cookies = w.Result().Cookies()
newOauthSession := cookies[0].Value
var session models.Session
session, ok = database.GetSession(newOauthSession)
test.IsEqualBool(t, ok, true)
isEqual := time.Now().Add(20*time.Hour).Unix()-session.ValidUntil < 10 &&
time.Now().Add(20*time.Hour).Unix()-session.ValidUntil > -1
test.IsEqualBool(t, isEqual, true)
}
func TestLogoutSession(t *testing.T) {
test.IsEqualBool(t, IsValidSession(getRecorder([]test.Cookie{{
user, ok := IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: newSession},
})), true)
}))
test.IsEqualBool(t, ok, true)
test.IsEqualInt(t, user.Id, 5)
w, r, _, _ := getRecorder([]test.Cookie{{
Name: "session_token",
Value: newSession},
})
LogoutSession(w, r)
test.IsEqualBool(t, IsValidSession(getRecorder([]test.Cookie{{
_, ok = IsValidSession(getRecorder([]test.Cookie{{
Name: "session_token",
Value: newSession},
})), false)
}))
test.IsEqualBool(t, ok, false)
}
+23 -18
View File
@@ -11,8 +11,8 @@ import (
"time"
)
// Process processes a file upload request
func Process(w http.ResponseWriter, r *http.Request, maxMemory int) error {
// ProcessCompleteFile processes a file upload request
func ProcessCompleteFile(w http.ResponseWriter, r *http.Request, userId, maxMemory int) error {
err := r.ParseMultipartForm(int64(maxMemory) * 1024 * 1024)
if err != nil {
return err
@@ -27,7 +27,7 @@ func Process(w http.ResponseWriter, r *http.Request, maxMemory int) error {
return err
}
result, err := storage.NewFile(file, header, config)
result, err := storage.NewFile(file, header, userId, config)
defer file.Close()
if err != nil {
return err
@@ -83,8 +83,25 @@ func ParseFileHeader(r *http.Request) (string, chunking.FileHeader, models.Uploa
// CompleteChunk processes a file after all the chunks have been completed
// The parameters can be generated with ParseFileHeader()
func CompleteChunk(chunkId string, header chunking.FileHeader, config models.UploadRequest) (models.File, error) {
return storage.NewFileFromChunk(chunkId, header, config)
func CompleteChunk(chunkId string, header chunking.FileHeader, userId int, config models.UploadRequest) (models.File, error) {
return storage.NewFileFromChunk(chunkId, header, userId, config)
}
// CreateUploadConfig populates a new models.UploadRequest struct
func CreateUploadConfig(allowedDownloads, expiryDays int, password string, unlimitedTime, unlimitedDownload, isEnd2End bool, realSize int64) models.UploadRequest {
settings := configuration.Get()
return models.UploadRequest{
AllowedDownloads: allowedDownloads,
Expiry: expiryDays,
ExpiryTimestamp: time.Now().Add(time.Duration(expiryDays) * time.Hour * 24).Unix(),
Password: password,
ExternalUrl: settings.ServerUrl,
MaxMemory: settings.MaxMemory,
UnlimitedTime: unlimitedTime,
UnlimitedDownload: unlimitedDownload,
IsEndToEndEncrypted: isEnd2End,
RealSize: realSize,
}
}
func parseConfig(values formOrHeader) (models.UploadRequest, error) {
@@ -120,19 +137,7 @@ func parseConfig(values formOrHeader) (models.UploadRequest, error) {
return models.UploadRequest{}, err
}
}
settings := configuration.Get()
return models.UploadRequest{
AllowedDownloads: allowedDownloadsInt,
Expiry: expiryDaysInt,
ExpiryTimestamp: time.Now().Add(time.Duration(expiryDaysInt) * time.Hour * 24).Unix(),
Password: password,
ExternalUrl: settings.ServerUrl,
MaxMemory: settings.MaxMemory,
UnlimitedTime: unlimitedTime,
UnlimitedDownload: unlimitedDownload,
IsEndToEndEncrypted: isEnd2End,
RealSize: realSize,
}, nil
return CreateUploadConfig(allowedDownloadsInt, expiryDaysInt, password, unlimitedTime, unlimitedDownload, isEnd2End, realSize), nil
}
type formOrHeader interface {
@@ -74,12 +74,12 @@ func TestParseConfig(t *testing.T) {
func TestProcess(t *testing.T) {
w, r := test.GetRecorder("POST", "/upload", nil, nil, strings.NewReader("invalid§$%&%§"))
err := Process(w, r, 20)
err := ProcessCompleteFile(w, r, 9, 20)
test.IsNotNil(t, err)
w = httptest.NewRecorder()
r = getFileUploadRecorder(false)
err = Process(w, r, 20)
err = ProcessCompleteFile(w, r, 9, 20)
test.IsNil(t, err)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
@@ -93,6 +93,7 @@ func TestProcess(t *testing.T) {
test.IsEqualString(t, result.FileInfo.Size, "11 B")
test.IsEqualBool(t, result.FileInfo.UnlimitedTime, false)
test.IsEqualBool(t, result.FileInfo.UnlimitedDownloads, false)
test.IsEqualInt(t, result.FileInfo.UploaderId, 9)
}
func TestProcessNewChunk(t *testing.T) {
@@ -115,16 +116,28 @@ func TestProcessNewChunk(t *testing.T) {
}
func TestCompleteChunk(t *testing.T) {
w, r := test.GetRecorder("POST", "/uploadComplete", nil, nil, strings.NewReader("invalid§$%&%§"))
body := strings.NewReader("%")
r := httptest.NewRequest(http.MethodPost, "/upload", body)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
_, _, _, err := ParseFileHeader(r)
test.IsNotNil(t, err)
w = httptest.NewRecorder()
w := httptest.NewRecorder()
r = getFileUploadRecorder(false)
_, _, _, err = ParseFileHeader(r)
test.IsNotNil(t, err)
data := url.Values{}
data.Set("isE2E", "true")
data.Set("realSize", "none")
w, r = test.GetRecorder("POST", "/uploadComplete", nil, nil, strings.NewReader(data.Encode()))
r.Header.Set("Content-type", "application/x-www-form-urlencoded")
chunkId, header, config, err := ParseFileHeader(r)
test.IsNotNil(t, err)
data.Del("isE2E")
data.Del("realSize")
data.Set("allowedDownloads", "9")
data.Set("expiryDays", "5")
data.Set("password", "123")
@@ -133,9 +146,9 @@ func TestCompleteChunk(t *testing.T) {
data.Set("filesize", "13")
w, r = test.GetRecorder("POST", "/uploadComplete", nil, nil, strings.NewReader(data.Encode()))
r.Header.Set("Content-type", "application/x-www-form-urlencoded")
chunkId, header, config, err := ParseFileHeader(r)
chunkId, header, config, err = ParseFileHeader(r)
test.IsNil(t, err)
file, err := CompleteChunk(chunkId, header, config)
file, err := CompleteChunk(chunkId, header, 9, config)
test.IsNil(t, err)
test.IsEqualString(t, file.Name, "random.file")
@@ -148,7 +161,7 @@ func TestCompleteChunk(t *testing.T) {
r.Header.Set("Content-type", "application/x-www-form-urlencoded")
_, _, _, err = ParseFileHeader(r)
test.IsNil(t, err)
_, err = CompleteChunk(chunkId, header, config)
_, err = CompleteChunk(chunkId, header, 9, config)
test.IsNotNil(t, err)
}
@@ -3,7 +3,7 @@
"info": {
"title": "Gokapi",
"description": "[https://github.com/Forceu/Gokapi](https://github.com/Forceu/Gokapi)\n",
"version": "1.0"
"version": "2.0"
},
"servers": [
{
@@ -22,6 +22,12 @@
},
{
"name": "auth"
},
{
"name": "user"
},
{
"name": "chunk"
}
],
"paths": {
@@ -31,7 +37,7 @@
"files"
],
"summary": "Lists all files",
"description": "This API call lists all files that are not expired. Returns null, if no files are stored. Requires permission VIEW",
"description": "This API call lists all files that are not expired. Returns null, if no files are stored. Requires API permission VIEW. To view files that were not uploaded by the user, the user needs to have the user permission LIST",
"operationId": "list",
"security": [
{
@@ -57,7 +63,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -68,7 +74,7 @@
"files"
],
"summary": "Get metadata by ID",
"description": "This API call lists all metadata about a file that is not expired. Returns 404 if an invalid/expired ID was passed. Requires permission VIEW",
"description": "This API call lists all metadata about a file that is not expired. Returns 404 if an invalid/expired ID was passed. Requires API permission VIEW. To view files that were not uploaded by the user, the user needs to have the user permission LIST",
"operationId": "listbyid",
"parameters": [
{
@@ -102,7 +108,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -116,7 +122,7 @@
"chunk"
],
"summary": "Uploads a new chunk",
"description": "Uploads a file in chunks, in case a reverse proxy does not support upload of larger files. Parallel uploading is supported. Must call /chunk/complete after all chunks have been uploaded. WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires permission UPLOAD",
"description": "Uploads a file in chunks, in case a reverse proxy does not support upload of larger files. Parallel uploading is supported. Must call /chunk/complete after all chunks have been uploaded. WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires API permission UPLOAD",
"operationId": "chunkadd",
"security": [
{
@@ -148,7 +154,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -159,23 +165,86 @@
"chunk"
],
"summary": "Finalises uploaded chunks",
"description": "Needs to be called after all chunks have been uploaded. Adds the uploaded file to Gokapi. Requires permission UPLOAD",
"description": "Needs to be called after all chunks have been uploaded. Adds the uploaded file to Gokapi. Requires API permission UPLOAD",
"operationId": "chunkcomplete",
"security": [
{
"apikey": ["UPLOAD"]
},
],
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/chunkingcomplete"
}
}
},
"required": true
"parameters": [
{
"name": "uuid",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "The unique ID that was used for the uploaded chunks"
},{
"name": "filename",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "The filename of the uploaded file"
},
{
"name": "filesize",
"in": "header",
"required": true,
"schema": {
"type": "integer"
},
"description": "The total filesize of the uploaded file in bytes"
},
{
"name": "contenttype",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "The MIME content type. If empty, application/octet-stream will be used."
},
{
"name": "allowedDownloads",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many downloads are allowed. Default of 1 will be used if empty. Unlimited if 0 is passed."
},
{
"name": "expiryDays",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many days the file will be stored. Original value will be used if empty. Unlimited if 0 is passed."
},
{
"name": "password",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Password for this file to be set. No password will be used if empty value is passed."
},
{
"name": "nonblocking",
"in": "header",
"required": false,
"schema": {
"type": "boolean"
},
"description": "If true, the call is non blocking and does not wait until the upload is fully processed. No info regarding the file or any errors during the processing will be included in the output."
}
],
"responses": {
"200": {
"description": "Operation successful",
@@ -191,7 +260,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -202,7 +271,7 @@
"files"
],
"summary": "Adds a new file without chunking",
"description": "Uploads the submitted file to Gokapi. Please note: This method does not use chunking, therefore if you are behind a reverse proxy or have a provider that limits upload filesizes, this might not work for bigger files (e.g. Cloudflare). WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires permission UPLOAD",
"description": "Uploads the submitted file to Gokapi. Please note: This method does not use chunking, therefore if you are behind a reverse proxy or have a provider that limits upload filesizes, this might not work for bigger files (e.g. Cloudflare). WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires API permission UPLOAD",
"operationId": "add",
"security": [
{
@@ -234,7 +303,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -245,23 +314,69 @@
"files"
],
"summary": "Duplicates an existing file",
"description": "This API call duplicates an existing file with new parameters. Requires permission UPLOAD",
"description": "This API call duplicates an existing file with new parameters. Requires API permission UPLOAD. To duplicate files that were not uploaded by the user, the user needs to have the user permission LIST",
"operationId": "duplicate",
"security": [
{
"apikey": ["VIEW","UPLOAD"]
},
],
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/duplicate"
}
}
},
"required": true
"parameters": [
{
"name": "id",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of file to be duplicated"
},
{
"name": "allowedDownloads",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many remaining downloads are allowed. Original value will be used if empty. Unlimited if 0 is passed."
},
{
"name": "expiryDays",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many days the file will be stored. Original value will be used if empty. Unlimited if 0 is passed."
},
{
"name": "password",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Password for this file to be set. No password will be used if empty value is passed."
},
{
"name": "originalPassword",
"in": "header",
"required": false,
"schema": {
"type": "boolean"
},
"description": "Set to true to use the original password. Field \"password\" will be ignored if set."
},
{
"name": "filename",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Sets a new filename. Filename will be unchanged if empty."
}
],
"responses": {
"200": {
"description": "Operation successful",
@@ -277,7 +392,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -291,7 +406,7 @@
"files"
],
"summary": "Changes parameters of an uploaded file",
"description": "This API call changes parameters of an uploaded file. Requires permission EDIT",
"description": "This API call changes parameters of an uploaded file. Requires API permission EDIT. To edit files that were not uploaded by the user, the user needs to have the user permission EDIT",
"operationId": "modifyfile",
"security": [
{
@@ -360,7 +475,7 @@
"description": "Invalid ID supplied or incorrect data type sent"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -374,7 +489,7 @@
"files"
],
"summary": "Replaces an uploaded file",
"description": "This API replaces the content of an uploaded file with the content of a different (already uplaoded) file. Note: Replacing end-to-end ecrypted files is NOT possible and will result in an error. Requires permission REPLACE",
"description": "This API replaces the content of an uploaded file with the content of a different (already uplaoded) file. Note: Replacing end-to-end ecrypted files is NOT possible and will result in an error. Requires API permission REPLACE. To replace a file that was not uploaded by the user, the user needs to have the user permission REPLACE_OTHERS. To replace a file with the content uploaded by another user, the user needs to have the user permission LIST",
"operationId": "replacefile",
"security": [
{
@@ -424,7 +539,7 @@
"description": "Invalid ID supplied or incorrect data type sent"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -438,7 +553,7 @@
"files"
],
"summary": "Deletes the selected file",
"description": "This API call deletes the selected file and runs the clean-up procedure which purges all expired files from the data directory immediately. Requires permission DELETE",
"description": "This API call deletes the selected file and runs the clean-up procedure which purges all expired files from the data directory immediately. Requires API permission DELETE. To delete a file that was not uploaded by the user, the user needs to have the user permission DELETE",
"operationId": "delete",
"security": [
{
@@ -462,11 +577,11 @@
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID supplied"
}
}
}
@@ -477,7 +592,7 @@
"auth"
],
"summary": "Creates a new API key",
"description": "This API call returns a new API key. The new key does not have any permissions, unless specified. Requires permission API_MOD",
"description": "This API call returns a new API key. The new key does not have any permissions, unless specified. Requires API permission API_MOD",
"operationId": "create",
"security": [
{
@@ -520,7 +635,7 @@
}
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -531,7 +646,7 @@
"auth"
],
"summary": "Changes the name of the API key",
"description": "This API call changes the name of the API key that is shown in the API overview. Requires permission API_MOD",
"description": "This API call changes the name of the API key that is shown in the API overview. Requires API permission API_MOD. To change the name of an API key not owned by the user, the user needs to have the user permission API",
"operationId": "friendlyname",
"security": [
{
@@ -540,9 +655,9 @@
],
"parameters": [
{
"name": "apiKeyToModify",
"name": "targetKey",
"in": "header",
"description": "The API key to change the name of",
"description": "The API key to change the name of. Can be either the public ID or the actual API key",
"required": true,
"style": "simple",
"explode": false,
@@ -566,11 +681,11 @@
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "API key not found"
}
}
}
@@ -581,7 +696,7 @@
"auth"
],
"summary": "Changes the permissions of the API key",
"description": "This API call changes the permissions for the given API key. Requires permission API_MOD",
"description": "This API call changes the permission for the given API key. Requires API permission API_MOD. To to edit an API key not owned by the user, the user needs to have the user permission API",
"operationId": "modifypermission",
"security": [
{
@@ -590,9 +705,9 @@
],
"parameters": [
{
"name": "apiKeyToModify",
"name": "targetKey",
"in": "header",
"description": "The API key to change the permission of",
"description": "The API key to change the permission of. Can be either the public ID or the actual API key",
"required": true,
"style": "simple",
"explode": false,
@@ -609,7 +724,7 @@
"explode": false,
"schema": {
"type": "string",
"enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_API_MOD"]
"enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_REPLACE", "PERM_MANAGE_USERS", "PERM_API_MOD"]
}
},
{
@@ -630,10 +745,13 @@
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
"description": "Invalid parameter supplied or API key owner does not have the sufficient user permissions"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "API key not found"
}
}
}
@@ -644,7 +762,7 @@
"auth"
],
"summary": "Deletes an API key",
"description": "This API call deletes the given API key. Requires permission API_MOD",
"description": "This API call deletes the given API key. Requires API permission API_MOD. To to delete an API key not owned by the user, the user needs to have the user permission API",
"operationId": "apidelete",
"security": [
{
@@ -653,9 +771,9 @@
],
"parameters": [
{
"name": "apiKeyToModify",
"name": "targetKey",
"in": "header",
"description": "The API key to delete",
"description": "The API key to delete. Can be either the public ID or the actual API key",
"required": true,
"style": "simple",
"explode": false,
@@ -671,12 +789,287 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided"
"404": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
}
},
"/user/create": {
"post": {
"tags": [
"user"
],
"summary": "Creates a new user",
"description": "This API call adds a new user. The new user does not have any specific permissions and is userlevel USER. Requires API permission MANAGE_USERS",
"operationId": "createuser",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "username",
"in": "header",
"description": "Name of new user, must be at least 4 characters",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Operation successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NewUser"
}
}
}
},
"400": {
"description": "Invalid parameters supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"409": {
"description": "A user already exists with that email address"
}
}
}
},
"/user/modify": {
"put": {
"tags": [
"user"
],
"summary": "Changes the permissions of a user",
"description": "This API call changes the permission for the given user. Requires API permission MANAGE_USERS",
"operationId": "usermodify",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to change the permission of",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},
{
"name": "userpermission",
"in": "header",
"description": "The name of the permission",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string",
"enum": ["PERM_REPLACE", "PERM_LIST", "PERM_EDIT", "PERM_REPLACE_OTHER", "PERM_DELETE", "PERM_LOGS", "PERM_API", "PERM_USERS"]
}
},
{
"name": "permissionModifier",
"in": "header",
"description": "If the permission shall be granted or revoked",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string",
"enum": ["GRANT", "REVOKE"]
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid parameter supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "User not found"
}
}
}
},
"/user/changeRank": {
"put": {
"tags": [
"user"
],
"summary": "Changes the rank of a user",
"description": "This API call changes the rank for the given user. Requires API permission MANAGE_USERS",
"operationId": "userchangerank",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to change the rank of",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},
{
"name": "newRank",
"in": "header",
"description": "The name of the new rank",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string",
"enum": ["ADMIN", "USER"]
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid parameter supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "User not found"
}
}
}
},
"/user/delete": {
"delete": {
"tags": [
"user"
],
"summary": "Deletes the selected user",
"description": "This API call changes deletes the given user. If files are associated with the user, they will be linked with the user that initiated the deletion. If deleteFiles is \"true\", the files will be deleted instead. Requires API permission MANAGE_USERS",
"operationId": "userdelete",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to be deleted",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},
{
"name": "deleteFiles",
"in": "header",
"description": "Delete all associated uploads from this user",
"required": false,
"style": "simple",
"explode": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID or parameters supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
},
"/user/resetPassword": {
"put": {
"tags": [
"user"
],
"summary": "Resets the password of the current user",
"description": "This API call forces a passwrd change once the given user logs in the next time. If generateNewPassword is \"true\", the current password will be replaced with a generated one. Requires API permission MANAGE_USERS",
"operationId": "userresetpw",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to reset",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},{
"name": "generateNewPassword",
"in": "header",
"description": "Generate a new password",
"required": false,
"style": "simple",
"explode": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Operation successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PasswordReset"
}
}
}
},
"400": {
"description": "Invalid ID or parameters supplied, user is super admin or user is equal to owner of API key"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
},
},
"components": {
"schemas": {
@@ -826,11 +1219,62 @@
"Id": {
"type": "string",
"example": "ar3iecahghiethiemeeR"
},
"PublicId": {
"type": "string",
"example": "oepah5iesae8YeeZohrain5ahNgax8su"
}
},
"description": "NewApiKey is the struct used for the result after creating a new API key",
"x-go-package": "Gokapi/internal/models"
},
"NewUser": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "user@gokapi"
},
"id": {
"type": "integer",
"example": 14
},
"lastOnline": {
"type": "integer",
"example": 0
},
"name": {
"type": "string",
"example": "Gokapi user"
},
"permissions": {
"type": "integer",
"example": 0
},
"userLevel": {
"type": "integer",
"example": 2
}
},
"description": "NewUser is the struct used for the result after creating a new API key",
"x-go-package": "Gokapi/internal/models"
},
"PasswordReset": {
"type": "object",
"properties": {
"result": {
"type": "string",
"example": "OK"
},
"password": {
"type": "string",
"description": "Empty if no new password was generated, otherwise contains the new password",
"example": "ahseth6ahV"
}
},
"description": "NewUser is the struct used for the result after creating a new API key",
"x-go-package": "Gokapi/internal/models"
},
"body": {
"required": [
"file"
@@ -855,38 +1299,7 @@
"description": "Password for this file to be set. No password will be used if empty"
}
}
},"duplicate": {
"required": [
"id"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of file to be duplicated"
},
"allowedDownloads": {
"type": "integer",
"description": "How many downloads are allowed. Original value from web interface will be used if empty. Unlimited if 0 is passed."
},
"expiryDays": {
"type": "integer",
"description": "How many days the file will be stored. Original value from web interface will be used if empty. Unlimited if 0 is passed."
},
"password": {
"type": "string",
"description": "Password for this file to be set. No password will be used if empty."
},
"originalPassword": {
"type": "boolean",
"description": "Set to true to use original password. Field \"password\" will be ignored if set."
},
"filename": {
"type": "string",
"description": "Sets a new filename. Filename will be unchanged if empty."
}
}
},"chunking": {
},"chunking": {
"required": [
"file","uuid","filesize","offset"
],
@@ -910,41 +1323,6 @@
"description": "The chunk's offset starting at the beginning of the file"
}
}
},"chunkingcomplete": {
"required": [
"uuid","filename","filesize"
],
"type": "object",
"properties": {
"uuid": {
"type": "string",
"description": "The unique ID that was used for the uploaded chunks"
},
"filename": {
"type": "string",
"description": "The filename of the uploaded file"
},
"filesize": {
"type": "integer",
"description": "The total filesize of the uploaded file in bytes"
},
"contenttype": {
"type": "string",
"description": "The MIME content type. If empty, application/octet-stream will be used."
},
"allowedDownloads": {
"type": "integer",
"description": "How many downloads are allowed. Default of 1 will be used if empty. Unlimited if 0 is passed."
},
"expiryDays": {
"type": "integer",
"description": "How many days the file will be stored. Default of 14 will be used if empty. Unlimited if 0 is passed."
},
"password": {
"type": "string",
"description": "Password for this file to be set. No password will be used if empty"
}
}
}
},
"securitySchemes": {
File diff suppressed because one or more lines are too long
+52 -7
View File
@@ -179,19 +179,36 @@ a:hover {
pointer-events: auto;
}
.apiperm-granted {
.perm-granted {
cursor: pointer;
color: #19b90e;
color: #0edf00;
}
.apiperm-notgranted {
.perm-notgranted {
cursor: pointer;
color: #7e7e7e;
color: #9f9999;
}
.apiperm-processing {
.perm-unavailable {
color: #525252;
}
.perm-processing {
pointer-events: none;
color: #e5eb00;
}
.perm-nochange {
cursor: default;
}
.prevent-select {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
.gokapi-dialog {
background-color: #212529;
color: #ddd;
@@ -221,7 +238,7 @@ td.newItem {
}
@keyframes subtleHighlightNewApiKey {
@keyframes subtleHighlightNewJson {
0% {
background-color: green; /* Pale green for new items */
}
@@ -241,8 +258,36 @@ td.newItem {
}
.newApiKey {
animation: subtleHighlightNewApiKey 0.7s ease-out;
animation: subtleHighlightNewJson 0.7s ease-out;
}
.newUser {
animation: subtleHighlightNewJson 0.7s ease-out;
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.rowDeleting {
animation: fadeOut 0.3s ease-out forwards;
}
.highlighted-password {
background-color: #444; /* Dark gray background for subtle contrast */
color: #ddd; /* Light gray text */
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
font-family: monospace;
display: inline-block; /* Keeps the styling inline but ensures proper padding */
margin-left: 8px; /* Adds space between the label and the password */
border: 1px solid #555; /* Slight border to define the element */
}
@@ -1 +1 @@
.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastnotification.show{opacity:1;pointer-events:auto}.apiperm-granted{cursor:pointer;color:#19b90e}.apiperm-notgranted{cursor:pointer;color:#7e7e7e}.apiperm-processing{color:#e5eb00}.gokapi-dialog{background-color:#212529;color:#ddd}td.newItem{background-color:green}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:initial}}@keyframes subtleHighlightNewItem{0%{background-color:#a8e6a3}100%{background-color:green}}@keyframes subtleHighlightNewApiKey{0%{background-color:green}100%{background-color:initial}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.updatedDownloadCount.newItem{animation:subtleHighlightNewItem .5s ease-out}.newApiKey{animation:subtleHighlightNewApiKey .7s ease-out}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden}
.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastnotification.show{opacity:1;pointer-events:auto}.perm-granted{cursor:pointer;color:#0edf00}.perm-notgranted{cursor:pointer;color:#9f9999}.perm-unavailable{color:#525252}.perm-processing{pointer-events:none;color:#e5eb00}.perm-nochange{cursor:default}.prevent-select{-webkit-user-select:none;-ms-user-select:none;user-select:none}.gokapi-dialog{background-color:#212529;color:#ddd}td.newItem{background-color:green}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:initial}}@keyframes subtleHighlightNewItem{0%{background-color:#a8e6a3}100%{background-color:green}}@keyframes subtleHighlightNewJson{0%{background-color:green}100%{background-color:initial}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.updatedDownloadCount.newItem{animation:subtleHighlightNewItem .5s ease-out}.newApiKey{animation:subtleHighlightNewJson .7s ease-out}.newUser{animation:subtleHighlightNewJson .7s ease-out}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.rowDeleting{animation:fadeOut .3s ease-out forwards}.highlighted-password{background-color:#444;color:#ddd;padding:2px 6px;border-radius:4px;font-weight:700;font-family:monospace;display:inline-block;margin-left:8px;border:1px solid #555}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden}
+197 -6
View File
@@ -1,4 +1,8 @@
// API related
// This file contains JS code to connect to the API
// All files named admin_*.js will be merged together and minimised by calling
// go generate ./...
// /auth
async function apiAuthModify(apiKey, permission, modifier) {
const apiUrl = './api/auth/modify';
@@ -8,7 +12,7 @@ async function apiAuthModify(apiKey, permission, modifier) {
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'apiKeyToModify': apiKey,
'targetKey': apiKey,
'permission': permission,
'permissionModifier': modifier
@@ -35,7 +39,7 @@ async function apiAuthFriendlyName(apiKey, newName) {
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'apiKeyToModify': apiKey,
'targetKey': apiKey,
'friendlyName': newName
},
@@ -61,7 +65,7 @@ async function apiAuthDelete(apiKey) {
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'apiKeyToModify': apiKey,
'targetKey': apiKey,
},
};
@@ -105,7 +109,56 @@ async function apiAuthCreate() {
// File related
// /chunk
async function apiChunkComplete(uuid, filename, filesize, realsize, contenttype, allowedDownloads, expiryDays, password, isE2E, nonblocking) {
const apiUrl = './api/chunk/complete';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'uuid': uuid,
'filename': filename,
'filesize': filesize,
'realsize': realsize,
'contenttype': contenttype,
'allowedDownloads': allowedDownloads,
'expiryDays': expiryDays,
'password': password,
'isE2E': isE2E,
'nonblocking': nonblocking
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
let errorMessage;
// Attempt to parse JSON, fallback to text if parsing fails
try {
const errorResponse = await response.json();
errorMessage = errorResponse.ErrorMessage || `Request failed with status: ${response.status}`;
} catch {
// Handle non-JSON error
const errorText = await response.text();
errorMessage = errorText || `Request failed with status: ${response.status}`;
}
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error in apiChunkComplete:", error);
throw error;
}
}
// /files
async function apiFilesReplace(id, newId) {
@@ -202,7 +255,6 @@ async function apiFilesDelete(id) {
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
@@ -213,3 +265,142 @@ async function apiFilesDelete(id) {
throw error;
}
}
// users
async function apiUserCreate(userName) {
const apiUrl = './api/user/create';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'username': userName
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
if (response.status==409) {
throw new Error("duplicate");
}
throw new Error(`Request failed with status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error in apiUserModify:", error);
throw error;
}
}
async function apiUserModify(userId, permission, modifier) {
const apiUrl = './api/user/modify';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'userid': userId,
'userpermission': permission,
'permissionModifier': modifier
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error("Error in apiUserModify:", error);
throw error;
}
}
async function apiUserChangeRank(userId, newRank) {
const apiUrl = './api/user/changeRank';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'userid': userId,
'newRank': newRank
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error("Error in apiUserModify:", error);
throw error;
}
}
async function apiUserDelete(id, deleteFiles) {
const apiUrl = './api/user/delete';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'userid': id,
'deleteFiles': deleteFiles
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error("Error in apiUserDelete:", error);
throw error;
}
}
async function apiUserResetPassword(id, generatePw) {
const apiUrl = './api/user/resetPassword';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'userid': id,
'generateNewPassword': generatePw
},
};
try {
const response = await fetch(apiUrl, requestOptions);
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error in apiUserResetPassword:", error);
throw error;
}
}
@@ -0,0 +1,19 @@
// This file contains shared JS code for all admin views
// All files named admin_*.js will be merged together and minimised by calling
// go generate ./...
var clipboard = new ClipboardJS('.btn');
function showToast(timeout, text) {
let notification = document.getElementById("toastnotification");
if (typeof text !== 'undefined')
notification.innerText = text;
else
notification.innerText = notification.dataset.default;
notification.classList.add("show");
setTimeout(() => {
notification.classList.remove("show");
}, timeout);
}
@@ -3,16 +3,16 @@
// go generate ./...
function changeApiPermission(apiKey, permission, buttonId) {
function changeApiPermission(userId, permission, buttonId) {
var indicator = document.getElementById(buttonId);
if (indicator.classList.contains("apiperm-processing")) {
if (indicator.classList.contains("perm-processing") || indicator.classList.contains("perm-nochange")) {
return;
}
var wasGranted = indicator.classList.contains("apiperm-granted");
indicator.classList.add("apiperm-processing");
indicator.classList.remove("apiperm-granted");
indicator.classList.remove("apiperm-notgranted");
var wasGranted = indicator.classList.contains("perm-granted");
indicator.classList.add("perm-processing");
indicator.classList.remove("perm-granted");
indicator.classList.remove("perm-notgranted");
var modifier = "GRANT";
if (wasGranted) {
@@ -20,22 +20,22 @@ function changeApiPermission(apiKey, permission, buttonId) {
}
apiAuthModify(apiKey, permission, modifier)
apiAuthModify(userId, permission, modifier)
.then(data => {
if (wasGranted) {
indicator.classList.add("apiperm-notgranted");
indicator.classList.add("perm-notgranted");
} else {
indicator.classList.add("apiperm-granted");
indicator.classList.add("perm-granted");
}
indicator.classList.remove("apiperm-processing");
indicator.classList.remove("perm-processing");
})
.catch(error => {
if (wasGranted) {
indicator.classList.add("apiperm-granted");
indicator.classList.add("perm-granted");
} else {
indicator.classList.add("apiperm-notgranted");
indicator.classList.add("perm-notgranted");
}
indicator.classList.remove("apiperm-processing");
indicator.classList.remove("perm-processing");
alert("Unable to set permission: " + error);
console.error('Error:', error);
});
@@ -47,7 +47,10 @@ function deleteApiKey(apiKey) {
apiAuthDelete(apiKey)
.then(data => {
document.getElementById("row-" + apiKey).classList.add("rowDeleting");
setTimeout(() => {
document.getElementById("row-" + apiKey).remove();
}, 290);
})
.catch(error => {
alert("Unable to delete API key: " + error);
@@ -61,7 +64,7 @@ function newApiKey() {
document.getElementById("button-newapi").disabled = true;
apiAuthCreate()
.then(data => {
addRowApi(data.Id);
addRowApi(data.Id, data.PublicId);
document.getElementById("button-newapi").disabled = false;
})
.catch(error => {
@@ -89,6 +92,9 @@ function addFriendlyNameChange(apiKey) {
return;
allowEdit = false;
let newName = input.value;
if (newName == "") {
newName = "Unnamed key";
}
cell.innerHTML = newName;
cell.classList.remove("isBeingEdited");
@@ -116,41 +122,63 @@ function addFriendlyNameChange(apiKey) {
function addRowApi(apiKey) {
function addRowApi(apiKey, publicId) {
let table = document.getElementById("apitable");
let row = table.insertRow(0);
row.id = "row-" + apiKey;
let cellFriendlyName = row.insertCell(0);
let cellId = row.insertCell(1);
let cellLastUsed = row.insertCell(2);
let cellPermissions = row.insertCell(3);
let cellButtons = row.insertCell(4);
let cellEmpty = row.insertCell(5);
row.id = "row-" + publicId;
let cellCount = 0;
let cellFriendlyName = row.insertCell(cellCount++);
let cellId = row.insertCell(cellCount++);
let cellLastUsed = row.insertCell(cellCount++);
let cellPermissions = row.insertCell(cellCount++);
let cellUserName;
if (canViewOtherApiKeys) {
cellUserName= row.insertCell(cellCount++);
}
let cellButtons= row.insertCell(cellCount++);
if (canViewOtherApiKeys) {
cellUserName.classList.add("newApiKey");
cellUserName.innerText = userName;
}
cellFriendlyName.classList.add("newApiKey");
cellId.classList.add("newApiKey");
cellLastUsed.classList.add("newApiKey");
cellPermissions.classList.add("newApiKey");
cellPermissions.classList.add("prevent-select");
cellButtons.classList.add("newApiKey");
cellEmpty.classList.add("newApiKey");
cellFriendlyName.innerText = "Unnamed key";
cellFriendlyName.id = "friendlyname-" + apiKey;
cellFriendlyName.id = "friendlyname-" + publicId;
cellFriendlyName.onclick = function() {
addFriendlyNameChange(apiKey);
addFriendlyNameChange(publicId);
};
cellId.innerText = apiKey;
cellId.innerHTML = '<div class="font-monospace">'+apiKey+'</div>';
cellLastUsed.innerText = "Never";
cellButtons.innerHTML = '<button type="button" data-clipboard-text="' + apiKey + '" onclick="showToast()" title="Copy API Key" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i></button> <button id="delete-' + apiKey + '" type="button" class="btn btn-outline-danger btn-sm" onclick="deleteApiKey(\'' + apiKey + '\')" title="Delete"><i class="bi bi-trash3"></i></button>';
cellButtons.innerHTML = '<button type="button" data-clipboard-text="' + apiKey + '" onclick="showToast(1000)" title="Copy API Key" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i></button> <button id="delete-' + publicId + '" type="button" class="btn btn-outline-danger btn-sm" onclick="deleteApiKey(\'' + publicId + '\')" title="Delete"><i class="bi bi-trash3"></i></button>';
cellPermissions.innerHTML = `
<i id="perm_view_` + apiKey + `" class="bi bi-eye apiperm-granted" title="List Uploads" onclick='changeApiPermission("` + apiKey + `","PERM_VIEW", "perm_view_` + apiKey + `");'></i>
<i id="perm_upload_` + apiKey + `" class="bi bi-file-earmark-arrow-up apiperm-granted" title="Upload" onclick='changeApiPermission("` + apiKey + `","PERM_UPLOAD", "perm_upload_` + apiKey + `");'></i>
<i id="perm_edit_` + apiKey + `" class="bi bi-pencil apiperm-granted" title="Edit Uploads" onclick='changeApiPermission("` + apiKey + `","PERM_EDIT", "perm_edit_` + apiKey + `");'></i>
<i id="perm_delete_` + apiKey + `" class="bi bi-trash3 apiperm-granted" title="Delete Uploads" onclick='changeApiPermission("` + apiKey + `","PERM_DELETE", "perm_delete_` + apiKey + `");'></i>
<i id="perm_replace_` + apiKey + `" class="bi bi-recycle apiperm-notgranted" title="Replace Uploads" onclick='changeApiPermission("` + apiKey + `","PERM_REPLACE", "perm_replace_` + apiKey + `");'></i>
<i id="perm_api_` + apiKey + `" class="bi bi-sliders2 apiperm-notgranted" title="Manage API Keys" onclick='changeApiPermission("` + apiKey + `","PERM_API_MOD", "perm_api_` + apiKey + `");'></i>`;
<i id="perm_view_` + publicId + `" class="bi bi-eye perm-granted" title="List Uploads" onclick='changeApiPermission("` + publicId + `","PERM_VIEW", "perm_view_` + publicId + `");'></i>
<i id="perm_upload_` + publicId + `" class="bi bi-file-earmark-arrow-up perm-granted" title="Upload" onclick='changeApiPermission("` + publicId + `","PERM_UPLOAD", "perm_upload_` + publicId + `");'></i>
<i id="perm_edit_` + publicId + `" class="bi bi-pencil perm-granted" title="Edit Uploads" onclick='changeApiPermission("` + publicId + `","PERM_EDIT", "perm_edit_` + publicId + `");'></i>
<i id="perm_delete_` + publicId + `" class="bi bi-trash3 perm-granted" title="Delete Uploads" onclick='changeApiPermission("` + publicId + `","PERM_DELETE", "perm_delete_` + publicId + `");'></i>
<i id="perm_replace_` + publicId + `" class="bi bi-recycle perm-notgranted" title="Replace Uploads" onclick='changeApiPermission("` + publicId + `","PERM_REPLACE", "perm_replace_` + publicId + `");'></i>
<i id="perm_users_` + publicId + `" class="bi bi-people perm-notgranted" title="Manage Users" onclick='changeApiPermission("` + publicId + `", "PERM_MANAGE_USERS", "` + publicId + `");'></i>
<i id="perm_api_` + publicId + `" class="bi bi-sliders2 perm-notgranted" title="Manage API Keys" onclick='changeApiPermission("` + publicId + `","PERM_API_MOD", "perm_api_` + publicId + `");'></i>`;
if (!canReplaceFiles) {
let cell = document.getElementById("perm_replace_"+publicId);
cell.classList.add("perm-unavailable");
cell.classList.add("perm-nochange");
}
if (!canManageUsers) {
let cell = document.getElementById("perm_users_"+publicId);
cell.classList.add("perm-unavailable");
cell.classList.add("perm-nochange");
}
setTimeout(() => {
cellFriendlyName.classList.remove("newApiKey");
@@ -158,7 +186,6 @@ function addRowApi(apiKey) {
cellLastUsed.classList.remove("newApiKey");
cellPermissions.classList.remove("newApiKey");
cellButtons.classList.remove("newApiKey");
cellEmpty.classList.remove("newApiKey");
}, 700);
}
@@ -2,7 +2,6 @@
// All files named admin_*.js will be merged together and minimised by calling
// go generate ./...
var clipboard = new ClipboardJS('.btn');
var dropzoneObject;
var isE2EEnabled = false;
@@ -182,44 +181,43 @@ function urlencodeFormData(fd) {
return s;
}
function sendChunkComplete(file, done) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "./uploadComplete", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
let formData = new FormData();
formData.append("allowedDownloads", document.getElementById("allowedDownloads").value);
formData.append("expiryDays", document.getElementById("expiryDays").value);
formData.append("password", document.getElementById("password").value);
formData.append("isUnlimitedDownload", !document.getElementById("enableDownloadLimit").checked);
formData.append("isUnlimitedTime", !document.getElementById("enableTimeLimit").checked);
formData.append("chunkid", file.upload.uuid);
if (file.isEndToEndEncrypted === true) {
formData.append("filesize", file.sizeEncrypted);
formData.append("filename", "Encrypted File");
formData.append("filecontenttype", "");
formData.append("isE2E", "true");
formData.append("realSize", file.size);
} else {
formData.append("filesize", file.size);
formData.append("filename", file.name);
formData.append("filecontenttype", file.type);
let uuid = file.upload.uuid;
let filename = file.name;
let filesize = file.size;
let realsize = file.size;
let contenttype = file.type;
let allowedDownloads = document.getElementById("allowedDownloads").value;
let expiryDays = document.getElementById("expiryDays").value;
let password = document.getElementById("password").value;
let isE2E = file.isEndToEndEncrypted === true;
let nonblocking = true;
if (!document.getElementById("enableDownloadLimit").checked) {
allowedDownloads = 0;
}
xhr.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
done();
if (!document.getElementById("enableTimeLimit").checked) {
expiryDays = 0;
}
if (isE2E) {
filesize = file.sizeEncrypted;
filename = "Encrypted File";
contenttype = "";
}
apiChunkComplete(uuid, filename, filesize, realsize, contenttype, allowedDownloads, expiryDays, password, isE2E, nonblocking)
.then(data => {
done();
let progressText = document.getElementById(`us-progress-info-${file.upload.uuid}`);
if (progressText != null)
progressText.innerText = "In Queue...";
} else {
dropzoneUploadError(file, getErrorMessage(xhr.responseText));
}
}
};
xhr.send(urlencodeFormData(formData));
})
.catch(error => {
console.error('Error:', error);
dropzoneUploadError(file, error);
});
}
function dropzoneUploadError(file, errormessage) {
@@ -228,16 +226,6 @@ function dropzoneUploadError(file, errormessage) {
showError(file, errormessage);
}
function getErrorMessage(response) {
let result;
try {
result = JSON.parse(response);
} catch (e) {
return "Unknown error: Server could not process file";
}
return "Error: " + result.ErrorMessage;
}
function dropzoneGetFile(uid) {
for (let i = 0; i < dropzoneObject.files.length; i++) {
const currentFile = dropzoneObject.files[i];
@@ -354,8 +342,8 @@ function editFile() {
let replaceFile = false;
let replaceId = "";
if (document.getElementById('mc_replace').checked) {
replaceFile = true;
replaceId = document.getElementById('mi_edit_replace').value;
replaceFile = (replaceId != "");
}
apiFilesModify(id, allowedDownloads, expiryTimestamp, password, originalPassword)
@@ -422,7 +410,7 @@ function handleEditCheckboxChange(checkbox) {
}
function showEditModal(filename, id, downloads, expiry, password, unlimitedown, unlimitedtime, isE2e) {
function showEditModal(filename, id, downloads, expiry, password, unlimitedown, unlimitedtime, isE2e, canReplace) {
// Cloning removes any previous values or form validation
let originalModal = $('#modaledit').clone();
$("#modaledit").on('hide.bs.modal', function() {
@@ -430,6 +418,7 @@ function showEditModal(filename, id, downloads, expiry, password, unlimitedown,
let myClone = originalModal.clone();
$('body').append(myClone);
});
document.getElementById("m_filenamelabel").innerHTML = filename;
document.getElementById("mc_expiry").setAttribute("data-timestamp", expiry);
document.getElementById("mb_save").setAttribute('data-fileid', id);
@@ -468,23 +457,29 @@ function showEditModal(filename, id, downloads, expiry, password, unlimitedown,
}
let selectReplace = document.getElementById("mi_edit_replace");
if (!isE2e) {
let files = getAllAvailableFiles();
for (let i = 0; i < files[0].length; i++) {
if (files[0][i] == id)
continue;
selectReplace.add(new Option(files[1][i] + " (" + files[0][i] + ")", files[0][i]));
if (canReplace) {
document.getElementById("replaceGroup").style.display = 'flex';
if (!isE2e) {
let files = getAllAvailableFiles();
for (let i = 0; i < files[0].length; i++) {
if (files[0][i] == id)
continue;
selectReplace.add(new Option(files[1][i] + " (" + files[0][i] + ")", files[0][i]));
}
} else {
document.getElementById("mc_replace").disabled = true;
document.getElementById("mc_replace").title = "Replacing content is not available for end-to-end encrypted files";
selectReplace.add(new Option("Unavailable", 0));
selectReplace.title = "Replacing content is not available for end-to-end encrypted files";
selectReplace.value = "0";
}
} else {
document.getElementById("mc_replace").disabled = true;
document.getElementById("mc_replace").title = "Replacing content is not available for end-to-end encrypted files";
selectReplace.add(new Option("Unavailable", 0));
selectReplace.title = "Replacing content is not available for end-to-end encrypted files";
selectReplace.value = "0";
document.getElementById("replaceGroup").style.display = 'none';
}
$('#modaledit').modal('show');
new bootstrap.Modal('#modaledit', {}).show();
}
function selectTextForPw(input) {
@@ -699,14 +694,14 @@ function addRow(item) {
cellDownloadCount.innerHTML = '0';
cellUrl.innerHTML = '<a target="_blank" style="color: inherit" id="url-href-' + item.Id + '" href="' + item.UrlDownload + '">' + item.Id + '</a>' + lockIcon;
let buttons = '<button type="button" onclick="showToast()" id="url-button-' + item.Id + '" data-clipboard-text="' + item.UrlDownload + '" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> URL</button> ';
let buttons = '<button type="button" onclick="showToast(1000)" id="url-button-' + item.Id + '" data-clipboard-text="' + item.UrlDownload + '" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> URL</button> ';
if (item.UrlHotlink === "") {
buttons = buttons + '<button type="button"class="copyurl btn btn-outline-light btn-sm disabled"><i class="bi bi-copy"></i> Hotlink</button> ';
} else {
buttons = buttons + '<button type="button" onclick="showToast()" data-clipboard-text="' + item.UrlHotlink + '" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> Hotlink</button> ';
buttons = buttons + '<button type="button" onclick="showToast(1000)" data-clipboard-text="' + item.UrlHotlink + '" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> Hotlink</button> ';
}
buttons = buttons + '<button type="button" id="qrcode-' + item.Id + '" title="QR Code" class="btn btn-outline-light btn-sm" onclick="showQrCode(\'' + item.UrlDownload + '\');"><i class="bi bi-qr-code"></i></button> ';
buttons = buttons + '<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal(\'' + item.Name + '\',\'' + item.Id + '\', ' + item.DownloadsRemaining + ', ' + item.ExpireAt + ', ' + item.IsPasswordProtected + ', ' + item.UnlimitedDownloads + ', ' + item.UnlimitedTime + ', ' + item.IsEndToEndEncrypted + ');"><i class="bi bi-pencil"></i></button> ';
buttons = buttons + '<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal(\'' + item.Name + '\',\'' + item.Id + '\', ' + item.DownloadsRemaining + ', ' + item.ExpireAt + ', ' + item.IsPasswordProtected + ', ' + item.UnlimitedDownloads + ', ' + item.UnlimitedTime + ', ' + item.IsEndToEndEncrypted + ', canReplaceOwnFiles);"><i class="bi bi-pencil"></i></button> ';
buttons = buttons + '<button type="button" id="button-delete-' + item.Id + '" title="Delete" class="btn btn-outline-danger btn-sm" onclick="deleteFile(\'' + item.Id + '\')"><i class="bi bi-trash3"></i></button>';
cellButtons.innerHTML = buttons;
@@ -756,11 +751,3 @@ function showQrCode(url) {
});
overlay.addEventListener("click", hideQrCode);
}
function showToast() {
let notification = document.getElementById("toastnotification");
notification.classList.add("show");
setTimeout(() => {
notification.classList.remove("show");
}, 1000);
}
@@ -0,0 +1,251 @@
// This file contains JS code for the User view
// All files named admin_*.js will be merged together and minimised by calling
// go generate ./...
function changeUserPermission(userId, permission, buttonId) {
let indicator = document.getElementById(buttonId);
if (indicator.classList.contains("perm-processing") || indicator.classList.contains("perm-nochange")) {
return;
}
let wasGranted = indicator.classList.contains("perm-granted");
indicator.classList.add("perm-processing");
indicator.classList.remove("perm-granted");
indicator.classList.remove("perm-notgranted");
let modifier = "GRANT";
if (wasGranted) {
modifier = "REVOKE";
}
if (permission == "PERM_REPLACE_OTHER" && !wasGranted) {
hasNotPermissionReplace = document.getElementById("perm_replace_" + userId).classList.contains("perm-notgranted");
if (hasNotPermissionReplace) {
showToast(2000, "Also granting permission to replace own files");
changeUserPermission(userId, "PERM_REPLACE", "perm_replace_" + userId);
}
}
if (permission == "PERM_REPLACE" && wasGranted) {
hasPermissionReplaceOthers = document.getElementById("perm_replace_other_" + userId).classList.contains("perm-granted");
if (hasPermissionReplaceOthers) {
showToast(2000, "Also revoking permission to replace files of other users");
changeUserPermission(userId, "PERM_REPLACE_OTHER", "perm_replace_other_" + userId);
}
}
apiUserModify(userId, permission, modifier)
.then(data => {
if (wasGranted) {
indicator.classList.add("perm-notgranted");
} else {
indicator.classList.add("perm-granted");
}
indicator.classList.remove("perm-processing");
})
.catch(error => {
if (wasGranted) {
indicator.classList.add("perm-granted");
} else {
indicator.classList.add("perm-notgranted");
}
indicator.classList.remove("perm-processing");
alert("Unable to set permission: " + error);
console.error('Error:', error);
});
}
function changeRank(userId, newRank, buttonId) {
let indicator = document.getElementById(buttonId);
if (indicator.disabled) {
return;
}
indicator.disabled = true;
apiUserChangeRank(userId, newRank)
.then(data => {
location.reload();
})
.catch(error => {
indicator.disabled = false;
alert("Unable to change rank: " + error);
console.error('Error:', error);
});
}
function showDeleteModal(userId, userEmail) {
let checkboxDelete = document.getElementById("checkboxDelete");
checkboxDelete.checked = false;
document.getElementById("deleteModalBody").innerText = userEmail;
$('#deleteModal').modal('show');
document.getElementById("buttonDelete").onclick = function() {
apiUserDelete(userId, checkboxDelete.checked)
.then(data => {
$('#deleteModal').modal('hide');
document.getElementById("row-" + userId).classList.add("rowDeleting");
setTimeout(() => {
document.getElementById("row-" + userId).remove();
}, 290);
})
.catch(error => {
alert("Unable to delete user: " + error);
console.error('Error:', error);
});
};
}
function showAddUserModal() {
// Cloning removes any previous values or form validation
let originalModal = $('#newUserModal').clone();
$("#newUserModal").on('hide.bs.modal', function() {
$('#newUserModal').remove();
let myClone = originalModal.clone();
$('body').append(myClone);
});
$('#newUserModal').modal('show');
}
function showResetPwModal(userid, name) {
// Cloning removes any previous values or form validation
let originalModal = $('#resetPasswordModal').clone();
$("#resetPasswordModal").on('hide.bs.modal', function() {
$('#resetPasswordModal').remove();
let myClone = originalModal.clone();
$('body').append(myClone);
});
document.getElementById("l_userpwreset").innerText = name;
let button = document.getElementById("resetPasswordButton");
button.onclick = function() {
resetPw(userid, document.getElementById("generateRandomPassword").checked);
};
$('#resetPasswordModal').modal('show');
}
function resetPw(userid, newPw) {
let button = document.getElementById("resetPasswordButton");
document.getElementById("resetPasswordButton").disabled = true;
apiUserResetPassword(userid, newPw)
.then(data => {
if (!newPw) {
$('#resetPasswordModal').modal('hide');
showToast(1000, 'Password change requirement set successfully')
return;
}
button.style.display = 'none';
document.getElementById("cancelPasswordButton").style.display = 'none';
document.getElementById("formentryReset").style.display = 'none';
document.getElementById("randomPasswordContainer").style.display = 'block';
document.getElementById("closeModalResetPw").style.display = 'block';
document.getElementById("l_returnedPw").innerText = data.password;
document.getElementById("copypwclip").onclick = function() {
// For some reason ClipboardJs is not working on the user PW reset modal, even when initilising again. Manually writing to clipboard
navigator.clipboard.writeText(data.password);
showToast(1000, "Password copied to clipboard");
}
})
.catch(error => {
alert("Unable to reset user password: " + error);
console.error('Error:', error);
button.disabled = false;
});
}
function addNewUser() {
let button = document.getElementById("mb_addUser");
button.disabled = true;
let form = document.getElementById('newUserForm');
if (!form.checkValidity()) {
form.classList.add('was-validated');
button.disabled = false;
} else {
let editName = document.getElementById("e_userName");
apiUserCreate(editName.value.trim())
.then(data => {
$('#newUserModal').modal('hide');
addRowUser(data.id, data.name);
})
.catch(error => {
if (error.message == "duplicate") {
alert("A user already exists with that name");
button.disabled = false;
} else {
alert("Unable to create user: " + error);
console.error('Error:', error);
button.disabled = false;
}
});
}
}
function addRowUser(userid, name) {
let table = document.getElementById("usertable");
let row = table.insertRow(1);
row.id = "row-" + userid;
let cellName = row.insertCell(0);
let cellGroup = row.insertCell(1);
let cellLastOnline = row.insertCell(2);
let cellUploads = row.insertCell(3);
let cellPermissions = row.insertCell(4);
let cellActions = row.insertCell(5);
cellName.classList.add("newUser");
cellGroup.classList.add("newUser");
cellLastOnline.classList.add("newUser");
cellUploads.classList.add("newUser");
cellPermissions.classList.add("newUser");
cellActions.classList.add("newUser");
cellName.innerText = name;
cellGroup.innerText = "User";
cellLastOnline.innerText = "Never";
cellUploads.innerText = "0";
let buttonResetPw = '<button id="pwchange-' + userid + '" type="button" class="btn btn-outline-light btn-sm" onclick="showResetPwModal(\'' + userid + '\', \'' + name + '\')" title="Reset Password"><i class="bi bi-key-fill"></i></button>&nbsp; ';
cellActions.innerHTML = '<button id="changeRank_' + userid + '" type="button" onclick="changeRank( ' + userid + ' , \'ADMIN\', \'changeRank_' + userid + '\')" title="Promote User" class="btn btn-outline-light btn-sm"><i class="bi bi-chevron-double-up"></i></button>&nbsp; <button id="delete-' + userid + '" type="button" class="btn btn-outline-danger btn-sm" onclick="showDeleteModal(' + userid + ', \'' + name + '\')" title="Delete"><i class="bi bi-trash3"></i></button>';
if (isInternalAuth) {
cellActions.innerHTML = buttonResetPw+cellActions.innerHTML;
}
cellPermissions.innerHTML = `
<i id="perm_replace_` + userid + `" class="bi bi-recycle perm-notgranted " title="Replace own uploads" onclick='changeUserPermission(` + userid + `,"PERM_REPLACE", "perm_replace_` + userid + `");'></i>
<i id="perm_list_` + userid + `" class="bi bi-eye perm-notgranted " title="List other uploads" onclick='changeUserPermission(` + userid + `,"PERM_LIST", "perm_list_` + userid + `");'></i>
<i id="perm_edit_` + userid + `" class="bi bi-pencil perm-notgranted " title="Edit other uploads" onclick='changeUserPermission(` + userid + `,"PERM_EDIT", "perm_edit_` + userid + `");'></i>
<i id="perm_delete_` + userid + `" class="bi bi-trash3 perm-notgranted " title="Delete other uploads" onclick='changeUserPermission(` + userid + `,"PERM_DELETE", "perm_delete_` + userid + `");'></i>
<i id="perm_replace_other_` + userid + `" class="bi bi-arrow-left-right perm-notgranted " title="Replace other uploads" onclick='changeUserPermission(` + userid + `,"PERM_REPLACE_OTHER", "perm_replace_other_` + userid + `");'></i>
<i id="perm_logs_` + userid + `" class="bi bi-card-list perm-notgranted " title="Manage system logs" onclick='changeUserPermission(` + userid + `,"PERM_LOGS", "perm_logs_` + userid + `");'></i>
<i id="perm_users_` + userid + `" class="bi bi-people perm-notgranted " title="Manage users" onclick='changeUserPermission(` + userid + `,"PERM_USERS", "perm_users_` + userid + `");'></i>
<i id="perm_api_` + userid + `" class="bi bi-sliders2 perm-notgranted " title="Manage API keys" onclick='changeUserPermission(` + userid + `,"PERM_API", "perm_api_` + userid + `");'></i>`;
setTimeout(() => {
cellName.classList.remove("newUser");
cellGroup.classList.remove("newUser");
cellLastOnline.classList.remove("newUser");
cellUploads.classList.remove("newUser");
cellPermissions.classList.remove("newUser");
cellActions.classList.remove("newUser");
}, 700);
}
File diff suppressed because one or more lines are too long
@@ -79,15 +79,15 @@
{{ end }}
<td id="cell-downloads-{{ .Id }}">{{ .DownloadCount }}</td>
<td><a id="url-href-{{ .Id }}" target="_blank" href="{{ .UrlDownload }}">{{ .Id }}</a>{{ if .IsPasswordProtected }} <i title="Password protected" class="bi bi-key"></i>{{ end }}</td>
<td><button id="url-button-{{ .Id }}" type="button" onclick="showToast()" data-clipboard-text="{{ .UrlDownload }}" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> URL</button>
<td><button id="url-button-{{ .Id }}" type="button" onclick="showToast(1000)" data-clipboard-text="{{ .UrlDownload }}" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> URL</button>
{{ if ne .UrlHotlink "" }}
<button type="button" onclick="showToast()" data-clipboard-text="{{ .UrlHotlink }}" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> Hotlink</button>
<button type="button" onclick="showToast(1000)" data-clipboard-text="{{ .UrlHotlink }}" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> Hotlink</button>
{{ else }}
<button type="button"class="copyurl btn btn-outline-light btn-sm disabled"><i class="bi bi-copy"></i> Hotlink</button>
{{ end }}
<button type="button" id="qrcode-{{ .Id }}" title="QR Code" class="btn btn-outline-light btn-sm" onclick="showQrCode('{{ .UrlDownload }}');"><i class="bi bi-qr-code"></i></button>
<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal('{{.Name }}','{{.Id}}', {{.DownloadsRemaining }}, {{.ExpireAt }}, {{.IsPasswordProtected}}, {{.UnlimitedDownloads }}, {{.UnlimitedTime}}, {{.IsEndToEndEncrypted}});"><i class="bi bi-pencil"></i></button>
<button id="button-delete-{{ .Id }}" type="button" title="Delete" class="btn btn-outline-danger btn-sm" onclick="deleteFile('{{ .Id }}')"><i class="bi bi-trash3"></i></button></td>
{{ template "admin_button_edit" (newAdminButtonContext . $.ActiveUser) }}
{{ template "admin_button_delete" (newAdminButtonContext . $.ActiveUser) }}</td>
</tr>
{{ end }}
{{ end }}
@@ -100,57 +100,10 @@
</div>
</div>
<!-- Modal for editing-->
<div class="modal fade" id="modaledit" tabindex="-1" aria-labelledby="m_filenamelabel" aria-hidden="true">
<div class="modal-dialog modal-lg gokapi-dialog">
<div class="modal-content gokapi-dialog">
<div class="modal-header">
<h1 class="modal-title fs-5" id="m_filenamelabel">Filename</h1>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<div class="input-group-text">
<input type="checkbox" id="mc_download" checked aria-label="Limit downloads" title="Limit downloads" data-toggle-target="mi_edit_down" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_down">Limit Downloads</span>
<input type="number" min="1" id="mi_edit_down" class="form-control" aria-label="Downloads Remaining" aria-describedby="edit_down">
</div>
<div class="input-group mb-3">
<div class="input-group-text">
<input id="mc_expiry" type="checkbox" checked aria-label="Expire files" title="Expire files" data-toggle-target="mi_edit_expiry" data-timestamp="" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_expdate">Expiry&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<input type="text" id="mi_edit_expiry" class="form-control" aria-label="Expiry" aria-describedby="edit_expdate">
</div>
<div class="input-group mb-3">
<div class="input-group-text">
<input type="checkbox" id="mc_password" aria-label="Require password" title="Require password" data-toggle-target="mi_edit_pw" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_pw">Password&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<input type="text" id="mi_edit_pw" class="form-control" aria-label="Password" disabled onclick="selectTextForPw(this)" aria-describedby="edit_pw">
</div>
<div class="input-group mb-3">
<div class="input-group-text">
<input type="checkbox" id="mc_replace" aria-label="Replace file content" title="Replace file content" data-toggle-target="mi_edit_replace" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_replace">Replace Content&nbsp;&nbsp;</span>
<select id="mi_edit_replace" class="form-select" aria-label="Replace File Content" disabled>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-light" aria-label="Close" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-fileid="" id="mb_save" onclick="editFile();">Save changes</button>
</div>
</div>
</div>
</div>
{{ template "admin_modal_edit" }}
<div id="toastnotification" class="toastnotification">URL copied to clipboard</div>
<div id="toastnotification" class="toastnotification" data-default="URL copied to clipboard">Toast Text</div>
<div id="qroverlay">
<div id="qrcode"></div>
</div>
@@ -192,6 +145,7 @@
registerChangeHandler();
var systemKey = "{{.SystemKey}}";
var canReplaceOwnFiles = {{.ActiveUser.HasPermissionReplace}};
setUploadDefaults();
@@ -220,3 +174,87 @@
{{ template "footer" true}}
{{ end }}
{{ define "admin_button_edit" }}
<button
type="button"
title="Edit"
{{ if and (ne .ActiveUser.Id .CurrentFile.UploaderId) (not .ActiveUser.HasPermissionEditOtherUploads) }}
disabled
{{ end }}
class="btn btn-outline-light btn-sm"
onclick="showEditModal('{{.CurrentFile.Name }}',
'{{.CurrentFile.Id}}', {{.CurrentFile.DownloadsRemaining }},
{{.CurrentFile.ExpireAt }}, {{.CurrentFile.IsPasswordProtected}},
{{.CurrentFile.UnlimitedDownloads }}, {{.CurrentFile.UnlimitedTime}},
{{.CurrentFile.IsEndToEndEncrypted}},
{{ or (and (eq .ActiveUser.Id .CurrentFile.UploaderId) (.ActiveUser.HasPermissionReplace))
(.ActiveUser.HasPermissionReplaceOtherUploads) }});">
<i class="bi bi-pencil"></i>
</button>
{{ end }}
{{ define "admin_button_delete" }}
<button id="button-delete-{{ .CurrentFile.Id}}"
type="button"
title="Delete"
{{ if and (ne .ActiveUser.Id .CurrentFile.UploaderId) (not .ActiveUser.HasPermissionDeleteOtherUploads) }}
disabled
{{ end }}
class="btn btn-outline-danger btn-sm"
onclick="deleteFile('{{ .CurrentFile.Id }}')">
<i class="bi bi-trash3"></i>
</button>
{{ end }}
{{ define "admin_modal_edit" }}
<div class="modal fade" id="modaledit" tabindex="-1" aria-labelledby="m_filenamelabel" aria-hidden="true">
<div class="modal-dialog modal-lg gokapi-dialog">
<div class="modal-content gokapi-dialog">
<div class="modal-header">
<h1 class="modal-title fs-5" id="m_filenamelabel">Filename</h1>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<div class="input-group-text">
<input type="checkbox" id="mc_download" checked aria-label="Limit downloads" title="Limit downloads" data-toggle-target="mi_edit_down" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_down">Limit Downloads</span>
<input type="number" min="1" id="mi_edit_down" class="form-control" aria-label="Downloads Remaining" aria-describedby="edit_down">
</div>
<div class="input-group mb-3">
<div class="input-group-text">
<input id="mc_expiry" type="checkbox" checked aria-label="Expire files" title="Expire files" data-toggle-target="mi_edit_expiry" data-timestamp="" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_expdate">Expiry&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<input type="text" id="mi_edit_expiry" class="form-control" aria-label="Expiry" aria-describedby="edit_expdate">
</div>
<div class="input-group mb-3">
<div class="input-group-text">
<input type="checkbox" id="mc_password" aria-label="Require password" title="Require password" data-toggle-target="mi_edit_pw" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_pw">Password&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
<input type="text" id="mi_edit_pw" class="form-control" aria-label="Password" disabled onclick="selectTextForPw(this)" aria-describedby="edit_pw">
</div>
<div class="input-group mb-3" id="replaceGroup">
<div class="input-group-text">
<input type="checkbox" id="mc_replace" aria-label="Replace file content" title="Replace file content" data-toggle-target="mi_edit_replace" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="edit_replace">Replace Content&nbsp;&nbsp;</span>
<select id="mi_edit_replace" class="form-select" aria-label="Replace File Content" disabled>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-light" aria-label="Close" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-fileid="" id="mb_save" onclick="editFile();">Save changes</button>
</div>
</div>
</div>
</div>
{{ end }}
+42 -20
View File
@@ -4,8 +4,21 @@
<div class="col">
<div id="container" class="card" style="width: 80%">
<div class="card-body">
<h3 class="card-title">API Keys</h3>
<br>
<div class="container">
<div class="row mb-4">
<div class="col">
</div>
<div class="col text-center">
<h3 class="card-title mb-0">API Keys</h3>
</div>
<div class="col text-end">
<button id="button-newapi" class="btn btn-outline-light" onclick="newApiKey()">
<i class="bi bi-plus-circle-fill"></i>
</button>
</div>
</div>
</div>
Please visit the <a target="_blank" href="./apidocumentation">API documentation</a> for more information about the API.<br>Click on the API key name to give it a new name. Permissions can be changed by clicking on them.
<br>
<br>
@@ -17,42 +30,51 @@
<th scope="col">API Key</th>
<th scope="col">Last Used</th>
<th scope="col">Permissions</th>
{{ if .ActiveUser.HasPermissionManageApi }}
<th scope="col">User</th>
{{ end }}
<th scope="col">Actions</th>
<th scope="col"><button id="button-newapi" type="button" class="btn btn-outline-light btn-sm" onclick="newApiKey()"><i class="bi bi-plus-circle-fill"></i> New Key</button></th>
</tr>
</thead>
<tbody id="apitable">
{{ range .ApiKeys }}
{{ if not .IsSystemKey }}
<tr id="row-{{ .Id }}">
<td scope="col" id="friendlyname-{{ .Id }}" onClick="addFriendlyNameChange('{{ .Id }}')">{{ .FriendlyName }}</td>
<td scope="col">{{ .Id }}</td>
<td scope="col">{{ .GetReadableDate }}</td>
<td scope="col">
<i id="perm_view_{{ .Id }}" class="bi bi-eye {{if not .HasPermissionView}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="List Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_VIEW", "perm_view_{{ .Id }}");'></i>
<i id="perm_upload_{{ .Id }}" class="bi bi-file-earmark-arrow-up {{if not .HasPermissionUpload}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Upload" onclick='changeApiPermission("{{ .Id }}","PERM_UPLOAD", "perm_upload_{{ .Id }}");'></i>
<i id="perm_edit_{{ .Id }}" class="bi bi-pencil {{if not .HasPermissionEdit}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Edit Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_EDIT", "perm_edit_{{ .Id }}");'></i>
<i id="perm_delete_{{ .Id }}" class="bi bi-trash3 {{if not .HasPermissionDelete}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Delete Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_DELETE", "perm_delete_{{ .Id }}");'></i>
<i id="perm_replace_{{ .Id }}" class="bi bi-recycle {{if not .HasPermissionReplace}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Replace Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_REPLACE", "perm_replace_{{ .Id }}");'></i>
<i id="perm_api_{{ .Id }}" class="bi bi-sliders2 {{if not .HasPermissionApiMod}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Manage API Keys" onclick='changeApiPermission("{{ .Id }}","PERM_API_MOD", "perm_api_{{ .Id }}");'></i>
<tr id="row-{{ .PublicId }}">
<td id="friendlyname-{{ .PublicId }}" onClick="addFriendlyNameChange('{{ .PublicId }}')">{{ .FriendlyName }}</td>
<td><div class="font-monospace">{{ .GetRedactedId }}</div></td>
<td>{{ .GetReadableDate }}</td>
<td class="prevent-select">
<i id="perm_view_{{ .PublicId }}" class="bi bi-eye {{if not .HasPermissionView}}perm-notgranted{{else}}perm-granted{{end}}" title="List Uploads" onclick='changeApiPermission("{{ .PublicId }}","PERM_VIEW", "perm_view_{{ .PublicId }}");'></i>
<i id="perm_upload_{{ .PublicId }}" class="bi bi-file-earmark-arrow-up {{if not .HasPermissionUpload}}perm-notgranted{{else}}perm-granted{{end}}" title="Upload" onclick='changeApiPermission("{{ .PublicId }}","PERM_UPLOAD", "perm_upload_{{ .PublicId }}");'></i>
<i id="perm_edit_{{ .PublicId }}" class="bi bi-pencil {{if not .HasPermissionEdit}}perm-notgranted{{else}}perm-granted{{end}}" title="Edit Uploads" onclick='changeApiPermission("{{ .PublicId }}","PERM_EDIT", "perm_edit_{{ .PublicId }}");'></i>
<i id="perm_delete_{{ .PublicId }}" class="bi bi-trash3 {{if not .HasPermissionDelete}}perm-notgranted{{else}}perm-granted{{end}}" title="Delete Uploads" onclick='changeApiPermission("{{ .PublicId }}","PERM_DELETE", "perm_delete_{{ .PublicId }}");'></i>
<i id="perm_replace_{{ .PublicId }}" class="bi bi-recycle {{if not (index $.UserMap .UserId).HasPermissionReplace}}perm-unavailable perm-nochange{{ else }}{{if not .HasPermissionReplace}}perm-notgranted{{else}}perm-granted{{end}}{{end}}" title="Replace Uploads" onclick='changeApiPermission("{{ .PublicId }}","PERM_REPLACE", "perm_replace_{{ .PublicId }}");'></i>
<i id="perm_users_{{ .PublicId }}" class="bi bi-people {{if not (index $.UserMap .UserId).HasPermissionManageUsers}}perm-unavailable perm-nochange{{ else }}{{if not .HasPermissionManageUsers}}perm-notgranted{{else}}perm-granted{{end}}{{end}}" title="Manage Users" onclick='changeApiPermission("{{ .PublicId }}","PERM_MANAGE_USERS", "perm_users_{{ .PublicId }}");'></i>
<i id="perm_api_{{ .PublicId }}" class="bi bi-sliders2 {{if not .HasPermissionApiMod}}perm-notgranted{{else}}perm-granted{{end}}" title="Manage API Keys" onclick='changeApiPermission("{{ .PublicId }}","PERM_API_MOD", "perm_api_{{ .PublicId }}");'></i>
</td>
<td scope="col"><button type="button" data-clipboard-text="{{ .Id }}" onclick="showToast()" title="Copy API Key" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i></button> <button id="delete-{{ .Id }}" type="button" class="btn btn-outline-danger btn-sm" onclick="deleteApiKey('{{ .Id }}')" title="Delete"><i class="bi bi-trash3"></i></button></td>
<td scope="col"></td>
</tr>
{{ if $.ActiveUser.HasPermissionManageApi }}
<td>{{(index $.UserMap .UserId).Name}}</td>
{{ end }}
<td><button id="delete-{{ .PublicId }}" type="button" class="btn btn-outline-danger btn-sm" onclick="deleteApiKey('{{ .PublicId }}')" title="Delete"><i class="bi bi-trash3"></i></button></td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div id="toastnotification" class="toastnotification">API key copied to clipboard</div>
<div id="toastnotification" class="toastnotification" data-default="API key copied to clipboard">Toast Text</div>
</div>
</div>
<script src="./js/min/admin.min.{{ template "js_admin_version"}}.js"></script>
<script>
var systemKey = "{{.SystemKey}}";
var userName = "{{.ActiveUser.Name}}";
var canViewOtherApiKeys = {{.ActiveUser.HasPermissionManageApi }};
var canReplaceFiles = {{.ActiveUser.HasPermissionManageApi }};
var canManageUsers = {{.ActiveUser.HasPermissionManageApi }};
</script>
{{ template "footer" true }}
{{ end }}
@@ -0,0 +1,59 @@
{{define "changepw"}}
{{ template "header" . }}
<div class="row">
<div class="col">
<div id="container" class="card" style="width: 25em">
<div class="card-body">
<h2 class="card-title">Change Password</h2>
<br>
<p class="card-text"> A password change has been requested.<br>Please enter a new password.<br><br>
<form method="post" action="./changePassword" id="form" name="form" onSubmit="return submitForm()">
<input type="password" required minlength="{{.MinPasswordLength}}" placeholder="New Password" id="newpw" name="newpw" autocomplete="new-password">
<br><br>
<input type="password" required minlength="{{.MinPasswordLength}}" placeholder="Confirm New Password" id="newpw_c" name="newpw_c" onchange="checkSame()" autocomplete="new-password">
{{ if ne .ErrorMessage "" }}
<p id="errormessage" style="color:red; margin-top:1em">{{.ErrorMessage}}</p>
{{ end }}
<div id="pwnotsame" class="text-warning" style="display: none; margin-top:1em">
Passwords do not match
</div>
<button style="margin-top:2em" type="submit" id="submitbutton" class="btn btn-light">Change Password</button>
</form>
<br>
</div>
</div>
</div>
</div>
<script>
function isSamePw() {
let pw1 = document.getElementById("newpw").value;
let pw2 = document.getElementById("newpw_c").value;
return pw1 === pw2;
}
function checkSame() {
if (!isSamePw()) {
document.getElementById("pwnotsame").style.display = "block";
} else {
document.getElementById("pwnotsame").style.display = "none";
}
let serverErr = document.getElementById("errormessage");
if (serverErr!= null) {
serverErr.style.display = "none";
}
}
function submitForm() {
if (!isSamePw()) {
return false;
}
document.getElementById("submitbutton").disabled = true;
return true;
}
</script>
{{ template "footer" }}
{{end}}
@@ -0,0 +1,16 @@
{{define "error_auth_header"}}
{{template "header" .}}
<div class="row">
<div class="col">
<div class="card" style="width: 18rem;">
<div class="card-body">
<h2 class="card-title">Unauthorised</h2>
<br>
<p class="card-text">Error: No login information was sent from the authentication provider.</p><br>
</div>
</div>
</div>
</div>
{{template "footer"}}
{{end}}
@@ -2,13 +2,13 @@
{{ template "header" . }}
<div class="row">
<div class="col">
<div id="container" class="card" style="width: 20em">
<div id="container" class="card" style="width: 35em">
<div class="card-body">
<h3 class="card-title">Forgot password</h3>
<br>
<p class="card-text">
Please restart the server with the argument <code>--reconfigure</code> in order to change the password.
</p>
If you forgot your user password, please ask your administrator to reset it.<br><br>To reset an administrator password, restart the server with the argument <code class="text-nowrap">--reconfigure</code> and change it in the authentication section.
</p><br>
</div>
</div>
</div>
@@ -71,7 +71,12 @@
<h1>{{.PublicName}}</h1>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link {{ if eq .ActiveView 0}}active{{ end }}" href="./admin">Upload</a>
{{ if .ActiveUser.HasPermissionManageLogs }}
<a class="nav-link {{ if eq .ActiveView 1 }}active{{ end }}" href="./logs">Logs</a>
{{ end }}
{{ if and .ActiveUser.HasPermissionManageUsers .IsUserTabAvailable }}
<a class="nav-link {{ if eq .ActiveView 3 }}active{{ end }}" href="./users">Users</a>
{{ end }}
<a class="nav-link {{ if eq .ActiveView 2 }}active{{ end }}" href="./apiKeys">API</a>
{{ if .IsLogoutAvailable }}<a class="nav-link" href="./logout">Logout</a>{{ end }}
</nav>
@@ -0,0 +1,209 @@
{{ define "users" }}
{{ template "header" . }}
<div class="row">
<div class="col">
<div id="container" class="card" style="width: 80%">
<div class="card-body">
<div class="container">
<div class="row mb-4">
<div class="col">
</div>
<div class="col text-center">
<h3 class="card-title mb-0">Users</h3>
</div>
<div class="col text-end">
<button id="add-user-btn" class="btn btn-outline-light" onclick="showAddUserModal()">
<i class="bi bi-plus-circle-fill"></i>
</button>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-dark">
<thead>
<tr>
<th scope="col">User</th>
<th scope="col">Group</th>
<th scope="col">Last online</th>
<th scope="col">Uploads</th>
<th scope="col">Permissions</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody id="usertable">
{{ range .Users }}
<tr id="row-{{ .User.Id }}">
<td>{{ .User.Name }}</td>
<td id="userlevel_{{ .User.Id }}">{{ .User.GetReadableUserLevel }}</td>
<td>{{ .User.GetReadableDate }}</td>
<td>{{ .UploadCount }}</td>
<td class="prevent-select">
<i id="perm_replace_{{ .User.Id }}" class="bi bi-recycle {{if not .User.HasPermissionReplace}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Replace own uploads" onclick='changeUserPermission("{{ .User.Id }}","PERM_REPLACE", "perm_replace_{{ .User.Id }}");'></i>
<i id="perm_list_{{ .User.Id }}" class="bi bi-eye {{if not .User.HasPermissionListOtherUploads}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="List other uploads" onclick='changeUserPermission("{{ .User.Id }}","PERM_LIST", "perm_list_{{ .User.Id }}");'></i>
<i id="perm_edit_{{ .User.Id }}" class="bi bi-pencil {{if not .User.HasPermissionEditOtherUploads}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Edit other uploads" onclick='changeUserPermission("{{ .User.Id }}","PERM_EDIT", "perm_edit_{{ .User.Id }}");'></i>
<i id="perm_delete_{{ .User.Id }}" class="bi bi-trash3 {{if not .User.HasPermissionDeleteOtherUploads}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Delete other uploads" onclick='changeUserPermission("{{ .User.Id }}","PERM_DELETE", "perm_delete_{{ .User.Id }}");'></i>
<i id="perm_replace_other_{{ .User.Id }}" class="bi bi-arrow-left-right {{if not .User.HasPermissionReplaceOtherUploads}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Replace other uploads" onclick='changeUserPermission("{{ .User.Id }}","PERM_REPLACE_OTHER", "perm_replace_other_{{ .User.Id }}");'></i>
<i id="perm_logs_{{ .User.Id }}" class="bi bi-card-list {{if not .User.HasPermissionManageLogs}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Manage system logs" onclick='changeUserPermission("{{ .User.Id }}","PERM_LOGS", "perm_logs_{{ .User.Id }}");'></i>
<i id="perm_users_{{ .User.Id }}" class="bi bi-people {{if not .User.HasPermissionManageUsers}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Manage users" onclick='changeUserPermission("{{ .User.Id }}","PERM_USERS", "perm_users_{{ .User.Id }}");'></i>
<i id="perm_api_{{ .User.Id }}" class="bi bi-sliders2 {{if not .User.HasPermissionManageApi}}perm-notgranted{{else}}perm-granted{{end}} {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}perm-nochange{{end}}" title="Manage API keys" onclick='changeUserPermission("{{ .User.Id }}","PERM_API", "perm_api_{{ .User.Id }}");'></i>
</td>
<td>
{{if $.IsInternalAuth}}
<button id="pwchange-{{ .User.Id }}" type="button" class="btn btn-outline-light btn-sm" {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}disabled{{end}} onclick="showResetPwModal('{{ .User.Id }}', '{{ .User.Name }}')" title="Reset Password"><i class="bi bi-key-fill"></i></button>&nbsp;
{{end}}
{{if gt .User.UserLevel 1}}
<button id="changeRank_{{ .User.Id }}" type="button" onclick="changeRank({{ .User.Id }}, 'ADMIN', 'changeRank_{{ .User.Id }}')" title="Promote User" {{ if eq .User.Id $.ActiveUser.Id}}disabled{{end}} class="btn btn-outline-light btn-sm">
<i class="bi bi-chevron-double-up"></i></button>
{{ else }}
<button id="changeRank_{{ .User.Id }}" type="button" onclick="changeRank({{ .User.Id }}, 'USER', 'changeRank_{{ .User.Id }}')" {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}disabled{{end}} title="Demote User" class="btn btn-outline-light btn-sm">
<i class="bi bi-chevron-double-down"></i></button>
{{ end }}
&nbsp;<button id="delete-{{ .User.Id }}" type="button" class="btn btn-outline-danger btn-sm" {{if or (eq .User.UserLevel 0) (eq .User.Id $.ActiveUser.Id)}}disabled{{end}} onclick="showDeleteModal('{{ .User.Id }}', '{{ .User.Name }}')" title="Delete"><i class="bi bi-trash3"></i></button></td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div id="toastnotification" class="toastnotification" data-default="Invalid notification">Toast Text</div>
<!-- Modal for deletion confirmation -->
<div class="modal" tabindex="-1" id="deleteModal">
<div class="modal-dialog gokapi-dialog">
<div class="modal-content gokapi-dialog">
<div class="modal-header">
<h5 class="modal-title">Delete User</h5>
</div>
<div class="modal-body">
<p>Are you sure you want to delete user <span id="deleteModalBody" class="fw-bold"></span>?
This action cannot be undone.</p>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="checkboxDelete" value="">
<label class="form-check-label" for="checkboxDelete">
Permanently delete all files uploaded by this user.
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="buttonDelete" class="btn btn-danger">Delete User</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap Modal for Adding a New User -->
<div class="modal fade" id="newUserModal" tabindex="-1" aria-labelledby="newUserModalLabel" aria-hidden="true">
<div class="modal-dialog gokapi-dialog">
<div class="modal-content gokapi-dialog">
<div class="modal-header">
<h5 class="modal-title" id="newUserModalLabel">Create New User</h5>
</div>
<div class="modal-body">
<form id="newUserForm">
<div class="mb-3">
<div class="text-start">
<label for="e_userName" class="form-label">Username</label>
</div>
<input type="text" class="form-control" id="e_userName" minlength="2" placeholder="Enter a username" required>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="newUserForm" onclick="addNewUser();" id="mb_addUser" class="btn btn-primary">Add User</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Reset Password -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog gokapi-dialog text-start">
<div class="modal-content gokapi-dialog">
<!-- Modal Header -->
<div class="modal-header">
<h5 class="modal-title" id="resetPasswordModalLabel">Reset Password</h5>
</div>
<!-- Modal Body -->
<div class="modal-body">
<div id="formentryReset">
<p>Choose an option to reset the password for the user <span class="fw-bold" id="l_userpwreset"></span>:</p>
<!-- Option 1: Force user to set a new password on next login -->
<div class="form-check mb-3">
<input
type="radio"
id="forceNewPassword"
name="resetOption"
class="form-check-input"
value="forceNew"
checked>
<label for="forceNewPassword" class="form-check-label">
Force user to set a new password on next login
</label>
</div>
<!-- Option 2: Generate and display a random password -->
<div class="form-check mb-3">
<input
type="radio"
id="generateRandomPassword"
name="resetOption"
class="form-check-input"
value="generateRandom">
<label for="generateRandomPassword" class="form-check-label">
Generate a new random password (user will be forced to change it on next login)
</label>
</div>
</div>
<!-- Display Random Password (Inline) -->
<div id="randomPasswordContainer" class="mt-3" style="display: none;">
<label class="form-label">
New Password:
<span class="highlighted-password" id="l_returnedPw"></span> <button type="button" id="copypwclip" title="Copy Password" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i></button>
</label>
</div>
</div>
<!-- Modal Footer -->
<div class="modal-footer">
<button type="button" id="cancelPasswordButton"class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="resetPasswordButton" class="btn btn-primary">Confirm</button>
<button type="button" style="display:none" id="closeModalResetPw" class="btn btn-primary" data-bs-dismiss="modal" >OK</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="./js/min/admin.min.{{ template "js_admin_version"}}.js"></script>
<script>
var systemKey = "{{.SystemKey}}";
var isInternalAuth = {{.IsInternalAuth}};
</script>
{{ template "footer" true }}
{{ end }}
@@ -1,5 +1,5 @@
// File contains auto-generated values. Do not change manually
{{define "version"}}1.9.6{{end}}
{{define "version"}}2.0.0-beta1{{end}}
// Specifies the version of JS files, so that the browser doesn't
// use a cached version, if the file has been updated
+1 -1
View File
@@ -56,7 +56,7 @@ test:
test-specific:
@echo Testing package "$(TEST_PACKAGE)"
@echo
go test -v $(GOPACKAGE)/$(TEST_PACKAGE)/... -parallel 8 -count=1 --tags=test,awsmock
go test $(GOPACKAGE)/$(TEST_PACKAGE)/... -parallel 8 -count=1 --tags=test,awsmock
.PHONY: test-all
+506 -128
View File
@@ -3,7 +3,7 @@
"info": {
"title": "Gokapi",
"description": "[https://github.com/Forceu/Gokapi](https://github.com/Forceu/Gokapi)\n",
"version": "1.0"
"version": "2.0"
},
"servers": [
{
@@ -22,6 +22,12 @@
},
{
"name": "auth"
},
{
"name": "user"
},
{
"name": "chunk"
}
],
"paths": {
@@ -31,7 +37,7 @@
"files"
],
"summary": "Lists all files",
"description": "This API call lists all files that are not expired. Returns null, if no files are stored. Requires permission VIEW",
"description": "This API call lists all files that are not expired. Returns null, if no files are stored. Requires API permission VIEW. To view files that were not uploaded by the user, the user needs to have the user permission LIST",
"operationId": "list",
"security": [
{
@@ -57,7 +63,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -68,7 +74,7 @@
"files"
],
"summary": "Get metadata by ID",
"description": "This API call lists all metadata about a file that is not expired. Returns 404 if an invalid/expired ID was passed. Requires permission VIEW",
"description": "This API call lists all metadata about a file that is not expired. Returns 404 if an invalid/expired ID was passed. Requires API permission VIEW. To view files that were not uploaded by the user, the user needs to have the user permission LIST",
"operationId": "listbyid",
"parameters": [
{
@@ -102,7 +108,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -116,7 +122,7 @@
"chunk"
],
"summary": "Uploads a new chunk",
"description": "Uploads a file in chunks, in case a reverse proxy does not support upload of larger files. Parallel uploading is supported. Must call /chunk/complete after all chunks have been uploaded. WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires permission UPLOAD",
"description": "Uploads a file in chunks, in case a reverse proxy does not support upload of larger files. Parallel uploading is supported. Must call /chunk/complete after all chunks have been uploaded. WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires API permission UPLOAD",
"operationId": "chunkadd",
"security": [
{
@@ -148,7 +154,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -159,23 +165,86 @@
"chunk"
],
"summary": "Finalises uploaded chunks",
"description": "Needs to be called after all chunks have been uploaded. Adds the uploaded file to Gokapi. Requires permission UPLOAD",
"description": "Needs to be called after all chunks have been uploaded. Adds the uploaded file to Gokapi. Requires API permission UPLOAD",
"operationId": "chunkcomplete",
"security": [
{
"apikey": ["UPLOAD"]
},
],
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/chunkingcomplete"
}
}
},
"required": true
"parameters": [
{
"name": "uuid",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "The unique ID that was used for the uploaded chunks"
},{
"name": "filename",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "The filename of the uploaded file"
},
{
"name": "filesize",
"in": "header",
"required": true,
"schema": {
"type": "integer"
},
"description": "The total filesize of the uploaded file in bytes"
},
{
"name": "contenttype",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "The MIME content type. If empty, application/octet-stream will be used."
},
{
"name": "allowedDownloads",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many downloads are allowed. Default of 1 will be used if empty. Unlimited if 0 is passed."
},
{
"name": "expiryDays",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many days the file will be stored. Original value will be used if empty. Unlimited if 0 is passed."
},
{
"name": "password",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Password for this file to be set. No password will be used if empty value is passed."
},
{
"name": "nonblocking",
"in": "header",
"required": false,
"schema": {
"type": "boolean"
},
"description": "If true, the call is non blocking and does not wait until the upload is fully processed. No info regarding the file or any errors during the processing will be included in the output."
}
],
"responses": {
"200": {
"description": "Operation successful",
@@ -191,7 +260,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -202,7 +271,7 @@
"files"
],
"summary": "Adds a new file without chunking",
"description": "Uploads the submitted file to Gokapi. Please note: This method does not use chunking, therefore if you are behind a reverse proxy or have a provider that limits upload filesizes, this might not work for bigger files (e.g. Cloudflare). WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires permission UPLOAD",
"description": "Uploads the submitted file to Gokapi. Please note: This method does not use chunking, therefore if you are behind a reverse proxy or have a provider that limits upload filesizes, this might not work for bigger files (e.g. Cloudflare). WARNING: Does not support end-to-end encryption! If server is setup to utilise end-to-end encryption, file will be stored in plain-text! Requires API permission UPLOAD",
"operationId": "add",
"security": [
{
@@ -234,7 +303,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -245,23 +314,69 @@
"files"
],
"summary": "Duplicates an existing file",
"description": "This API call duplicates an existing file with new parameters. Requires permission UPLOAD",
"description": "This API call duplicates an existing file with new parameters. Requires API permission UPLOAD. To duplicate files that were not uploaded by the user, the user needs to have the user permission LIST",
"operationId": "duplicate",
"security": [
{
"apikey": ["VIEW","UPLOAD"]
},
],
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/duplicate"
}
}
},
"required": true
"parameters": [
{
"name": "id",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "ID of file to be duplicated"
},
{
"name": "allowedDownloads",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many remaining downloads are allowed. Original value will be used if empty. Unlimited if 0 is passed."
},
{
"name": "expiryDays",
"in": "header",
"required": false,
"schema": {
"type": "integer"
},
"description": "How many days the file will be stored. Original value will be used if empty. Unlimited if 0 is passed."
},
{
"name": "password",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Password for this file to be set. No password will be used if empty value is passed."
},
{
"name": "originalPassword",
"in": "header",
"required": false,
"schema": {
"type": "boolean"
},
"description": "Set to true to use the original password. Field \"password\" will be ignored if set."
},
{
"name": "filename",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Sets a new filename. Filename will be unchanged if empty."
}
],
"responses": {
"200": {
"description": "Operation successful",
@@ -277,7 +392,7 @@
"description": "Invalid input"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -291,7 +406,7 @@
"files"
],
"summary": "Changes parameters of an uploaded file",
"description": "This API call changes parameters of an uploaded file. Requires permission EDIT",
"description": "This API call changes parameters of an uploaded file. Requires API permission EDIT. To edit files that were not uploaded by the user, the user needs to have the user permission EDIT",
"operationId": "modifyfile",
"security": [
{
@@ -360,7 +475,7 @@
"description": "Invalid ID supplied or incorrect data type sent"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -374,7 +489,7 @@
"files"
],
"summary": "Replaces an uploaded file",
"description": "This API replaces the content of an uploaded file with the content of a different (already uplaoded) file. Note: Replacing end-to-end ecrypted files is NOT possible and will result in an error. Requires permission REPLACE",
"description": "This API replaces the content of an uploaded file with the content of a different (already uplaoded) file. Note: Replacing end-to-end ecrypted files is NOT possible and will result in an error. Requires API permission REPLACE. To replace a file that was not uploaded by the user, the user needs to have the user permission REPLACE_OTHERS. To replace a file with the content uploaded by another user, the user needs to have the user permission LIST",
"operationId": "replacefile",
"security": [
{
@@ -424,7 +539,7 @@
"description": "Invalid ID supplied or incorrect data type sent"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID provided or file has expired"
@@ -438,7 +553,7 @@
"files"
],
"summary": "Deletes the selected file",
"description": "This API call deletes the selected file and runs the clean-up procedure which purges all expired files from the data directory immediately. Requires permission DELETE",
"description": "This API call deletes the selected file and runs the clean-up procedure which purges all expired files from the data directory immediately. Requires API permission DELETE. To delete a file that was not uploaded by the user, the user needs to have the user permission DELETE",
"operationId": "delete",
"security": [
{
@@ -462,11 +577,11 @@
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "Invalid ID supplied"
}
}
}
@@ -477,7 +592,7 @@
"auth"
],
"summary": "Creates a new API key",
"description": "This API call returns a new API key. The new key does not have any permissions, unless specified. Requires permission API_MOD",
"description": "This API call returns a new API key. The new key does not have any permissions, unless specified. Requires API permission API_MOD",
"operationId": "create",
"security": [
{
@@ -520,7 +635,7 @@
}
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
@@ -531,7 +646,7 @@
"auth"
],
"summary": "Changes the name of the API key",
"description": "This API call changes the name of the API key that is shown in the API overview. Requires permission API_MOD",
"description": "This API call changes the name of the API key that is shown in the API overview. Requires API permission API_MOD. To change the name of an API key not owned by the user, the user needs to have the user permission API",
"operationId": "friendlyname",
"security": [
{
@@ -540,9 +655,9 @@
],
"parameters": [
{
"name": "apiKeyToModify",
"name": "targetKey",
"in": "header",
"description": "The API key to change the name of",
"description": "The API key to change the name of. Can be either the public ID or the actual API key",
"required": true,
"style": "simple",
"explode": false,
@@ -566,11 +681,11 @@
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "API key not found"
}
}
}
@@ -581,7 +696,7 @@
"auth"
],
"summary": "Changes the permissions of the API key",
"description": "This API call changes the permissions for the given API key. Requires permission API_MOD",
"description": "This API call changes the permission for the given API key. Requires API permission API_MOD. To to edit an API key not owned by the user, the user needs to have the user permission API",
"operationId": "modifypermission",
"security": [
{
@@ -590,9 +705,9 @@
],
"parameters": [
{
"name": "apiKeyToModify",
"name": "targetKey",
"in": "header",
"description": "The API key to change the permission of",
"description": "The API key to change the permission of. Can be either the public ID or the actual API key",
"required": true,
"style": "simple",
"explode": false,
@@ -609,7 +724,7 @@
"explode": false,
"schema": {
"type": "string",
"enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_API_MOD"]
"enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_REPLACE", "PERM_MANAGE_USERS", "PERM_API_MOD"]
}
},
{
@@ -630,10 +745,13 @@
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
"description": "Invalid parameter supplied or API key owner does not have the sufficient user permissions"
},
"401": {
"description": "Invalid API key provided"
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "API key not found"
}
}
}
@@ -644,7 +762,7 @@
"auth"
],
"summary": "Deletes an API key",
"description": "This API call deletes the given API key. Requires permission API_MOD",
"description": "This API call deletes the given API key. Requires API permission API_MOD. To to delete an API key not owned by the user, the user needs to have the user permission API",
"operationId": "apidelete",
"security": [
{
@@ -653,9 +771,9 @@
],
"parameters": [
{
"name": "apiKeyToModify",
"name": "targetKey",
"in": "header",
"description": "The API key to delete",
"description": "The API key to delete. Can be either the public ID or the actual API key",
"required": true,
"style": "simple",
"explode": false,
@@ -671,12 +789,287 @@
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided"
"404": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
}
},
"/user/create": {
"post": {
"tags": [
"user"
],
"summary": "Creates a new user",
"description": "This API call adds a new user. The new user does not have any specific permissions and is userlevel USER. Requires API permission MANAGE_USERS",
"operationId": "createuser",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "username",
"in": "header",
"description": "Name of new user, must be at least 4 characters",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Operation successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NewUser"
}
}
}
},
"400": {
"description": "Invalid parameters supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"409": {
"description": "A user already exists with that email address"
}
}
}
},
"/user/modify": {
"put": {
"tags": [
"user"
],
"summary": "Changes the permissions of a user",
"description": "This API call changes the permission for the given user. Requires API permission MANAGE_USERS",
"operationId": "usermodify",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to change the permission of",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},
{
"name": "userpermission",
"in": "header",
"description": "The name of the permission",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string",
"enum": ["PERM_REPLACE", "PERM_LIST", "PERM_EDIT", "PERM_REPLACE_OTHER", "PERM_DELETE", "PERM_LOGS", "PERM_API", "PERM_USERS"]
}
},
{
"name": "permissionModifier",
"in": "header",
"description": "If the permission shall be granted or revoked",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string",
"enum": ["GRANT", "REVOKE"]
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid parameter supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "User not found"
}
}
}
},
"/user/changeRank": {
"put": {
"tags": [
"user"
],
"summary": "Changes the rank of a user",
"description": "This API call changes the rank for the given user. Requires API permission MANAGE_USERS",
"operationId": "userchangerank",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to change the rank of",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},
{
"name": "newRank",
"in": "header",
"description": "The name of the new rank",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string",
"enum": ["ADMIN", "USER"]
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid parameter supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
},
"404": {
"description": "User not found"
}
}
}
},
"/user/delete": {
"delete": {
"tags": [
"user"
],
"summary": "Deletes the selected user",
"description": "This API call changes deletes the given user. If files are associated with the user, they will be linked with the user that initiated the deletion. If deleteFiles is \"true\", the files will be deleted instead. Requires API permission MANAGE_USERS",
"operationId": "userdelete",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to be deleted",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},
{
"name": "deleteFiles",
"in": "header",
"description": "Delete all associated uploads from this user",
"required": false,
"style": "simple",
"explode": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID or parameters supplied"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
},
"/user/resetPassword": {
"put": {
"tags": [
"user"
],
"summary": "Resets the password of the current user",
"description": "This API call forces a passwrd change once the given user logs in the next time. If generateNewPassword is \"true\", the current password will be replaced with a generated one. Requires API permission MANAGE_USERS",
"operationId": "userresetpw",
"security": [
{
"apikey": ["MANAGE_USERS"]
}
],
"parameters": [
{
"name": "userid",
"in": "header",
"description": "The user to reset",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
},{
"name": "generateNewPassword",
"in": "header",
"description": "Generate a new password",
"required": false,
"style": "simple",
"explode": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Operation successful",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PasswordReset"
}
}
}
},
"400": {
"description": "Invalid ID or parameters supplied, user is super admin or user is equal to owner of API key"
},
"401": {
"description": "Invalid API key provided for authentication or API key does not have the required permission"
}
}
}
},
},
"components": {
"schemas": {
@@ -826,11 +1219,62 @@
"Id": {
"type": "string",
"example": "ar3iecahghiethiemeeR"
},
"PublicId": {
"type": "string",
"example": "oepah5iesae8YeeZohrain5ahNgax8su"
}
},
"description": "NewApiKey is the struct used for the result after creating a new API key",
"x-go-package": "Gokapi/internal/models"
},
"NewUser": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "user@gokapi"
},
"id": {
"type": "integer",
"example": 14
},
"lastOnline": {
"type": "integer",
"example": 0
},
"name": {
"type": "string",
"example": "Gokapi user"
},
"permissions": {
"type": "integer",
"example": 0
},
"userLevel": {
"type": "integer",
"example": 2
}
},
"description": "NewUser is the struct used for the result after creating a new API key",
"x-go-package": "Gokapi/internal/models"
},
"PasswordReset": {
"type": "object",
"properties": {
"result": {
"type": "string",
"example": "OK"
},
"password": {
"type": "string",
"description": "Empty if no new password was generated, otherwise contains the new password",
"example": "ahseth6ahV"
}
},
"description": "NewUser is the struct used for the result after creating a new API key",
"x-go-package": "Gokapi/internal/models"
},
"body": {
"required": [
"file"
@@ -855,38 +1299,7 @@
"description": "Password for this file to be set. No password will be used if empty"
}
}
},"duplicate": {
"required": [
"id"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of file to be duplicated"
},
"allowedDownloads": {
"type": "integer",
"description": "How many downloads are allowed. Original value from web interface will be used if empty. Unlimited if 0 is passed."
},
"expiryDays": {
"type": "integer",
"description": "How many days the file will be stored. Original value from web interface will be used if empty. Unlimited if 0 is passed."
},
"password": {
"type": "string",
"description": "Password for this file to be set. No password will be used if empty."
},
"originalPassword": {
"type": "boolean",
"description": "Set to true to use original password. Field \"password\" will be ignored if set."
},
"filename": {
"type": "string",
"description": "Sets a new filename. Filename will be unchanged if empty."
}
}
},"chunking": {
},"chunking": {
"required": [
"file","uuid","filesize","offset"
],
@@ -910,41 +1323,6 @@
"description": "The chunk's offset starting at the beginning of the file"
}
}
},"chunkingcomplete": {
"required": [
"uuid","filename","filesize"
],
"type": "object",
"properties": {
"uuid": {
"type": "string",
"description": "The unique ID that was used for the uploaded chunks"
},
"filename": {
"type": "string",
"description": "The filename of the uploaded file"
},
"filesize": {
"type": "integer",
"description": "The total filesize of the uploaded file in bytes"
},
"contenttype": {
"type": "string",
"description": "The MIME content type. If empty, application/octet-stream will be used."
},
"allowedDownloads": {
"type": "integer",
"description": "How many downloads are allowed. Default of 1 will be used if empty. Unlimited if 0 is passed."
},
"expiryDays": {
"type": "integer",
"description": "How many days the file will be stored. Default of 14 will be used if empty. Unlimited if 0 is passed."
},
"password": {
"type": "string",
"description": "Password for this file to be set. No password will be used if empty"
}
}
}
},
"securitySchemes": {