From 75745aebb8648e24fdce9232d8b7b514efb3a73c Mon Sep 17 00:00:00 2001 From: Marc Bulling Date: Sat, 20 Dec 2025 21:02:17 +0100 Subject: [PATCH] Use short-lived tokens instead of user API keys (#340) --- build/go-generate/minifyStaticContent.go | 2 +- build/go-generate/updateProtectedUrls.go | 2 +- build/go-generate/updateVersionNumbers.go | 2 +- docs/setup.rst | 1 + internal/configuration/database/Database.go | 5 - .../configuration/database/Database_test.go | 35 +-- .../database/dbabstraction/DbAbstraction.go | 3 +- .../database/provider/redis/Redis_test.go | 50 +--- .../database/provider/redis/apikeys.go | 26 +- .../database/provider/sqlite/Sqlite_test.go | 48 +--- .../database/provider/sqlite/apikeys.go | 29 +- internal/configuration/setup/ProtectedUrls.go | 2 +- internal/models/ApiKey.go | 41 ++- internal/models/ApiKey_test.go | 92 +++++- internal/webserver/Webserver.go | 26 +- internal/webserver/api/Api.go | 62 +---- internal/webserver/api/Api_test.go | 129 +-------- internal/webserver/api/routing.go | 30 +- .../tokengeneration/TokenGeneration.go | 39 +++ .../web/static/apidocumentation/openapi.json | 2 +- internal/webserver/web/static/js/admin_api.js | 261 ++++++++++++++++-- .../min/{admin.min.13.js => admin.min.14.js} | 2 +- .../webserver/web/templates/html_admin.tmpl | 1 - .../webserver/web/templates/html_api.tmpl | 1 - .../webserver/web/templates/html_end2end.tmpl | 4 - .../webserver/web/templates/html_logs.tmpl | 1 - .../webserver/web/templates/html_users.tmpl | 1 - .../web/templates/string_constants.tmpl | 2 +- openapi.json | 2 +- 29 files changed, 481 insertions(+), 420 deletions(-) create mode 100644 internal/webserver/authentication/tokengeneration/TokenGeneration.go rename internal/webserver/web/static/js/min/{admin.min.13.js => admin.min.14.js} (65%) diff --git a/build/go-generate/minifyStaticContent.go b/build/go-generate/minifyStaticContent.go index 245ed46..cbb7ed7 100644 --- a/build/go-generate/minifyStaticContent.go +++ b/build/go-generate/minifyStaticContent.go @@ -137,6 +137,6 @@ func fileExists(filename string) bool { // Auto-generated content below, do not modify // Version codes can be changed in updateVersionNumbers.go -const jsAdminVersion = 13 +const jsAdminVersion = 14 const jsE2EVersion = 8 const cssMainVersion = 5 diff --git a/build/go-generate/updateProtectedUrls.go b/build/go-generate/updateProtectedUrls.go index d123d08..0c81e4a 100644 --- a/build/go-generate/updateProtectedUrls.go +++ b/build/go-generate/updateProtectedUrls.go @@ -97,7 +97,7 @@ func writeDocumentationFile(urls []string) { for _, url := range urls { output = output + "- ``" + url + "``\n" } - regex := regexp.MustCompile(`proxy:(?:\r?\n)+((?:- ` + "``" + `\/\w+` + "``" + `\r?\n)+)`) + regex := regexp.MustCompile("proxy:(?:\\r?\\n)+(?:- ``\\/[^`]+``\\r?\\n)+") matches := regex.FindAllIndex(documentationContent, -1) if len(matches) != 1 { fmt.Println("ERROR: Not one match found exactly for documentation") diff --git a/build/go-generate/updateVersionNumbers.go b/build/go-generate/updateVersionNumbers.go index a16a016..b0fb5af 100644 --- a/build/go-generate/updateVersionNumbers.go +++ b/build/go-generate/updateVersionNumbers.go @@ -11,7 +11,7 @@ import ( "strings" ) -const versionJsAdmin = 13 +const versionJsAdmin = 14 const versionJsDropzone = 5 const versionJsE2EAdmin = 8 const versionCssMain = 5 diff --git a/docs/setup.rst b/docs/setup.rst index bed9a01..613c4d7 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -283,6 +283,7 @@ This option disables Gokapis internal authentication completely, except for API - ``/admin`` - ``/apiKeys`` +- ``/auth/token`` - ``/changePassword`` - ``/e2eSetup`` - ``/logs`` diff --git a/internal/configuration/database/Database.go b/internal/configuration/database/Database.go index e4922c8..3e8503b 100644 --- a/internal/configuration/database/Database.go +++ b/internal/configuration/database/Database.go @@ -139,11 +139,6 @@ func DeleteApiKey(id string) { db.DeleteApiKey(id) } -// GetSystemKey returns the latest UI API key -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) diff --git a/internal/configuration/database/Database_test.go b/internal/configuration/database/Database_test.go index 31aa807..e72efa5 100644 --- a/internal/configuration/database/Database_test.go +++ b/internal/configuration/database/Database_test.go @@ -1,15 +1,16 @@ package database import ( + "log" + "os" + "testing" + "time" + "github.com/alicebob/miniredis/v2" "github.com/forceu/gokapi/internal/configuration/database/dbabstraction" "github.com/forceu/gokapi/internal/configuration/database/dbcache" "github.com/forceu/gokapi/internal/models" "github.com/forceu/gokapi/internal/test" - "log" - "os" - "testing" - "time" ) var configSqlite = models.DbConnection{ @@ -92,32 +93,6 @@ func TestApiKeys(t *testing.T) { 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) { diff --git a/internal/configuration/database/dbabstraction/DbAbstraction.go b/internal/configuration/database/dbabstraction/DbAbstraction.go index 28f14ac..981e3e3 100644 --- a/internal/configuration/database/dbabstraction/DbAbstraction.go +++ b/internal/configuration/database/dbabstraction/DbAbstraction.go @@ -2,6 +2,7 @@ package dbabstraction import ( "fmt" + "github.com/forceu/gokapi/internal/configuration/database/provider/redis" "github.com/forceu/gokapi/internal/configuration/database/provider/sqlite" "github.com/forceu/gokapi/internal/models" @@ -43,8 +44,6 @@ type Database interface { UpdateTimeApiKey(apikey models.ApiKey) // DeleteApiKey deletes an API key with the given ID DeleteApiKey(id string) - // GetSystemKey returns the latest UI API key - GetSystemKey(userId int) (models.ApiKey, bool) // GetApiKeyByPublicKey returns an API key by using the public key GetApiKeyByPublicKey(publicKey string) (string, bool) diff --git a/internal/configuration/database/provider/redis/Redis_test.go b/internal/configuration/database/provider/redis/Redis_test.go index 9fbfd6b..5d67b4f 100644 --- a/internal/configuration/database/provider/redis/Redis_test.go +++ b/internal/configuration/database/provider/redis/Redis_test.go @@ -1,15 +1,16 @@ package redis import ( - "github.com/alicebob/miniredis/v2" - "github.com/forceu/gokapi/internal/models" - "github.com/forceu/gokapi/internal/test" - redigo "github.com/gomodule/redigo/redis" "log" "os" "slices" "testing" "time" + + "github.com/alicebob/miniredis/v2" + "github.com/forceu/gokapi/internal/models" + "github.com/forceu/gokapi/internal/test" + redigo "github.com/gomodule/redigo/redis" ) var config = models.DbConnection{ @@ -253,47 +254,6 @@ func TestApiKeys(t *testing.T) { 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) { diff --git a/internal/configuration/database/provider/redis/apikeys.go b/internal/configuration/database/provider/redis/apikeys.go index 59f379f..3020052 100644 --- a/internal/configuration/database/provider/redis/apikeys.go +++ b/internal/configuration/database/provider/redis/apikeys.go @@ -1,10 +1,11 @@ package redis import ( + "strings" + "github.com/forceu/gokapi/internal/helper" "github.com/forceu/gokapi/internal/models" redigo "github.com/gomodule/redigo/redis" - "strings" ) const ( @@ -41,29 +42,6 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) { return apikey, true } -// GetSystemKey returns the latest UI API key -func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) { - keys := p.GetAllApiKeys() - foundKey := "" - var latestExpiry int64 - for _, key := range keys { - if !key.IsSystemKey { - continue - } - if key.UserId != userId { - continue - } - if key.Expiry > latestExpiry { - foundKey = key.Id - latestExpiry = key.Expiry - } - } - if foundKey == "" { - return models.ApiKey{}, false - } - 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() diff --git a/internal/configuration/database/provider/sqlite/Sqlite_test.go b/internal/configuration/database/provider/sqlite/Sqlite_test.go index 09169b0..902edad 100644 --- a/internal/configuration/database/provider/sqlite/Sqlite_test.go +++ b/internal/configuration/database/provider/sqlite/Sqlite_test.go @@ -3,15 +3,16 @@ package sqlite import ( - "github.com/forceu/gokapi/internal/helper" - "github.com/forceu/gokapi/internal/models" - "github.com/forceu/gokapi/internal/test" "math" "os" "slices" "sync" "testing" "time" + + "github.com/forceu/gokapi/internal/helper" + "github.com/forceu/gokapi/internal/models" + "github.com/forceu/gokapi/internal/test" ) var config = models.DbConnection{ @@ -451,47 +452,6 @@ func TestUpdateTimeApiKey(t *testing.T) { 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) { diff --git a/internal/configuration/database/provider/sqlite/apikeys.go b/internal/configuration/database/provider/sqlite/apikeys.go index 98ef907..0ffbdc6 100644 --- a/internal/configuration/database/provider/sqlite/apikeys.go +++ b/internal/configuration/database/provider/sqlite/apikeys.go @@ -3,9 +3,10 @@ package sqlite import ( "database/sql" "errors" + "time" + "github.com/forceu/gokapi/internal/helper" "github.com/forceu/gokapi/internal/models" - "time" ) type schemaApiKeys struct { @@ -76,32 +77,6 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) { return result, true } -// GetSystemKey returns the latest UI API key -func (p DatabaseProvider) GetSystemKey(userId int) (models.ApiKey, bool) { - var rowResult schemaApiKeys - 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 - } - helper.Check(err) - return models.ApiKey{}, false - } - - result := models.ApiKey{ - Id: rowResult.Id, - PublicId: rowResult.PublicId, - FriendlyName: rowResult.FriendlyName, - LastUsed: rowResult.LastUsed, - 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 diff --git a/internal/configuration/setup/ProtectedUrls.go b/internal/configuration/setup/ProtectedUrls.go index 197f51e..cc0a367 100644 --- a/internal/configuration/setup/ProtectedUrls.go +++ b/internal/configuration/setup/ProtectedUrls.go @@ -6,4 +6,4 @@ package setup // 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", "/changePassword", "/e2eSetup", "/logs", "/uploadChunk", "/uploadStatus", "/users"} +var protectedUrls = []string{"/admin", "/apiKeys", "/auth/token", "/changePassword", "/e2eSetup", "/logs", "/uploadChunk", "/uploadStatus", "/users"} diff --git a/internal/models/ApiKey.go b/internal/models/ApiKey.go index 4df4796..afcc377 100644 --- a/internal/models/ApiKey.go +++ b/internal/models/ApiKey.go @@ -1,25 +1,27 @@ package models import ( + "errors" + "strings" "time" ) const ( - // ApiPermView is the permission for viewing metadata of all uploaded files + // ApiPermView is the permission for viewing metadata of all uploaded files PERM_VIEW ApiPermView ApiPermission = 1 << iota - // ApiPermUpload is the permission for creating new files + // ApiPermUpload is the permission for creating new files PERM_UPLOAD ApiPermUpload - // ApiPermDelete is the permission for deleting files + // ApiPermDelete is the permission for deleting files PERM_DELETE ApiPermDelete - // ApiPermApiMod is the permission for adding / removing API key permissions + // ApiPermApiMod is the permission for adding / removing API key permissions PERM_API_MOD ApiPermApiMod - // ApiPermEdit is the permission for editing parameters of uploaded files + // ApiPermEdit is the permission for editing parameters of uploaded files PERM_EDIT ApiPermEdit - // ApiPermReplace is the permission for replacing the content of uploaded files + // ApiPermReplace is the permission for replacing the content of uploaded files PERM_REPLACE ApiPermReplace - // ApiPermManageUsers is the permission for managing users + // ApiPermManageUsers is the permission for managing users PERM_MANAGE_USERS ApiPermManageUsers - // ApiPermManageLogs is the permission required for managing the log file + // ApiPermManageLogs is the permission required for managing the log file PERM_MANAGE_LOGS ApiPermManageLogs ) @@ -48,6 +50,29 @@ type ApiKey struct { // ApiPermission contains zero or more permissions as an uint8 format type ApiPermission uint8 +func ApiPermissionFromString(permString string) (ApiPermission, error) { + switch strings.ToUpper(permString) { + case "PERM_VIEW": + return ApiPermView, nil + case "PERM_UPLOAD": + return ApiPermUpload, nil + case "PERM_DELETE": + return ApiPermDelete, nil + case "PERM_API_MOD": + return ApiPermApiMod, nil + case "PERM_EDIT": + return ApiPermEdit, nil + case "PERM_REPLACE": + return ApiPermReplace, nil + case "PERM_MANAGE_USERS": + return ApiPermManageUsers, nil + case "PERM_MANAGE_LOGS": + return ApiPermManageLogs, nil + default: + return 0, errors.New("invalid permission") + } +} + // GetReadableDate returns the date as YYYY-MM-DD HH:MM:SS func (key *ApiKey) GetReadableDate() string { if key.LastUsed == 0 { diff --git a/internal/models/ApiKey_test.go b/internal/models/ApiKey_test.go index 8e5b005..9757383 100644 --- a/internal/models/ApiKey_test.go +++ b/internal/models/ApiKey_test.go @@ -1,9 +1,10 @@ package models import ( - "github.com/forceu/gokapi/internal/test" "os" "testing" + + "github.com/forceu/gokapi/internal/test" ) func TestApiKey_GetReadableDate(t *testing.T) { @@ -120,6 +121,13 @@ func TestHasPermissionManageUsers(t *testing.T) { t.Errorf("expected edit permission to be set") } } +func TestHasPermissionManageLogs(t *testing.T) { + key := &ApiKey{} + key.GrantPermission(ApiPermManageLogs) + if !key.HasPermissionManageLogs() { + t.Errorf("expected edit permission to be set") + } +} func TestApiPermAllNoApiMod(t *testing.T) { key := &ApiKey{} @@ -141,7 +149,8 @@ func TestApiPermAll(t *testing.T) { !key.HasPermission(ApiPermApiMod) || !key.HasPermission(ApiPermEdit) || !key.HasPermission(ApiPermReplace) || - !key.HasPermission(ApiPermManageUsers) { + !key.HasPermission(ApiPermManageUsers) || + !key.HasPermission(ApiPermManageLogs) { t.Errorf("expected all permissions to be set") } } @@ -159,6 +168,7 @@ func checkOnlyPermissionSet(t *testing.T, key *ApiKey, perm ApiPermission) { {ApiPermEdit, "ApiPermEdit"}, {ApiPermReplace, "ApiPermReplace"}, {ApiPermManageUsers, "ApiPermManageUsers"}, + {ApiPermManageLogs, "ApiPermManageLogs"}, } for _, p := range allPermissions { @@ -189,6 +199,7 @@ func TestSetIndividualPermissions(t *testing.T) { {ApiPermEdit, "ApiPermEdit"}, {ApiPermReplace, "ApiPermReplace"}, {ApiPermManageUsers, "ApiPermManageUsers"}, + {ApiPermManageLogs, "ApiPermManageLogs"}, } for _, p := range permissions { @@ -217,6 +228,7 @@ func TestSetCombinedPermissions(t *testing.T) { ApiPermEdit, ApiPermReplace, ApiPermManageUsers, + ApiPermManageLogs, } // Test setting permissions in combination @@ -228,3 +240,79 @@ func TestSetCombinedPermissions(t *testing.T) { checkCombinedPermissions(t, key, allPermissions[:i+1]) } } + +func TestApiPermissionFromString(t *testing.T) { + tests := []struct { + name string + input string + wantPerm ApiPermission + wantError bool + }{ + { + name: "PERM_VIEW", + input: "PERM_VIEW", + wantPerm: ApiPermView, + }, + { + name: "PERM_UPLOAD lowercase", + input: "perm_upload", + wantPerm: ApiPermUpload, + }, + { + name: "PERM_DELETE mixed case", + input: "Perm_Delete", + wantPerm: ApiPermDelete, + }, + { + name: "PERM_API_MOD", + input: "PERM_API_MOD", + wantPerm: ApiPermApiMod, + }, + { + name: "PERM_EDIT", + input: "PERM_EDIT", + wantPerm: ApiPermEdit, + }, + { + name: "PERM_REPLACE", + input: "PERM_REPLACE", + wantPerm: ApiPermReplace, + }, + { + name: "PERM_MANAGE_USERS", + input: "PERM_MANAGE_USERS", + wantPerm: ApiPermManageUsers, + }, + { + name: "PERM_MANAGE_LOGS", + input: "PERM_MANAGE_LOGS", + wantPerm: ApiPermManageLogs, + }, + { + name: "invalid permission", + input: "PERM_UNKNOWN", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPerm, err := ApiPermissionFromString(tt.input) + + if tt.wantError { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if gotPerm != tt.wantPerm { + t.Fatalf("expected %v, got %v", tt.wantPerm, gotPerm) + } + }) + } +} diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 8380434..bcc9ea3 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -17,6 +17,7 @@ import ( "log" "net/http" "sort" + "strconv" "strings" templatetext "text/template" "time" @@ -33,6 +34,7 @@ import ( "github.com/forceu/gokapi/internal/webserver/authentication" "github.com/forceu/gokapi/internal/webserver/authentication/oauth" "github.com/forceu/gokapi/internal/webserver/authentication/sessionmanager" + "github.com/forceu/gokapi/internal/webserver/authentication/tokengeneration" "github.com/forceu/gokapi/internal/webserver/favicon" "github.com/forceu/gokapi/internal/webserver/fileupload" "github.com/forceu/gokapi/internal/webserver/sse" @@ -91,6 +93,7 @@ func Start() { loadExpiryImage() mux.Handle("/", filesystemHandler(webserverDir)) + mux.HandleFunc("/auth/token", requireLogin(handleGenerateAuthToken, false, false)) mux.HandleFunc("/admin", requireLogin(showAdminMenu, true, false)) mux.HandleFunc("/api/", processApi) mux.HandleFunc("/apiKeys", requireLogin(showApiAdmin, true, false)) @@ -278,6 +281,25 @@ func showIndex(w http.ResponseWriter, r *http.Request) { helper.CheckIgnoreTimeout(err) } +func handleGenerateAuthToken(w http.ResponseWriter, r *http.Request) { + user, err := authentication.GetUserFromRequest(r) + if err != nil { + panic(err) + } + permString := r.Header.Get("permission") + permission, err := models.ApiPermissionFromString(permString) + if err != nil { + http.Error(w, "Invalid permission", http.StatusBadRequest) + return + } + token, expiry, err := tokengeneration.Generate(user, permission) + if err != nil { + http.Error(w, "Invalid permission", http.StatusBadRequest) + return + } + _, _ = w.Write([]byte("{\"key\":\"" + token + "\",\"expiry\":" + strconv.FormatInt(expiry, 10) + "}")) +} + // Handling of /changePassword func changePassword(w http.ResponseWriter, r *http.Request) { var errMessage string @@ -631,7 +653,6 @@ func showE2ESetup(w http.ResponseWriter, r *http.Request) { err = templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{ HasBeenSetup: e2einfo.HasBeenSetUp(), PublicName: configuration.Get().PublicName, - SystemKey: api.GetSystemKey(user.Id), CustomContent: customStaticInfo}) helper.CheckIgnoreTimeout(err) } @@ -659,7 +680,6 @@ type e2ESetupView struct { IsDownloadView bool HasBeenSetup bool PublicName string - SystemKey string CustomContent customStatic } @@ -673,7 +693,6 @@ type AdminView struct { ServerUrl string Logs string PublicName string - SystemKey string IsAdminView bool IsDownloadView bool IsApiView bool @@ -785,7 +804,6 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView { u.MinLengthPassword = config.MinLengthPassword u.ChunkSize = config.ChunkSize u.IncludeFilename = config.IncludeFilename - u.SystemKey = api.GetSystemKey(user.Id) return u } diff --git a/internal/webserver/api/Api.go b/internal/webserver/api/Api.go index 4861fc1..dc31f00 100644 --- a/internal/webserver/api/Api.go +++ b/internal/webserver/api/Api.go @@ -3,6 +3,11 @@ package api import ( "encoding/json" "errors" + "io" + "net/http" + "strings" + "time" + "github.com/forceu/gokapi/internal/configuration" "github.com/forceu/gokapi/internal/configuration/database" "github.com/forceu/gokapi/internal/encryption" @@ -11,14 +16,10 @@ import ( "github.com/forceu/gokapi/internal/models" "github.com/forceu/gokapi/internal/storage" "github.com/forceu/gokapi/internal/webserver/fileupload" - "io" - "net/http" - "strings" - "time" ) -const lengthPublicId = 35 -const lengthApiKey = 30 +const LengthPublicId = 35 +const LengthApiKey = 30 const minLengthUser = 2 // Process parses the request and executes the API call or returns an error message to the sender @@ -109,8 +110,8 @@ func generateNewKey(defaultPermissions bool, userId int, friendlyName string) mo friendlyName = "Unnamed key" } newKey := models.ApiKey{ - Id: helper.GenerateRandomString(lengthApiKey), - PublicId: helper.GenerateRandomString(lengthPublicId), + Id: helper.GenerateRandomString(LengthApiKey), + PublicId: helper.GenerateRandomString(LengthPublicId), FriendlyName: friendlyName, Permissions: models.ApiPermDefault, IsSystemKey: false, @@ -123,49 +124,6 @@ func generateNewKey(defaultPermissions bool, userId int, friendlyName string) mo return newKey } -// newSystemKey generates a new API key that is only used internally for the GUI -// and will be valid for 48 hours -func newSystemKey(userId int) string { - user, ok := database.GetUser(userId) - if !ok { - panic("user not found") - } - tempKey := models.ApiKey{ - Permissions: models.ApiPermAll, - } - if !user.HasPermissionReplace() { - tempKey.RemovePermission(models.ApiPermReplace) - } - if !user.HasPermissionManageUsers() { - tempKey.RemovePermission(models.ApiPermManageUsers) - } - if !user.HasPermissionManageLogs() { - tempKey.RemovePermission(models.ApiPermManageLogs) - } - - newKey := models.ApiKey{ - Id: helper.GenerateRandomString(lengthApiKey), - PublicId: helper.GenerateRandomString(lengthPublicId), - FriendlyName: "Internal System Key", - Permissions: tempKey.Permissions, - Expiry: time.Now().Add(time.Hour * 48).Unix(), - IsSystemKey: true, - UserId: userId, - } - database.SaveApiKey(newKey) - return newKey.Id -} - -// GetSystemKey returns the latest System API key or generates a new one, if none exists or the current one expires -// within the next 24 hours -func GetSystemKey(userId int) string { - key, ok := database.GetSystemKey(userId) - if !ok || key.Expiry < time.Now().Add(time.Hour*24).Unix() { - return newSystemKey(userId) - } - return key.Id -} - func apiDeleteKey(w http.ResponseWriter, r requestParser, user models.User) { request, ok := r.(*paramAuthDelete) if !ok { @@ -812,7 +770,7 @@ func sendError(w http.ResponseWriter, errorInt int, errorMessage string) { // publicKeyToApiKey tries to convert a (possible) public key to a private key // If not a public key or if invalid, the original value is returned func publicKeyToApiKey(publicKey string) string { - if len(publicKey) == lengthPublicId { + if len(publicKey) == LengthPublicId { privateApiKey, ok := database.GetApiKeyByPublicKey(publicKey) if ok { return privateApiKey diff --git a/internal/webserver/api/Api_test.go b/internal/webserver/api/Api_test.go index 64fa8cb..10740ce 100644 --- a/internal/webserver/api/Api_test.go +++ b/internal/webserver/api/Api_test.go @@ -3,13 +3,6 @@ package api import ( "bytes" "encoding/json" - "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/storage" - "github.com/forceu/gokapi/internal/test" - "github.com/forceu/gokapi/internal/test/testconfiguration" "io" "mime/multipart" "net/http" @@ -19,6 +12,14 @@ import ( "strconv" "testing" "time" + + "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/storage" + "github.com/forceu/gokapi/internal/test" + "github.com/forceu/gokapi/internal/test/testconfiguration" ) func TestMain(m *testing.M) { @@ -534,65 +535,7 @@ func TestUserModify(t *testing.T) { retrievedUser, ok := database.GetUserByName("ToModify") test.IsEqualBool(t, ok, true) test.IsEqualBool(t, retrievedUser.Id != idUser, true) - systemKeyId := GetSystemKey(retrievedUser.Id) - systemKey, ok := database.GetApiKey(systemKeyId) test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, systemKey.HasPermissionReplace(), false) - test.IsEqualBool(t, systemKey.HasPermissionManageUsers(), false) - - for permissionUint, permissionString := range getUserPermMap(t) { - test.IsEqualBool(t, retrievedUser.HasPermission(permissionUint), false) - testUserModifyCall(t, apiKey.Id, retrievedUser.Id, permissionString, true) - retrievedUser, ok = database.GetUserByName("ToModify") - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, retrievedUser.HasPermission(permissionUint), true) - if permissionUint == models.UserPermReplaceUploads || permissionUint == models.UserPermManageUsers { - affectedPermission := getAffectedApiPerm(t, permissionUint) - systemKey, ok = database.GetApiKey(systemKeyId) - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, systemKey.HasPermission(affectedPermission), true) - key := models.ApiKey{ - Id: idNewKey, - PublicId: idNewKey, - Permissions: models.ApiPermNone, - UserId: retrievedUser.Id, - } - key.GrantPermission(affectedPermission) - database.SaveApiKey(key) - newKey, ok := database.GetApiKey(idNewKey) - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, newKey.HasPermission(affectedPermission), true) - } - - testUserModifyCall(t, apiKey.Id, retrievedUser.Id, permissionString, false) - retrievedUser, ok = database.GetUserByName("ToModify") - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, retrievedUser.HasPermission(permissionUint), false) - if permissionUint == models.UserPermReplaceUploads || permissionUint == models.UserPermManageUsers { - affectedPermission := getAffectedApiPerm(t, permissionUint) - newKey, ok := database.GetApiKey(idNewKey) - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, newKey.HasPermission(affectedPermission), false) - systemKey, ok = database.GetApiKey(systemKeyId) - test.IsEqualBool(t, systemKey.HasPermission(affectedPermission), false) - } - } - database.DeleteApiKey(systemKeyId) - - defer test.ExpectPanic(t) - apiModifyUser(nil, ¶mAuthCreate{}, models.User{Id: 7}) -} - -func getAffectedApiPerm(t *testing.T, permission models.UserPermission) models.ApiPermission { - switch permission { - case models.UserPermManageUsers: - return models.ApiPermManageUsers - case models.UserPermReplaceUploads: - return models.ApiPermReplace - default: - t.Errorf("Invalid permission %d", permission) - return models.ApiPermNone - } } func TestUserPasswordReset(t *testing.T) { @@ -856,62 +799,6 @@ func getUserPermMap(t *testing.T) map[models.UserPermission]string { return result } -func TestGetSystemKey(t *testing.T) { - keys := database.GetAllApiKeys() - for _, key := range keys { - if key.IsSystemKey { - t.Error("No system key expected, but found") - } - } - systemKey := GetSystemKey(5) - retrievedSystemKey, ok := database.GetApiKey(systemKey) - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, retrievedSystemKey.IsSystemKey, true) - test.IsEqualBool(t, retrievedSystemKey.Permissions == models.ApiPermAll, true) - test.IsEqualBool(t, retrievedSystemKey.Expiry > time.Now().Add(time.Hour*47).Unix(), true) - newKey := GetSystemKey(5) - test.IsEqualBool(t, systemKey == newKey, true) - retrievedSystemKey.Expiry = time.Now().Add(time.Hour * 23).Unix() - database.SaveApiKey(retrievedSystemKey) - newKey = GetSystemKey(5) - test.IsEqualBool(t, systemKey != newKey, true) - - newUser := models.User{ - Id: 70, - Name: "TestNoUser", - Permissions: models.UserPermissionAll, - UserLevel: models.UserLevelUser, - LastOnline: 0, - } - newUser.RemovePermission(models.UserPermManageUsers) - database.SaveUser(newUser, false) - newUser = models.User{ - Id: 71, - Name: "TestNoReplace", - Permissions: models.UserPermissionAll, - UserLevel: models.UserLevelUser, - LastOnline: 0, - } - newUser.RemovePermission(models.UserPermReplaceUploads) - database.SaveUser(newUser, false) - - newKey = GetSystemKey(70) - systemApiKey, ok := database.GetApiKey(newKey) - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, systemApiKey.HasPermissionEdit(), true) - test.IsEqualBool(t, systemApiKey.HasPermissionManageUsers(), false) - test.IsEqualBool(t, systemApiKey.HasPermissionReplace(), true) - newKey = GetSystemKey(71) - systemApiKey, ok = database.GetApiKey(newKey) - test.IsEqualBool(t, ok, true) - test.IsEqualBool(t, systemApiKey.HasPermissionEdit(), true) - test.IsEqualBool(t, systemApiKey.HasPermissionManageUsers(), true) - test.IsEqualBool(t, systemApiKey.HasPermissionReplace(), false) - - defer test.ExpectPanic(t) - GetSystemKey(idInvalidUser) -} - func grantUserPermission(t *testing.T, userId int, permission models.UserPermission) { user, ok := database.GetUser(userId) test.IsEqualBool(t, ok, true) diff --git a/internal/webserver/api/routing.go b/internal/webserver/api/routing.go index eebcce9..71e21cf 100644 --- a/internal/webserver/api/routing.go +++ b/internal/webserver/api/routing.go @@ -4,12 +4,13 @@ import ( "encoding/base64" "encoding/json" "errors" - "github.com/forceu/gokapi/internal/models" - "github.com/forceu/gokapi/internal/storage" - "github.com/forceu/gokapi/internal/storage/chunking" "net/http" "strconv" "strings" + + "github.com/forceu/gokapi/internal/models" + "github.com/forceu/gokapi/internal/storage" + "github.com/forceu/gokapi/internal/storage/chunking" ) type apiRoute struct { @@ -323,26 +324,11 @@ type paramAuthModify struct { } 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 - case "PERM_MANAGE_LOGS": - p.Permission = models.ApiPermManageLogs - default: - return errors.New("invalid permission") + permission, err := models.ApiPermissionFromString(p.permissionRaw) + if err != nil { + return err } + p.Permission = permission switch strings.ToUpper(p.permissionModifier) { case "GRANT": p.GrantPermission = true diff --git a/internal/webserver/authentication/tokengeneration/TokenGeneration.go b/internal/webserver/authentication/tokengeneration/TokenGeneration.go new file mode 100644 index 0000000..d54d3d4 --- /dev/null +++ b/internal/webserver/authentication/tokengeneration/TokenGeneration.go @@ -0,0 +1,39 @@ +package tokengeneration + +import ( + "errors" + "time" + + "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/api" +) + +func containsApiPermission(requestedPermissions models.ApiPermission, containsPermission models.ApiPermission) bool { + return containsPermission&requestedPermissions == containsPermission +} + +func Generate(user models.User, permission models.ApiPermission) (string, int64, error) { + if containsApiPermission(permission, models.ApiPermReplace) && !user.HasPermissionReplace() { + return "", 0, errors.New("user does not have permission to generate a token with PERM_REPLACE") + } + if containsApiPermission(permission, models.ApiPermManageUsers) && !user.HasPermissionManageUsers() { + return "", 0, errors.New("user does not have permission to generate a token with PERM_MANAGE_USERS") + } + if containsApiPermission(permission, models.ApiPermManageLogs) && !user.HasPermissionManageLogs() { + return "", 0, errors.New("user does not have permission to generate a token with PERM_MANAGE_LOGS") + } + + key := models.ApiKey{ + Id: helper.GenerateRandomString(api.LengthApiKey), + PublicId: helper.GenerateRandomString(api.LengthPublicId), + FriendlyName: "Temporary Token", + Permissions: permission, + IsSystemKey: true, + UserId: user.Id, + Expiry: time.Now().Add(time.Minute * 5).Unix(), + } + database.SaveApiKey(key) + return key.Id, key.Expiry, nil +} diff --git a/internal/webserver/web/static/apidocumentation/openapi.json b/internal/webserver/web/static/apidocumentation/openapi.json index 3b5193f..5436647 100644 --- a/internal/webserver/web/static/apidocumentation/openapi.json +++ b/internal/webserver/web/static/apidocumentation/openapi.json @@ -825,7 +825,7 @@ "explode": false, "schema": { "type": "string", - "enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_REPLACE", "PERM_MANAGE_USERS", "PERM_API_MOD"] + "enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_REPLACE", "PERM_MANAGE_LOGS", "PERM_MANAGE_USERS", "PERM_API_MOD"] } }, { diff --git a/internal/webserver/web/static/js/admin_api.js b/internal/webserver/web/static/js/admin_api.js index f2ae8c8..829a9d4 100644 --- a/internal/webserver/web/static/js/admin_api.js +++ b/internal/webserver/web/static/js/admin_api.js @@ -2,16 +2,70 @@ // All files named admin_*.js will be merged together and minimised by calling // go generate ./... + +const storedTokens = new Map(); + +async function getToken(permission, forceRenewal) { + const apiUrl = './auth/token'; + + if (!forceRenewal) { + if (!storedTokens.has(permission)) { + return getToken(permission, true); + } + let token = storedTokens.get(permission); + if (token.expiry - (Date.now() / 1000) < 60) { + return getToken(permission, true); + } + return token.key; + } + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'permission': permission + + }, + }; + try { + const response = await fetch(apiUrl, requestOptions); + if (!response.ok) { + throw new Error(`Request failed with status: ${response.status}`); + } + const data = await response.json(); + if (!data.hasOwnProperty("key")) { + throw new Error(`Invalid response when trying to get token`); + } + storedTokens.set(permission, { + key: data.key, + expiry: data.expiry + }); + return data.key; + } catch (error) { + console.error("Error in getToken:", error); + throw error; + } +} + // /auth async function apiAuthModify(apiKey, permission, modifier) { const apiUrl = './api/auth/modify'; + const reqPerm = 'PERM_API_MOD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'targetKey': apiKey, 'permission': permission, 'permissionModifier': modifier @@ -33,12 +87,22 @@ async function apiAuthModify(apiKey, permission, modifier) { async function apiAuthFriendlyName(apiKey, newName) { const apiUrl = './api/auth/friendlyname'; + const reqPerm = 'PERM_API_MOD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'targetKey': apiKey, 'friendlyName': newName @@ -59,12 +123,22 @@ async function apiAuthFriendlyName(apiKey, newName) { async function apiAuthDelete(apiKey) { const apiUrl = './api/auth/delete'; + const reqPerm = 'PERM_API_MOD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'targetKey': apiKey, }, }; @@ -83,12 +157,22 @@ async function apiAuthDelete(apiKey) { async function apiAuthCreate() { const apiUrl = './api/auth/create'; + const reqPerm = 'PERM_API_MOD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'basicPermissions': 'true' }, }; @@ -114,12 +198,22 @@ async function apiAuthCreate() { async function apiChunkComplete(uuid, filename, filesize, realsize, contenttype, allowedDownloads, expiryDays, password, isE2E, nonblocking) { const apiUrl = './api/chunk/complete'; + const reqPerm = 'PERM_UPLOAD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'uuid': uuid, 'filename': 'base64:' + Base64.encode(filename), 'filesize': filesize, @@ -163,13 +257,23 @@ async function apiChunkComplete(uuid, filename, filesize, realsize, contenttype, async function apiFilesReplace(id, newId) { const apiUrl = './api/files/replace'; + const reqPerm = 'PERM_REPLACE'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', 'id': id, - 'apikey': systemKey, + 'apikey': token, 'idNewContent': newId, 'deleteNewFile': false }, @@ -190,11 +294,22 @@ async function apiFilesReplace(id, newId) { async function apiFilesListById(fileId) { const apiUrl = './api/files/list/' + fileId; + const reqPerm = 'PERM_VIEW'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } + const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, }, }; @@ -215,13 +330,23 @@ async function apiFilesListById(fileId) { async function apiFilesModify(id, allowedDownloads, expiry, password, originalPw) { const apiUrl = './api/files/modify'; + const reqPerm = 'PERM_EDIT'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'PUT', headers: { 'Content-Type': 'application/json', 'id': id, - 'apikey': systemKey, + 'apikey': token, 'allowedDownloads': allowedDownloads, 'expiryTimestamp': expiry, 'password': password, @@ -245,12 +370,22 @@ async function apiFilesModify(id, allowedDownloads, expiry, password, originalPw async function apiFilesDelete(id, delay) { const apiUrl = './api/files/delete'; + const reqPerm = 'PERM_DELETE'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'id': id, 'delay': delay }, @@ -270,12 +405,22 @@ async function apiFilesDelete(id, delay) { async function apiFilesRestore(id) { const apiUrl = './api/files/restore'; + const reqPerm = 'PERM_DELETE'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'id': id }, }; @@ -300,12 +445,22 @@ async function apiFilesRestore(id) { async function apiUserCreate(userName) { const apiUrl = './api/user/create'; + const reqPerm = 'PERM_MANAGE_USERS'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'username': userName }, @@ -330,12 +485,22 @@ async function apiUserCreate(userName) { async function apiUserModify(userId, permission, modifier) { const apiUrl = './api/user/modify'; + const reqPerm = 'PERM_MANAGE_USERS'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'userid': userId, 'userpermission': permission, 'permissionModifier': modifier @@ -357,12 +522,22 @@ async function apiUserModify(userId, permission, modifier) { async function apiUserChangeRank(userId, newRank) { const apiUrl = './api/user/changeRank'; + const reqPerm = 'PERM_MANAGE_USERS'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'userid': userId, 'newRank': newRank @@ -382,12 +557,22 @@ async function apiUserChangeRank(userId, newRank) { async function apiUserDelete(id, deleteFiles) { const apiUrl = './api/user/delete'; + const reqPerm = 'PERM_MANAGE_USERS'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'userid': id, 'deleteFiles': deleteFiles }, @@ -408,12 +593,22 @@ async function apiUserDelete(id, deleteFiles) { async function apiUserResetPassword(id, generatePw) { const apiUrl = './api/user/resetPassword'; + const reqPerm = 'PERM_MANAGE_USERS'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'userid': id, 'generateNewPassword': generatePw }, @@ -436,12 +631,22 @@ async function apiUserResetPassword(id, generatePw) { async function apiLogsDelete(timestamp) { const apiUrl = './api/logs/delete'; + const reqPerm = 'PERM_MANAGE_LOGS'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey, + 'apikey': token, 'timestamp': timestamp }, }; @@ -462,12 +667,22 @@ async function apiLogsDelete(timestamp) { async function apiE2eGet() { const apiUrl = './api/e2e/get'; + const reqPerm = 'PERM_UPLOAD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey + 'apikey': token }, }; @@ -487,12 +702,22 @@ async function apiE2eGet() { async function apiE2eStore(content) { const apiUrl = './api/e2e/set'; + const reqPerm = 'PERM_UPLOAD'; + + let token; + + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'apikey': systemKey + 'apikey': token }, body: JSON.stringify({ content: content diff --git a/internal/webserver/web/static/js/min/admin.min.13.js b/internal/webserver/web/static/js/min/admin.min.14.js similarity index 65% rename from internal/webserver/web/static/js/min/admin.min.13.js rename to internal/webserver/web/static/js/min/admin.min.14.js index 66fef52..91cfa53 100644 --- a/internal/webserver/web/static/js/min/admin.min.13.js +++ b/internal/webserver/web/static/js/min/admin.min.14.js @@ -1,4 +1,4 @@ -async function apiAuthModify(e,t,n){const s="./api/auth/modify",o={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,targetKey:e,permission:t,permissionModifier:n}};try{const e=await fetch(s,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,t){const n="./api/auth/friendlyname",s={method:"PUT",headers:{"Content-Type":"application/json",apikey:systemKey,targetKey:e,friendlyName:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const t="./api/auth/delete",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,targetKey:e}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const e="./api/auth/create",t={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,basicPermissions:"true"}};try{const n=await fetch(e,t);if(!n.ok)throw new Error(`Request failed with status: ${n.status}`);const s=await n.json();return s}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,t,n,s,o,i,a,r,c,l){const d="./api/chunk/complete",u={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,uuid:e,filename:"base64:"+Base64.encode(t),filesize:n,realsize:s,contenttype:o,allowedDownloads:i,expiryDays:a,password:r,isE2E:c,nonblocking:l}};try{const e=await fetch(d,u);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}const t=await e.json();return t}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,t){const n="./api/files/replace",s={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:systemKey,idNewContent:t,deleteNewFile:!1}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const t="./api/files/list/"+e,n={method:"GET",headers:{"Content-Type":"application/json",apikey:systemKey}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const s=await e.json();return s}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesModify(e,t,n,s,o){const i="./api/files/modify",a={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:systemKey,allowedDownloads:t,expiryTimestamp:n,password:s,originalPassword:o}};try{const e=await fetch(i,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,t){const n="./api/files/delete",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,id:e,delay:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const t="./api/files/restore",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,id:e}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const s=await e.json();return s}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const t="./api/user/create",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,username:e}};try{const e=await fetch(t,n);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(`Request failed with status: ${e.status}`);const s=await e.json();return s}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,t,n){const s="./api/user/modify",o={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,userpermission:t,permissionModifier:n}};try{const e=await fetch(s,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,t){const n="./api/user/changeRank",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,newRank:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,t){const n="./api/user/delete",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,deleteFiles:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,t){const n="./api/user/resetPassword",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,generateNewPassword:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogsDelete(e){const t="./api/logs/delete",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,timestamp:e}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const e="./api/e2e/get",t={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey}};try{const n=await fetch(e,t);if(!n.ok)throw new Error(`Request failed with status: ${n.status}`);return await n.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eStore(e){const t="./api/e2e/set",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey},body:JSON.stringify({content:e})};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiE2eStore:",e),e}}try{var toastId,dropzoneObject,isE2EEnabled,isUploading,rowCount,calendarInstance,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}function changeApiPermission(e,t,n){var o,i,s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;o=s.classList.contains("perm-granted"),s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted"),i="GRANT",o&&(i="REVOKE"),apiAuthModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete API key: "+e),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert("Unable to create API key: "+e),console.error("Error:",e)})}function addFriendlyNameChange(e){let t=document.getElementById("friendlyname-"+e);if(t.classList.contains("isBeingEdited"))return;t.classList.add("isBeingEdited");let i=t.innerText,n=document.createElement("input");n.size=5,n.value=i;let s=!0,o=function(){if(!s)return;s=!1;let o=n.value;o==""&&(o="Unnamed key"),t.innerText=o,t.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,o).catch(e=>{alert("Unable to save name: "+e),console.error("Error:",e)})};n.onblur=o,n.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),o())}),t.innerText="",t.appendChild(n),n.focus()}function addRowApi(e,t){let p=document.getElementById("apitable"),s=p.insertRow(0);s.id="row-"+t;let i=0,c=s.insertCell(i++),l=s.insertCell(i++),d=s.insertCell(i++),a=s.insertCell(i++),u;canViewOtherApiKeys&&(u=s.insertCell(i++));let h=s.insertCell(i++);canViewOtherApiKeys&&(u.classList.add("newApiKey"),u.innerText=userName),c.classList.add("newApiKey"),l.classList.add("newApiKey"),d.classList.add("newApiKey"),a.classList.add("newApiKey"),a.classList.add("prevent-select"),h.classList.add("newApiKey"),c.innerText="Unnamed key",c.id="friendlyname-"+t,c.onclick=function(){addFriendlyNameChange(t)},l.innerText=e,l.classList.add("font-monospace"),d.innerText="Never";const r=document.createElement("div");r.className="btn-group",r.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.dataset.clipboardText=e,n.title="Copy API Key",n.className="copyurl btn btn-outline-light btn-sm",n.setAttribute("onclick","showToast(1000)");const m=document.createElement("i");m.className="bi bi-copy",n.appendChild(m);const o=document.createElement("button");o.type="button",o.id=`delete-${t}`,o.title="Delete",o.className="btn btn-outline-danger btn-sm",o.setAttribute("onclick",`deleteApiKey('${t}')`);const f=document.createElement("i");f.className="bi bi-trash3",o.appendChild(f),r.appendChild(n),r.appendChild(o),h.appendChild(r);const g=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:"List Uploads"},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-arrow-up",granted:!0,title:"Upload"},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:"Edit Uploads"},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:"Delete Uploads"},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:"Replace Uploads"},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:"Manage Users"},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:"Manage System Logs"},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:"Manage API Keys"}];if(g.forEach(({perm:e,icon:n,granted:s,title:o})=>{const i=document.createElement("i"),r=`${e.toLowerCase()}_${t}`;i.id=r,i.className=`bi ${n} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${t}","${e}", "${r}");`),a.appendChild(i),a.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_manage_users_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canViewSystemLog){let e=document.getElementById("perm_manage_logs_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{c.classList.remove("newApiKey"),l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),a.classList.remove("newApiKey"),h.classList.remove("newApiKey")},700)}function filterLogs(e){e=="all"?textarea.value=logContent:textarea.value=logContent.split(` +const storedTokens=new Map;async function getToken(e,t){const n="./auth/token";if(!t){if(!storedTokens.has(e))return getToken(e,!0);let t=storedTokens.get(e);return t.expiry-Date.now()/1e3<60?getToken(e,!0):t.key}const s={method:"POST",headers:{"Content-Type":"application/json",permission:e}};try{const o=await fetch(n,s);if(!o.ok)throw new Error(`Request failed with status: ${o.status}`);const t=await o.json();if(!t.hasOwnProperty("key"))throw new Error(`Invalid response when trying to get token`);return storedTokens.set(e,{key:t.key,expiry:t.expiry}),t.key}catch(e){throw console.error("Error in getToken:",e),e}}async function apiAuthModify(e,t,n){const o="./api/auth/modify",i="PERM_API_MOD";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,targetKey:e,permission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,t){const s="./api/auth/friendlyname",o="PERM_API_MOD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",apikey:n,targetKey:e,friendlyName:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const n="./api/auth/delete",s="PERM_API_MOD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,targetKey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const t="./api/auth/create",n="PERM_API_MOD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e,basicPermissions:"true"}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,t,n,s,o,i,a,r,c,l){const u="./api/chunk/complete",h="PERM_UPLOAD";let d;try{d=await getToken(h,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const m={method:"POST",headers:{"Content-Type":"application/json",apikey:d,uuid:e,filename:"base64:"+Base64.encode(t),filesize:n,realsize:s,contenttype:o,allowedDownloads:i,expiryDays:a,password:r,isE2E:c,nonblocking:l}};try{const e=await fetch(u,m);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}const t=await e.json();return t}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,t){const s="./api/files/replace",o="PERM_REPLACE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:n,idNewContent:t,deleteNewFile:!1}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const n="./api/files/list/"+e,s="PERM_VIEW";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesModify(e,t,n,s,o){const a="./api/files/modify",r="PERM_EDIT";let i;try{i=await getToken(r,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const c={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:i,allowedDownloads:t,expiryTimestamp:n,password:s,originalPassword:o}};try{const e=await fetch(a,c);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,t){const s="./api/files/delete",o="PERM_DELETE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,id:e,delay:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const n="./api/files/restore",s="PERM_DELETE";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const n="./api/user/create",s="PERM_MANAGE_USERS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,username:e}};try{const e=await fetch(n,o);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,t,n){const o="./api/user/modify",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,userpermission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,t){const s="./api/user/changeRank",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,newRank:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,t){const s="./api/user/delete",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,deleteFiles:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,t){const s="./api/user/resetPassword",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,generateNewPassword:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogsDelete(e){const n="./api/logs/delete",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const t="./api/e2e/get",n="PERM_UPLOAD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eStore(e){const n="./api/e2e/set",s="PERM_UPLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t},body:JSON.stringify({content:e})};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiE2eStore:",e),e}}try{var toastId,dropzoneObject,isE2EEnabled,isUploading,rowCount,calendarInstance,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}function changeApiPermission(e,t,n){var o,i,s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;o=s.classList.contains("perm-granted"),s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted"),i="GRANT",o&&(i="REVOKE"),apiAuthModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete API key: "+e),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert("Unable to create API key: "+e),console.error("Error:",e)})}function addFriendlyNameChange(e){let t=document.getElementById("friendlyname-"+e);if(t.classList.contains("isBeingEdited"))return;t.classList.add("isBeingEdited");let i=t.innerText,n=document.createElement("input");n.size=5,n.value=i;let s=!0,o=function(){if(!s)return;s=!1;let o=n.value;o==""&&(o="Unnamed key"),t.innerText=o,t.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,o).catch(e=>{alert("Unable to save name: "+e),console.error("Error:",e)})};n.onblur=o,n.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),o())}),t.innerText="",t.appendChild(n),n.focus()}function addRowApi(e,t){let p=document.getElementById("apitable"),s=p.insertRow(0);s.id="row-"+t;let i=0,c=s.insertCell(i++),l=s.insertCell(i++),d=s.insertCell(i++),a=s.insertCell(i++),u;canViewOtherApiKeys&&(u=s.insertCell(i++));let h=s.insertCell(i++);canViewOtherApiKeys&&(u.classList.add("newApiKey"),u.innerText=userName),c.classList.add("newApiKey"),l.classList.add("newApiKey"),d.classList.add("newApiKey"),a.classList.add("newApiKey"),a.classList.add("prevent-select"),h.classList.add("newApiKey"),c.innerText="Unnamed key",c.id="friendlyname-"+t,c.onclick=function(){addFriendlyNameChange(t)},l.innerText=e,l.classList.add("font-monospace"),d.innerText="Never";const r=document.createElement("div");r.className="btn-group",r.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.dataset.clipboardText=e,n.title="Copy API Key",n.className="copyurl btn btn-outline-light btn-sm",n.setAttribute("onclick","showToast(1000)");const m=document.createElement("i");m.className="bi bi-copy",n.appendChild(m);const o=document.createElement("button");o.type="button",o.id=`delete-${t}`,o.title="Delete",o.className="btn btn-outline-danger btn-sm",o.setAttribute("onclick",`deleteApiKey('${t}')`);const f=document.createElement("i");f.className="bi bi-trash3",o.appendChild(f),r.appendChild(n),r.appendChild(o),h.appendChild(r);const g=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:"List Uploads"},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-arrow-up",granted:!0,title:"Upload"},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:"Edit Uploads"},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:"Delete Uploads"},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:"Replace Uploads"},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:"Manage Users"},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:"Manage System Logs"},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:"Manage API Keys"}];if(g.forEach(({perm:e,icon:n,granted:s,title:o})=>{const i=document.createElement("i"),r=`${e.toLowerCase()}_${t}`;i.id=r,i.className=`bi ${n} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${t}","${e}", "${r}");`),a.appendChild(i),a.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_manage_users_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canViewSystemLog){let e=document.getElementById("perm_manage_logs_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{c.classList.remove("newApiKey"),l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),a.classList.remove("newApiKey"),h.classList.remove("newApiKey")},700)}function filterLogs(e){e=="all"?textarea.value=logContent:textarea.value=logContent.split(` `).filter(t=>t.includes("["+e+"]")).join(` `),textarea.scrollTop=textarea.scrollHeight}function deleteLogs(e){if(e=="none")return;if(!confirm("Do you want to delete the selected logs?")){document.getElementById("deleteLogs").selectedIndex=0;return}let t=Math.floor(Date.now()/1e3);switch(e){case"all":t=0;break;case"2":t=t-2*24*60*60;break;case"7":t=t-7*24*60*60;break;case"14":t=t-14*24*60*60;break;case"30":t=t-30*24*60*60;break}apiLogsDelete(t).then(e=>{location.reload()}).catch(e=>{alert("Unable to delete logs: "+e),console.error("Error:",e)})}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"Drop files, paste or click here to upload",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,t,n){n&&n.status===413?showError(e,"File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB."):showError(e,"Error: "+t)}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),dropzoneObject.options.dictDefaultMessage="Loading end-to-end encryption...",document.getElementsByClassName("dz-button")[0].innerText="Loading end-to-end encryption...",setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue="Upload is still in progress. Do you want to close this page?")})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,t){let c=e.upload.uuid,n=e.name,s=e.size,l=e.size,o=e.type,i=document.getElementById("allowedDownloads").value,a=document.getElementById("expiryDays").value,d=document.getElementById("password").value,r=e.isEndToEndEncrypted===!0,u=!0;document.getElementById("enableDownloadLimit").checked||(i=0),document.getElementById("enableTimeLimit").checked||(a=0),r&&(s=e.sizeEncrypted,n="Encrypted File",o=""),apiChunkComplete(c,n,s,l,o,i,a,d,r,u).then(n=>{t();let s=document.getElementById(`us-progress-info-${e.upload.uuid}`);s!=null&&(s.innerText="In Queue...")}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n);let s=dropzoneGetFile(t);if(s==null)return;if(s.isEndToEndEncrypted===!0){try{let o=GokapiE2EAddFile(t,e,s.name);if(o instanceof Error)throw o;let n=GokapiE2EInfoEncrypt();if(n instanceof Error)throw n;storeE2EInfo(n)}catch(e){s.accepted=!1,dropzoneObject._errorProcessing([s],e);return}GokapiE2EDecryptMenu()}removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let n=document.getElementById(`us-container-${e.chunk_id}`);if(n==null)return;n.setAttribute("data-complete","true");let t;switch(e.upload_status){case 0:t="Processing file...";break;case 1:t="Uploading file...";break;case 2:t="Finalising...",requestFileInfo(e.file_id,e.chunk_id);break;case 3:t="Error";let n=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message="Server Error"),n!=null&&dropzoneUploadError(n,e.error_message);return;default:t="Unknown status";break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=t}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let s=e.getAttribute("data-fileid"),o=document.getElementById("mi_edit_down").value,i=document.getElementById("mi_edit_expiry").value,t=document.getElementById("mi_edit_pw").value,a=t==="(unchanged)";document.getElementById("mc_download").checked||(o=0),document.getElementById("mc_expiry").checked||(i=0),document.getElementById("mc_password").checked||(a=!1,t="");let r=!1,n="";document.getElementById("mc_replace").checked&&(n=document.getElementById("mi_edit_replace").value,r=n!=""),apiFilesModify(s,o,i,t,a).then(t=>{if(!r){location.reload();return}apiFilesReplace(s,n).then(e=>{location.reload()}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}calendarInstance=null;function createCalendar(e){const t=new Date(e*1e3);calendarInstance=flatpickr("#mi_edit_expiry",{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:t,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function showEditModal(e,t,n,s,o,i,a,r,c){let d=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=d.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",s),document.getElementById("mb_save").setAttribute("data-fileid",t),createCalendar(s),i?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=n,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),a?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(s),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=s,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),o?(document.getElementById("mi_edit_pw").value="(unchanged)",document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let l=document.getElementById("mi_edit_replace");if(c)if(document.getElementById("replaceGroup").style.display="flex",r)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title="Replacing content is not available for end-to-end encrypted files",l.add(new Option("Unavailable",0)),l.title="Replacing content is not available for end-to-end encrypted files",l.value="0";else{let e=getAllAvailableFiles();for(let n=0;n{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e)}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}function registerChangeHandler(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE..."),setTimeout(registerChangeHandler,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let d=document.getElementById("downloadtable"),t=d.insertRow(0);e.Id=sanitizeId(e.Id),t.id="row-"+e.Id;let i=t.insertCell(0),a=t.insertCell(1),s=t.insertCell(2),r=t.insertCell(3),c=t.insertCell(4),o=t.insertCell(5),l=t.insertCell(6);i.innerText=e.Name,i.id="cell-name-"+e.Id,c.id="cell-downloads-"+e.Id,a.innerText=e.Size,e.UnlimitedDownloads?s.innerText="Unlimited":(s.innerText=e.DownloadsRemaining,s.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?r.innerText="Unlimited":r.innerText=e.ExpireAtString,c.innerText=e.DownloadCount;const n=document.createElement("a");if(n.href=e.UrlDownload,n.target="_blank",n.style.color="inherit",n.id="url-href-"+e.Id,n.textContent=e.Id,o.appendChild(n),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title="Password protected",o.appendChild(document.createTextNode(" ")),o.appendChild(e)}return l.appendChild(createButtonGroup(e)),i.classList.add("newItem"),a.classList.add("newItem"),s.classList.add("newItem"),r.classList.add("newItem"),c.classList.add("newItem"),o.classList.add("newItem"),l.classList.add("newItem"),a.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,t),e.Id}function createButtonGroup(e){const h=document.createElement("div");h.className="btn-toolbar",h.setAttribute("role","toolbar");const t=document.createElement("div");t.className="btn-group me-2",t.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.className="copyurl btn btn-outline-light btn-sm",n.dataset.clipboardText=e.UrlDownload,n.id="url-button-"+e.Id,n.title="Copy URL";const j=document.createElement("i");j.className="bi bi-copy",n.appendChild(j),n.appendChild(document.createTextNode(" URL")),n.addEventListener("click",()=>{showToast(1e3)}),t.appendChild(n);const m=document.createElement("button");m.type="button",m.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",m.setAttribute("data-bs-toggle","dropdown"),m.setAttribute("aria-expanded","false"),t.appendChild(m);const f=document.createElement("ul");f.className="dropdown-menu dropdown-menu-end",f.setAttribute("data-bs-theme","dark");const g=document.createElement("li"),s=document.createElement("a");e.UrlHotlink!==""?(s.className="dropdown-item copyurl",s.title="Copy hotlink",s.style.cursor="pointer",s.setAttribute("data-clipboard-text",e.UrlHotlink),s.onclick=()=>showToast(1e3),s.innerHTML=` Hotlink`):(s.className="dropdown-item",s.innerText="Hotlink not available"),g.appendChild(s),f.appendChild(g),t.appendChild(f);const r=document.createElement("button");r.type="button",r.className="btn btn-outline-light btn-sm",r.title="Share",r.onclick=()=>shareUrl(e.Id),r.innerHTML=``,t.appendChild(r);const d=document.createElement("button");d.type="button",d.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",d.setAttribute("data-bs-toggle","dropdown"),d.setAttribute("aria-expanded","false"),t.appendChild(d);const u=document.createElement("ul");u.className="dropdown-menu dropdown-menu-end",u.setAttribute("data-bs-theme","dark");const p=document.createElement("li"),a=document.createElement("a");a.className="dropdown-item",a.id=`qrcode-${e.Id}`,a.style.cursor="pointer",a.title="Open QR Code",a.onclick=()=>showQrCode(e.UrlDownload),a.innerHTML=` QR Code`,p.appendChild(a),u.appendChild(p);const v=document.createElement("li"),i=document.createElement("a");i.className="dropdown-item",i.title="Share via email",i.id=`email-${e.Id}`,i.target="_blank",i.href=`mailto:?body=${encodeURIComponent(e.UrlDownload)}`,i.innerHTML=` Email`,v.appendChild(i),u.appendChild(v),t.appendChild(u);const l=document.createElement("div");l.className="btn-group me-2",l.setAttribute("role","group");const c=document.createElement("button");c.type="button",c.className="btn btn-outline-light btn-sm",c.title="Edit";const b=document.createElement("i");b.className="bi bi-pencil",c.appendChild(b),c.addEventListener("click",()=>{showEditModal(e.Name,e.Id,e.DownloadsRemaining,e.ExpireAt,e.IsPasswordProtected,e.UnlimitedDownloads,e.UnlimitedTime,e.IsEndToEndEncrypted,canReplaceOwnFiles)}),l.appendChild(c);const o=document.createElement("button");o.type="button",o.className="btn btn-outline-danger btn-sm",o.title="Delete",o.id="button-delete-"+e.Id;const y=document.createElement("i");return y.className="bi bi-trash3",o.appendChild(y),o.addEventListener("click",()=>{deleteFile(e.Id)}),l.appendChild(o),h.appendChild(t),h.appendChild(l),h}function sanitizeId(e){return e.replace(/[^a-zA-Z0-9]/g,"")}function changeRowCount(e,t){let n=$("#maintable").DataTable();rowCount==-1&&(rowCount=n.rows().count()),e?(rowCount=rowCount+1,n.row.add(t)):(rowCount=rowCount-1,t.classList.add("rowDeleting"),setTimeout(()=>{n.row(t).remove(),t.remove()},290));let s=document.getElementsByClassName("dataTables_empty")[0];typeof s!="undefined"?s.innerText="Files stored: "+rowCount:document.getElementsByClassName("dataTables_info")[0].innerText="Files stored: "+rowCount}function hideQrCode(){document.getElementById("qroverlay").style.display="none",document.getElementById("qrcode").innerHTML=""}function showQrCode(e){const t=document.getElementById("qroverlay");t.style.display="block",new QRCode(document.getElementById("qrcode"),{text:e,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H}),t.addEventListener("click",hideQrCode)}function showToastFileDeletion(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function hideFileToast(){document.getElementById("toastnotificationUndo").classList.remove("show")}function handleUndo(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{addRow(e.FileInfo)}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function shareUrl(e){if(!navigator.share)return;let t=document.getElementById("cell-name-"+e).innerText,n=document.getElementById("url-href-"+e).getAttribute("href");navigator.share({title:t,url:n})}function showDeprecationNotice(){let e=document.getElementById("toastDeprecation");e.classList.add("show"),setTimeout(()=>{e.classList.remove("show")},5e3)}function changeUserPermission(e,t,n){let s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;let o=s.classList.contains("perm-granted");s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted");let i="GRANT";o&&(i="REVOKE"),t=="PERM_REPLACE_OTHER"&&!o&&(hasNotPermissionReplace=document.getElementById("perm_replace_"+e).classList.contains("perm-notgranted"),hasNotPermissionReplace&&(showToast(2e3,"Also granting permission to replace own files"),changeUserPermission(e,"PERM_REPLACE","perm_replace_"+e))),t=="PERM_REPLACE"&&o&&(hasPermissionReplaceOthers=document.getElementById("perm_replace_other_"+e).classList.contains("perm-granted"),hasPermissionReplaceOthers&&(showToast(2e3,"Also revoking permission to replace files of other users"),changeUserPermission(e,"PERM_REPLACE_OTHER","perm_replace_other_"+e))),apiUserModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function changeRank(e,t,n){let s=document.getElementById(n);if(s.disabled)return;s.disabled=!0,apiUserChangeRank(e,t).then(e=>{location.reload()}).catch(e=>{s.disabled=!1,alert("Unable to change rank: "+e),console.error("Error:",e)})}function showDeleteModal(e,t){let n=document.getElementById("checkboxDelete");n.checked=!1,document.getElementById("deleteModalBody").innerText=t,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){apiUserDelete(e,n.checked).then(t=>{$("#deleteModal").modal("hide"),document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete user: "+e),console.error("Error:",e)})}}function showAddUserModal(){let e=$("#newUserModal").clone();$("#newUserModal").on("hide.bs.modal",function(){$("#newUserModal").remove();let t=e.clone();$("body").append(t)}),$("#newUserModal").modal("show")}function showResetPwModal(e,t){let n=$("#resetPasswordModal").clone();$("#resetPasswordModal").on("hide.bs.modal",function(){$("#resetPasswordModal").remove();let e=n.clone();$("body").append(e)}),document.getElementById("l_userpwreset").innerText=t;let s=document.getElementById("resetPasswordButton");s.onclick=function(){resetPw(e,document.getElementById("generateRandomPassword").checked)},$("#resetPasswordModal").modal("show")}function resetPw(e,t){let n=document.getElementById("resetPasswordButton");document.getElementById("resetPasswordButton").disabled=!0,apiUserResetPassword(e,t).then(e=>{if(!t){$("#resetPasswordModal").modal("hide"),showToast(1e3,"Password change requirement set successfully");return}n.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=e.password,document.getElementById("copypwclip").onclick=function(){navigator.clipboard.writeText(e.password),showToast(1e3,"Password copied to clipboard")}}).catch(e=>{alert("Unable to reset user password: "+e),console.error("Error:",e),n.disabled=!1})}function addNewUser(){let e=document.getElementById("mb_addUser");e.disabled=!0;let t=document.getElementById("newUserForm");if(t.checkValidity()){let t=document.getElementById("e_userName");apiUserCreate(t.value.trim()).then(e=>{$("#newUserModal").modal("hide"),addRowUser(e.id,e.name)}).catch(t=>{t.message=="duplicate"?(alert("A user already exists with that name"),e.disabled=!1):(alert("Unable to create user: "+t),console.error("Error:",t),e.disabled=!1)})}else t.classList.add("was-validated"),e.disabled=!1}function addRowUser(e,t){e=sanitizeUserId(e);let h=document.getElementById("usertable"),n=h.insertRow(1);n.id="row-"+e;let r=n.insertCell(0),c=n.insertCell(1),l=n.insertCell(2),d=n.insertCell(3),u=n.insertCell(4),a=n.insertCell(5);r.classList.add("newUser"),c.classList.add("newUser"),l.classList.add("newUser"),d.classList.add("newUser"),u.classList.add("newUser"),a.classList.add("newUser"),r.innerText=t,c.innerText="User",l.innerText="Never",d.innerText="0";const i=document.createElement("div");if(i.className="btn-group",i.setAttribute("role","group"),isInternalAuth){const n=document.createElement("button");n.id=`pwchange-${e}`,n.type="button",n.className="btn btn-outline-light btn-sm",n.title="Reset Password",n.onclick=()=>showResetPwModal(e,t),n.innerHTML=``,i.appendChild(n)}const s=document.createElement("button");s.id=`changeRank_${e}`,s.type="button",s.className="btn btn-outline-light btn-sm",s.title="Promote User",s.onclick=()=>changeRank(e,"ADMIN",`changeRank_${e}`),s.innerHTML=``,i.appendChild(s);const o=document.createElement("button");o.id=`delete-${e}`,o.type="button",o.className="btn btn-outline-danger btn-sm",o.title="Delete",o.onclick=()=>showDeleteModal(e,t),o.innerHTML=``,i.appendChild(o),a.innerHTML="",a.appendChild(i),u.innerHTML=` diff --git a/internal/webserver/web/templates/html_admin.tmpl b/internal/webserver/web/templates/html_admin.tmpl index 481faab..4639cc4 100644 --- a/internal/webserver/web/templates/html_admin.tmpl +++ b/internal/webserver/web/templates/html_admin.tmpl @@ -154,7 +154,6 @@ registerChangeHandler(); - var systemKey = "{{.SystemKey}}"; var canReplaceOwnFiles = {{.ActiveUser.HasPermissionReplace}}; setUploadDefaults(); diff --git a/internal/webserver/web/templates/html_api.tmpl b/internal/webserver/web/templates/html_api.tmpl index a3fb2c8..a038d88 100644 --- a/internal/webserver/web/templates/html_api.tmpl +++ b/internal/webserver/web/templates/html_api.tmpl @@ -71,7 +71,6 @@ @@ -122,9 +121,6 @@ document.getElementById("enterpwbutton").disabled = false; }); - - var systemKey = "{{.SystemKey}}"; - {{ end }} {{ template "pagename" "E2EGeneration"}} diff --git a/internal/webserver/web/templates/html_logs.tmpl b/internal/webserver/web/templates/html_logs.tmpl index da46242..460f5ac 100644 --- a/internal/webserver/web/templates/html_logs.tmpl +++ b/internal/webserver/web/templates/html_logs.tmpl @@ -39,7 +39,6 @@ {{ template "pagename" "OverviewUsers"}} diff --git a/internal/webserver/web/templates/string_constants.tmpl b/internal/webserver/web/templates/string_constants.tmpl index 475818a..6c32926 100644 --- a/internal/webserver/web/templates/string_constants.tmpl +++ b/internal/webserver/web/templates/string_constants.tmpl @@ -3,7 +3,7 @@ // Specifies the version of JS files, so that the browser doesn't // use a cached version, if the file has been updated -{{define "js_admin_version"}}13{{end}} +{{define "js_admin_version"}}14{{end}} {{define "js_dropzone_version"}}5{{end}} {{define "js_e2eversion"}}8{{end}} {{define "css_main"}}5{{end}} \ No newline at end of file diff --git a/openapi.json b/openapi.json index 3b5193f..5436647 100644 --- a/openapi.json +++ b/openapi.json @@ -825,7 +825,7 @@ "explode": false, "schema": { "type": "string", - "enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_REPLACE", "PERM_MANAGE_USERS", "PERM_API_MOD"] + "enum": ["PERM_VIEW", "PERM_UPLOAD", "PERM_EDIT", "PERM_DELETE", "PERM_REPLACE", "PERM_MANAGE_LOGS", "PERM_MANAGE_USERS", "PERM_API_MOD"] } }, {