mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-04-28 02:00:19 -05:00
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:
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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!
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 </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>
|
||||
<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>
|
||||
<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> 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> 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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ package models
|
||||
type Session struct {
|
||||
RenewAt int64 `redis:"renew_at"`
|
||||
ValidUntil int64 `redis:"valid_until"`
|
||||
UserId int `redis:"user_id"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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¶meter != 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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+508
-379
File diff suppressed because it is too large
Load Diff
+1271
-513
File diff suppressed because it is too large
Load Diff
@@ -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: ¶mFilesListSingle{},
|
||||
},
|
||||
{
|
||||
Url: "/chunk/add",
|
||||
ApiPerm: models.ApiPermUpload,
|
||||
execution: apiChunkAdd,
|
||||
RequestParser: ¶mChunkAdd{},
|
||||
},
|
||||
{
|
||||
Url: "/chunk/complete",
|
||||
ApiPerm: models.ApiPermUpload,
|
||||
execution: apiChunkComplete,
|
||||
RequestParser: ¶mChunkComplete{},
|
||||
},
|
||||
{
|
||||
Url: "/files/add",
|
||||
ApiPerm: models.ApiPermUpload,
|
||||
execution: apiUploadFile,
|
||||
RequestParser: ¶mFilesAdd{},
|
||||
},
|
||||
{
|
||||
Url: "/files/delete",
|
||||
ApiPerm: models.ApiPermDelete,
|
||||
execution: apiDeleteFile,
|
||||
RequestParser: ¶mFilesDelete{},
|
||||
},
|
||||
{
|
||||
Url: "/files/duplicate",
|
||||
ApiPerm: models.ApiPermUpload,
|
||||
execution: apiDuplicateFile,
|
||||
RequestParser: ¶mFilesDuplicate{},
|
||||
},
|
||||
{
|
||||
Url: "/files/modify",
|
||||
ApiPerm: models.ApiPermEdit,
|
||||
execution: apiEditFile,
|
||||
RequestParser: ¶mFilesModify{},
|
||||
},
|
||||
{
|
||||
Url: "/files/replace",
|
||||
ApiPerm: models.ApiPermReplace,
|
||||
execution: apiReplaceFile,
|
||||
RequestParser: ¶mFilesReplace{},
|
||||
},
|
||||
{
|
||||
Url: "/auth/create",
|
||||
ApiPerm: models.ApiPermApiMod,
|
||||
execution: apiCreateApiKey,
|
||||
RequestParser: ¶mAuthCreate{},
|
||||
},
|
||||
{
|
||||
Url: "/auth/friendlyname",
|
||||
ApiPerm: models.ApiPermApiMod,
|
||||
execution: apiChangeFriendlyName,
|
||||
RequestParser: ¶mAuthFriendlyName{},
|
||||
},
|
||||
{
|
||||
Url: "/auth/modify",
|
||||
ApiPerm: models.ApiPermApiMod,
|
||||
execution: apiModifyApiKey,
|
||||
RequestParser: ¶mAuthModify{},
|
||||
},
|
||||
{
|
||||
Url: "/auth/delete",
|
||||
ApiPerm: models.ApiPermApiMod,
|
||||
execution: apiDeleteKey,
|
||||
RequestParser: ¶mAuthDelete{},
|
||||
},
|
||||
{
|
||||
Url: "/user/create",
|
||||
ApiPerm: models.ApiPermManageUsers,
|
||||
execution: apiCreateUser,
|
||||
RequestParser: ¶mUserCreate{},
|
||||
},
|
||||
{
|
||||
Url: "/user/changeRank",
|
||||
ApiPerm: models.ApiPermManageUsers,
|
||||
execution: apiChangeUserRank,
|
||||
RequestParser: ¶mUserChangeRank{},
|
||||
},
|
||||
{
|
||||
Url: "/user/delete",
|
||||
ApiPerm: models.ApiPermManageUsers,
|
||||
execution: apiDeleteUser,
|
||||
RequestParser: ¶mUserDelete{},
|
||||
},
|
||||
{
|
||||
Url: "/user/modify",
|
||||
ApiPerm: models.ApiPermManageUsers,
|
||||
execution: apiModifyUser,
|
||||
RequestParser: ¶mUserModify{},
|
||||
},
|
||||
{
|
||||
Url: "/user/resetPassword",
|
||||
ApiPerm: models.ApiPermManageUsers,
|
||||
execution: apiResetPassword,
|
||||
RequestParser: ¶mUserResetPw{},
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 ¶mFilesListSingle{}
|
||||
}
|
||||
|
||||
// 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 ¶mFilesAdd{}
|
||||
}
|
||||
|
||||
// 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 ¶mFilesDuplicate{}
|
||||
}
|
||||
|
||||
// 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 ¶mFilesModify{}
|
||||
}
|
||||
|
||||
// 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 ¶mFilesReplace{}
|
||||
}
|
||||
|
||||
// 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 ¶mFilesDelete{}
|
||||
}
|
||||
|
||||
// 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 ¶mAuthCreate{}
|
||||
}
|
||||
|
||||
// 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 ¶mAuthFriendlyName{}
|
||||
}
|
||||
|
||||
// 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 ¶mAuthModify{}
|
||||
}
|
||||
|
||||
// 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 ¶mAuthDelete{}
|
||||
}
|
||||
|
||||
// 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 ¶mUserCreate{}
|
||||
}
|
||||
|
||||
// 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 ¶mUserChangeRank{}
|
||||
}
|
||||
|
||||
// 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 ¶mUserDelete{}
|
||||
}
|
||||
|
||||
// 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 ¶mUserModify{}
|
||||
}
|
||||
|
||||
// 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 ¶mUserResetPw{}
|
||||
}
|
||||
|
||||
// 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 ¶mChunkAdd{}
|
||||
}
|
||||
|
||||
// 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 ¶mChunkComplete{}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}
|
||||
@@ -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> ';
|
||||
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> <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 </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 </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 </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 </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 </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 </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 }}
|
||||
|
||||
@@ -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>
|
||||
{{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 }}
|
||||
|
||||
|
||||
<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
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user