Use API for most of UI interactions with an internal API key (#211), breaking API changes

* Changed color of API procressing status

* Added internal system key, use system key for API page perm changes, BREAKING: removed session auth

* API: Added /auth/delete, added option to include basic parameters in /auth/create, migrated API overview page completely to use API calls

* Added toast in API menu for clipboard, removed reference to session authentication in documentation

* Changed delete button in upload menu to API call, fixed modify API call in menu #210

* Set Api key in API menu for changing permissions, renamed title for Deleting Uploads permission, refactoring

* Added tests, refactoring
This commit is contained in:
Marc Ole Bulling
2024-12-05 13:55:11 +01:00
committed by GitHub
parent 5a155f7180
commit ddc72b0a98
28 changed files with 573 additions and 292 deletions

View File

@@ -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 = 5
const jsAdminVersion = 6
const jsE2EVersion = 3
const cssMainVersion = 2

View File

@@ -11,7 +11,7 @@ import (
"strings"
)
const versionJsAdmin = 5
const versionJsAdmin = 6
const versionJsDropzone = 4
const versionJsE2EAdmin = 3
const versionCssMain = 2

View File

@@ -220,7 +220,7 @@ Interacting with the API
============================
All API calls will need an API key as authentication or a valid admin session cookie. An API key can be generated in the web UI in the menu "API". The API key needs to be passed as a header.
All API calls will need an API key as authentication. An API key can be generated in the web UI in the menu "API". The API key needs to be passed as a header.
Example: Getting a list of all stored files with curl
::

View File

@@ -226,10 +226,7 @@ Only use this if you are running Gokapi behind a reverse proxy that is capable o
This option disables Gokapis internal authentication completely, except for API calls. The following URLs need to be restricted by the reverse proxy:
- ``/admin``
- ``/apiDelete``
- ``/apiKeys``
- ``/apiNew``
- ``/delete``
- ``/e2eInfo``
- ``/e2eSetup``
- ``/logs``

View File

@@ -130,6 +130,11 @@ func DeleteApiKey(id string) {
db.DeleteApiKey(id)
}
// GetSystemKey returns the latest UI API key
func GetSystemKey() (models.ApiKey, bool) {
return db.GetSystemKey()
}
// E2E Section
// SaveEnd2EndInfo stores the encrypted e2e info

View File

@@ -43,6 +43,8 @@ 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() (models.ApiKey, bool)
// SaveEnd2EndInfo stores the encrypted e2e info
SaveEnd2EndInfo(info models.E2EInfoEncrypted)

View File

@@ -41,9 +41,32 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
return apikey, true
}
// GetSystemKey returns the latest UI API key
func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
keys := p.GetAllApiKeys()
foundKey := ""
var latestExpiry int64
for _, key := range keys {
if !key.IsSystemKey {
continue
}
if key.Expiry > latestExpiry {
foundKey = key.Id
latestExpiry = key.Expiry
}
}
if foundKey == "" {
return models.ApiKey{}, false
}
return keys[foundKey], true
}
// SaveApiKey saves the API key to the database
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
p.setHashMap(p.buildArgs(prefixApiKeys + apikey.Id).AddFlat(apikey))
if apikey.Expiry != 0 {
p.setExpiryAt(prefixApiKeys+apikey.Id, apikey.Expiry)
}
}
// UpdateTimeApiKey writes the content of LastUsage to the database

View File

@@ -17,7 +17,7 @@ type DatabaseProvider struct {
sqliteDb *sql.DB
}
const DatabaseSchemeVersion = 3
const DatabaseSchemeVersion = 4
// New returns an instance
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -57,6 +57,14 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
ALTER TABLE "ApiKeys_New" RENAME TO "ApiKeys";`)
helper.Check(err)
}
// < v1.9.0
if currentDbVersion < 4 {
// Add Column LastUsedString, keeping old data
err := p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN "Expiry" INTEGER;`)
helper.Check(err)
err = p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN "IsSystemKey" INTEGER;`)
helper.Check(err)
}
}
// GetDbVersion gets the version number of the database
@@ -125,6 +133,7 @@ func (p DatabaseProvider) Close() {
func (p DatabaseProvider) RunGarbageCollection() {
p.cleanExpiredSessions()
p.cleanUploadStatus()
p.cleanApiKeys()
}
func (p DatabaseProvider) createNewDatabase() error {
@@ -133,6 +142,8 @@ func (p DatabaseProvider) createNewDatabase() error {
"FriendlyName" TEXT NOT NULL,
"LastUsed" INTEGER NOT NULL,
"Permissions" INTEGER NOT NULL DEFAULT 0,
"Expiry" INTEGER,
"IsSystemKey" INTEGER,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "E2EConfig" (

View File

@@ -12,24 +12,28 @@ type schemaApiKeys struct {
FriendlyName string
LastUsed int64
Permissions int
Expiry int64
IsSystemKey int
}
// GetAllApiKeys returns a map with all API keys
func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
result := make(map[string]models.ApiKey)
rows, err := p.sqliteDb.Query("SELECT * FROM ApiKeys")
rows, err := p.sqliteDb.Query("SELECT * FROM ApiKeys WHERE ApiKeys.Expiry == 0 OR ApiKeys.Expiry > ?", currentTime().Unix())
helper.Check(err)
defer rows.Close()
for rows.Next() {
rowData := schemaApiKeys{}
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions)
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions, &rowData.Expiry, &rowData.IsSystemKey)
helper.Check(err)
result[rowData.Id] = models.ApiKey{
Id: rowData.Id,
FriendlyName: rowData.FriendlyName,
LastUsed: rowData.LastUsed,
Permissions: uint8(rowData.Permissions),
Expiry: rowData.Expiry,
IsSystemKey: rowData.IsSystemKey == 1,
}
}
return result
@@ -39,7 +43,7 @@ func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE Id = ?", id)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.ApiKey{}, false
@@ -53,15 +57,45 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
FriendlyName: rowResult.FriendlyName,
LastUsed: rowResult.LastUsed,
Permissions: uint8(rowResult.Permissions),
Expiry: rowResult.Expiry,
IsSystemKey: rowResult.IsSystemKey == 1,
}
return result, true
}
// GetSystemKey returns the latest UI API key
func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE IsSystemKey = 1 ORDER BY Expiry DESC LIMIT 1")
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey)
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,
FriendlyName: rowResult.FriendlyName,
LastUsed: rowResult.LastUsed,
Permissions: uint8(rowResult.Permissions),
Expiry: rowResult.Expiry,
IsSystemKey: rowResult.IsSystemKey == 1,
}
return result, true
}
// SaveApiKey saves the API key to the database
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions) VALUES (?, ?, ?, ?)",
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions)
isSystemKey := 0
if apikey.IsSystemKey {
isSystemKey = 1
}
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions, Expiry, IsSystemKey) VALUES (?, ?, ?, ?, ?, ?)",
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions, apikey.Expiry, isSystemKey)
helper.Check(err)
}
@@ -77,3 +111,8 @@ func (p DatabaseProvider) DeleteApiKey(id string) {
_, err := p.sqliteDb.Exec("DELETE FROM ApiKeys WHERE Id = ?", id)
helper.Check(err)
}
func (p DatabaseProvider) cleanApiKeys() {
_, err := p.sqliteDb.Exec("DELETE FROM ApiKeys WHERE ApiKeys.Expiry > 0 AND ApiKeys.Expiry < ?", currentTime().Unix())
helper.Check(err)
}

View File

@@ -5,4 +5,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", "/apiDelete", "/apiKeys", "/apiNew", "/delete", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadComplete", "/uploadStatus"}
var protectedUrls = []string{"/admin", "/apiKeys", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadComplete", "/uploadStatus"}

View File

@@ -18,18 +18,21 @@ const (
// ApiPermNone means no permission granted
const ApiPermNone = 0
// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod
const ApiPermAllNoApiMod = 23
// ApiPermAll means all permission granted
const ApiPermAll = 31
// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod
// This is the default for new API keys that are created from the UI
const ApiPermAllNoApiMod = ApiPermAll - ApiPermApiMod
// ApiKey contains data of a single api key
type ApiKey struct {
Id string `json:"Id" redis:"Id"`
FriendlyName string `json:"FriendlyName" redis:"FriendlyName"`
LastUsed int64 `json:"LastUsed" redis:"LastUsed"`
Permissions uint8 `json:"Permissions" redis:"Permissions"`
Expiry int64 `json:"Expiry" redis:"Expiry"` // Does not expire if 0
IsSystemKey bool `json:"IsSystemKey" redis:"IsSystemKey"`
}
func (key *ApiKey) GetReadableDate() string {

View File

@@ -107,17 +107,16 @@ func (f *File) ToJsonResult(serverUrl string, includeFilename bool) string {
if err != nil {
return errorAsJson(err)
}
result := Result{
byteOutput, err := json.Marshal(Result{
Result: "OK",
IncludeFilename: includeFilename,
FileInfo: info,
}
bytes, err := json.Marshal(result)
})
if err != nil {
return errorAsJson(err)
}
return string(bytes)
return string(byteOutput)
}
// RequiresClientDecryption checks if the file needs to be decrypted by the client

View File

@@ -92,11 +92,8 @@ func Start() {
mux.HandleFunc("/admin", requireLogin(showAdminMenu, false))
mux.HandleFunc("/api/", processApi)
mux.HandleFunc("/apiDelete", requireLogin(deleteApiKey, false))
mux.HandleFunc("/apiKeys", requireLogin(showApiAdmin, false))
mux.HandleFunc("/apiNew", requireLogin(newApiKey, false))
mux.HandleFunc("/d", showDownload)
mux.HandleFunc("/delete", requireLogin(deleteFile, false))
mux.HandleFunc("/downloadFile", downloadFile)
mux.HandleFunc("/e2eInfo", requireLogin(e2eInfo, true))
mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, false))
@@ -302,21 +299,6 @@ func showApiAdmin(w http.ResponseWriter, r *http.Request) {
helper.CheckIgnoreTimeout(err)
}
// Handling of /apiNew
func newApiKey(w http.ResponseWriter, r *http.Request) {
api.NewKey(true)
redirect(w, "apiKeys")
}
// Handling of /apiDelete
func deleteApiKey(w http.ResponseWriter, r *http.Request) {
keys, ok := r.URL.Query()["id"]
if ok {
api.DeleteKey(keys[0])
}
redirect(w, "apiKeys")
}
// Handling of /api/
func processApi(w http.ResponseWriter, r *http.Request) {
api.Process(w, r, configuration.Get().MaxMemory)
@@ -505,17 +487,6 @@ func getE2eInfo(w http.ResponseWriter) {
_, _ = w.Write(bytesE2e)
}
// Handling of /delete
// User needs to be admin. Deletes the requested file
func deleteFile(w http.ResponseWriter, r *http.Request) {
keyId := queryUrl(w, r, "admin")
if keyId == "" {
return
}
storage.DeleteFile(keyId, true)
redirect(w, "admin")
}
// Checks if a file is associated with the GET parameter from the current URL
// Stops for 500ms to limit brute forcing if invalid key and redirects to redirectUrl
func queryUrl(w http.ResponseWriter, r *http.Request, redirectUrl string) string {
@@ -593,6 +564,7 @@ type UploadView struct {
DefaultPassword string
Logs string
PublicName string
SystemKey string
IsAdminView bool
IsDownloadView bool
IsApiView bool
@@ -678,6 +650,7 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
u.MaxParallelUploads = config.MaxParallelUploads
u.ChunkSize = config.ChunkSize
u.IncludeFilename = config.IncludeFilename
u.SystemKey = api.GetSystemKey()
return u
}

View File

@@ -5,7 +5,6 @@ package webserver
import (
"errors"
"github.com/forceu/gokapi/internal/configuration"
"github.com/forceu/gokapi/internal/configuration/database"
"github.com/forceu/gokapi/internal/test"
"github.com/forceu/gokapi/internal/test/testconfiguration"
"github.com/forceu/gokapi/internal/webserver/authentication"
@@ -428,37 +427,6 @@ func TestDownloadCorrectPassword(t *testing.T) {
})
}
func TestDeleteFileNonAuth(t *testing.T) {
t.Parallel()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL",
IsHtml: true,
RequiredContent: []string{"URL=./login"},
})
}
func TestDeleteFileInvalidKey(t *testing.T) {
t.Parallel()
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/delete",
IsHtml: true,
RequiredContent: []string{"URL=./admin"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/delete?id=",
IsHtml: true,
RequiredContent: []string{"URL=./admin"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
}
func TestPostUploadNoAuth(t *testing.T) {
t.Parallel()
test.HttpPostUploadRequest(t, test.HttpTestConfig{
@@ -522,18 +490,6 @@ func TestPostUpload(t *testing.T) {
})
}
func TestDeleteFile(t *testing.T) {
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/delete?id=e4TjE7CokWK0giiLNxDL",
IsHtml: true,
RequiredContent: []string{"URL=./admin"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
}
func TestApiPageAuthorized(t *testing.T) {
t.Parallel()
test.HttpPageResult(t, test.HttpTestConfig{
@@ -560,78 +516,6 @@ func TestApiPageNotAuthorized(t *testing.T) {
})
}
func TestNewApiKey(t *testing.T) {
// Authorised
amountKeys := len(database.GetAllApiKeys())
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiNew",
IsHtml: true,
RequiredContent: []string{"URL=./apiKeys"},
ExcludedContent: []string{"URL=./login"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
amountKeysAfter := len(database.GetAllApiKeys())
test.IsEqualInt(t, amountKeysAfter, amountKeys+1)
test.IsEqualInt(t, amountKeysAfter, 5)
// Not authorised
amountKeys++
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiNew",
IsHtml: true,
RequiredContent: []string{"URL=./login"},
ExcludedContent: []string{"URL=./apiKeys"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "invalid",
}},
})
amountKeysAfter = len(database.GetAllApiKeys())
test.IsEqualInt(t, amountKeysAfter, amountKeys)
test.IsEqualInt(t, amountKeysAfter, 5)
}
func TestDeleteApiKey(t *testing.T) {
// Not authorised
amountKeys := len(database.GetAllApiKeys())
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiDelete?id=jiREglQJW0bOqJakfjdVfe8T1EM8n8",
IsHtml: true,
RequiredContent: []string{"URL=./login"},
ExcludedContent: []string{"URL=./apiKeys"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "invalid",
}},
})
amountKeysAfter := len(database.GetAllApiKeys())
key, ok := database.GetApiKey("jiREglQJW0bOqJakfjdVfe8T1EM8n8")
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, key.Id, "jiREglQJW0bOqJakfjdVfe8T1EM8n8")
test.IsEqualInt(t, amountKeysAfter, amountKeys)
test.IsEqualInt(t, amountKeysAfter, 5)
// Authorised
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/apiDelete?id=jiREglQJW0bOqJakfjdVfe8T1EM8n8",
IsHtml: true,
RequiredContent: []string{"URL=./apiKeys"},
ExcludedContent: []string{"URL=./login"},
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",
}},
})
amountKeysAfter = len(database.GetAllApiKeys())
_, ok = database.GetApiKey("jiREglQJW0bOqJakfjdVfe8T1EM8n8")
test.IsEqualBool(t, ok, false)
test.IsEqualInt(t, amountKeysAfter, amountKeys-1)
test.IsEqualInt(t, amountKeysAfter, 4)
}
func TestProcessApi(t *testing.T) {
// Not authorised
test.HttpPageResult(t, test.HttpTestConfig{
@@ -652,11 +536,12 @@ func TestProcessApi(t *testing.T) {
Headers: []test.Header{{"apikey", "invalid"}},
})
// Authorised
// Valid session does not grant API access
test.HttpPageResult(t, test.HttpTestConfig{
Url: "http://127.0.0.1:53843/api/files/list",
RequiredContent: []string{"smallfile2"},
ExcludedContent: []string{"Unauthorized"},
RequiredContent: []string{"{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}"},
ExcludedContent: []string{"smallfile2"},
ResultCode: 401,
Cookies: []test.Cookie{{
Name: "session_token",
Value: "validsession",

View File

@@ -8,8 +8,6 @@ import (
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/storage"
"github.com/forceu/gokapi/internal/webserver/authentication"
"github.com/forceu/gokapi/internal/webserver/authentication/sessionmanager"
"github.com/forceu/gokapi/internal/webserver/fileupload"
"net/http"
"strconv"
@@ -46,6 +44,8 @@ func Process(w http.ResponseWriter, r *http.Request, maxMemory int) {
changeFriendlyName(w, request)
case "/auth/modify":
modifyApiPermission(w, request)
case "/auth/delete":
deleteApiKey(w, request)
default:
sendError(w, http.StatusBadRequest, "Invalid request")
}
@@ -122,6 +122,8 @@ func getApiPermissionRequired(requestUrl string) (uint8, bool) {
return models.ApiPermApiMod, true
case "/auth/modify":
return models.ApiPermApiMod, true
case "/auth/delete":
return models.ApiPermApiMod, true
default:
return models.ApiPermNone, false
}
@@ -143,6 +145,8 @@ func NewKey(defaultPermissions bool) string {
FriendlyName: "Unnamed key",
LastUsed: 0,
Permissions: models.ApiPermAllNoApiMod,
Expiry: 0,
IsSystemKey: false,
}
if !defaultPermissions {
newKey.Permissions = models.ApiPermNone
@@ -151,6 +155,38 @@ func NewKey(defaultPermissions bool) string {
return newKey.Id
}
// newSystemKey generates a new API key that is only used internally for the GUI
// and will be valid for 48 hours
func newSystemKey() string {
newKey := models.ApiKey{
Id: helper.GenerateRandomString(30),
FriendlyName: "Internal System Key",
LastUsed: 0,
Permissions: models.ApiPermAll,
Expiry: time.Now().Add(time.Hour * 48).Unix(),
IsSystemKey: true,
}
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() string {
key, ok := database.GetSystemKey()
if !ok || key.Expiry < time.Now().Add(time.Hour*24).Unix() {
return newSystemKey()
}
return key.Id
}
func deleteApiKey(w http.ResponseWriter, request apiRequest) {
if !isValidKeyForEditing(w, request) {
return
}
DeleteKey(request.apiInfo.apiKeyToModify)
}
func modifyApiPermission(w http.ResponseWriter, request apiRequest) {
if !isValidKeyForEditing(w, request) {
return
@@ -184,7 +220,7 @@ func isValidKeyForEditing(w http.ResponseWriter, request apiRequest) bool {
}
func createApiKey(w http.ResponseWriter, request apiRequest) {
key := NewKey(false)
key := NewKey(request.apiInfo.basicPermissions)
output := models.ApiKeyOutput{
Result: "OK",
Id: key,
@@ -283,7 +319,7 @@ func upload(w http.ResponseWriter, request apiRequest, maxMemory int) {
}
request.request.Body = http.MaxBytesReader(w, request.request.Body, maxUpload)
err := fileupload.Process(w, request.request, false, maxMemory)
err := fileupload.Process(w, request.request, maxMemory)
if err != nil {
sendError(w, http.StatusBadRequest, err.Error())
return
@@ -329,10 +365,7 @@ func isAuthorisedForApi(w http.ResponseWriter, request apiRequest) bool {
sendError(w, http.StatusBadRequest, "Invalid request")
return false
}
config := configuration.Get()
isOauth := config.Authentication.Method == authentication.OAuth2
interval := config.Authentication.OAuthRecheckInterval
if IsValidApiKey(request.apiKey, true, perm) || sessionmanager.IsValidSession(w, request.request, isOauth, interval) {
if IsValidApiKey(request.apiKey, true, perm) {
return true
}
sendError(w, http.StatusUnauthorized, "Unauthorized")
@@ -384,10 +417,11 @@ type fileInfo struct {
}
type apiInfo struct {
friendlyName string
apiKeyToModify string
permission uint8
grantPermission bool
friendlyName string
apiKeyToModify string
permission uint8
grantPermission bool
basicPermissions bool
}
type filemodInfo struct {
id string
@@ -424,10 +458,12 @@ func parseRequest(r *http.Request) apiRequest {
originalPassword: r.Header.Get("originalPassword") == "true",
},
apiInfo: apiInfo{
friendlyName: r.Header.Get("friendlyName"),
apiKeyToModify: r.Header.Get("apiKeyToModify"),
permission: uint8(permission),
grantPermission: r.Header.Get("permissionModifier") == "GRANT"},
friendlyName: r.Header.Get("friendlyName"),
apiKeyToModify: r.Header.Get("apiKeyToModify"),
permission: uint8(permission),
grantPermission: r.Header.Get("permissionModifier") == "GRANT",
basicPermissions: r.Header.Get("basicPermissions") == "true",
},
}
}
@@ -494,7 +530,7 @@ func IsValidApiKey(key string, modifyTime bool, requiredPermission uint8) bool {
return false
}
savedKey, ok := database.GetApiKey(key)
if ok && savedKey.Id != "" {
if ok && savedKey.Id != "" && (savedKey.Expiry == 0 || savedKey.Expiry > time.Now().Unix()) {
if modifyTime {
savedKey.LastUsed = time.Now().Unix()
database.UpdateTimeApiKey(savedKey)

View File

@@ -30,8 +30,6 @@ func TestMain(m *testing.M) {
os.Exit(exitVal)
}
// TODO test new permission system
const maxMemory = 20
var newKeyId string
@@ -62,22 +60,212 @@ func TestDeleteKey(t *testing.T) {
}
func TestIsValidApiKey(t *testing.T) {
test.IsEqualBool(t, IsValidApiKey("", false, models.ApiPermNone), false) // TODO permission
test.IsEqualBool(t, IsValidApiKey("invalid", false, models.ApiPermNone), false) // TODO permission
test.IsEqualBool(t, IsValidApiKey("validkey", false, models.ApiPermNone), true) // TODO permission
test.IsEqualBool(t, IsValidApiKey("", false, models.ApiPermNone), false)
test.IsEqualBool(t, IsValidApiKey("invalid", false, models.ApiPermNone), false)
test.IsEqualBool(t, IsValidApiKey("validkey", false, models.ApiPermNone), true)
key, ok := database.GetApiKey("validkey")
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, key.LastUsed == 0, true)
test.IsEqualBool(t, IsValidApiKey("validkey", true, models.ApiPermNone), true) // TODO permission
test.IsEqualBool(t, IsValidApiKey("validkey", true, models.ApiPermNone), true)
key, ok = database.GetApiKey("validkey")
test.IsEqualBool(t, ok, true)
test.IsEqualBool(t, key.LastUsed == 0, false)
newApiKey := NewKey(false)
test.IsEqualBool(t, IsValidApiKey(newApiKey, true, models.ApiPermNone), true)
for _, permission := range getAvailablePermissions(t) {
test.IsEqualBool(t, IsValidApiKey(newApiKey, true, permission), false)
}
for _, newPermission := range getAvailablePermissions(t) {
setPermissionApikey(newApiKey, newPermission, t)
for _, permission := range getAvailablePermissions(t) {
test.IsEqualBool(t, IsValidApiKey(newApiKey, true, permission), permission == newPermission)
}
}
setPermissionApikey(newApiKey, models.ApiPermEdit|models.ApiPermDelete, t)
test.IsEqualBool(t, IsValidApiKey(newApiKey, true, models.ApiPermEdit), true)
test.IsEqualBool(t, IsValidApiKey(newApiKey, true, models.ApiPermAll), false)
test.IsEqualBool(t, IsValidApiKey(newApiKey, true, models.ApiPermView), false)
}
func setPermissionApikey(key string, newPermission uint8, t *testing.T) {
apiKey, ok := database.GetApiKey(key)
test.IsEqualBool(t, ok, true)
apiKey.Permissions = newPermission
database.SaveApiKey(apiKey)
}
func getAvailablePermissions(t *testing.T) []uint8 {
result := []uint8{models.ApiPermView, models.ApiPermUpload, models.ApiPermDelete, models.ApiPermApiMod, models.ApiPermEdit}
sum := 0
for _, perm := range result {
sum = sum + int(perm)
}
if sum != models.ApiPermAll {
t.Fatal("List of permissions are incorrect")
}
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()
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()
test.IsEqualBool(t, systemKey == newKey, true)
retrievedSystemKey.Expiry = time.Now().Add(time.Hour * 23).Unix()
database.SaveApiKey(retrievedSystemKey)
newKey = GetSystemKey()
test.IsEqualBool(t, systemKey != newKey, true)
}
func TestDelete(t *testing.T) {
database.SaveApiKey(models.ApiKey{
Id: "toDelete",
})
_, ok := database.GetApiKey("toDelete")
test.IsEqualBool(t, ok, true)
w, r := test.GetRecorder("GET", "/auth/delete", nil, []test.Header{{
Name: "apikey",
Value: "invalid",
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/auth/delete", nil, []test.Header{{
Name: "apiKeyToModify",
Value: "toDelete",
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/auth/delete", nil, []test.Header{{
Name: "apiKeyToModify",
Value: "toDelete",
}, {
Name: "apikey",
Value: getNewKeyWithPermissionMissing(t, models.ApiPermApiMod).Id,
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/auth/delete", nil, []test.Header{{
Name: "apiKeyToModify",
Value: "toDelete",
}, {
Name: "apikey",
Value: getNewKeyWithAllPermissions(t).Id,
}}, nil)
Process(w, r, maxMemory)
test.IsEqualInt(t, w.Code, 200)
_, ok = database.GetApiKey("toDelete")
test.IsEqualBool(t, ok, false)
w, r = test.GetRecorder("GET", "/auth/delete", nil, []test.Header{{
Name: "apiKeyToModify",
Value: "toDelete",
}, {
Name: "apikey",
Value: "validkey",
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Invalid api key provided.\"}")
}
func getNewKeyWithAllPermissions(t *testing.T) models.ApiKey {
validKey, ok := database.GetApiKey(NewKey(false))
test.IsEqualBool(t, ok, true)
validKey.SetPermission(models.ApiPermAll)
database.SaveApiKey(validKey)
return validKey
}
func getNewKeyWithPermissionMissing(t *testing.T, removePerm uint8) models.ApiKey {
validKey, ok := database.GetApiKey(NewKey(false))
test.IsEqualBool(t, ok, true)
validKey.SetPermission(models.ApiPermAll)
validKey.RemovePermission(removePerm)
database.SaveApiKey(validKey)
return validKey
}
func countApiKeys() int {
return len(database.GetAllApiKeys())
}
func TestNewApiKey(t *testing.T) {
keysBefore := countApiKeys()
w, r := test.GetRecorder("GET", "/auth/create", nil, []test.Header{{
Name: "apikey",
Value: "invalid",
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/auth/create", nil, nil, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/auth/create", nil, []test.Header{{
Name: "apikey",
Value: "validkey",
}, {
Name: "friendlyName",
Value: "New Key",
}}, nil)
Process(w, r, maxMemory)
test.IsEqualInt(t, w.Code, 200)
keysAfter := countApiKeys()
test.IsEqualInt(t, keysAfter, keysBefore+1)
var result models.ApiKeyOutput
err := json.Unmarshal(w.Body.Bytes(), &result)
test.IsNil(t, err)
newKey, ok := database.GetApiKey(result.Id)
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, newKey.FriendlyName, "New Key")
w, r = test.GetRecorder("GET", "/auth/create", nil, []test.Header{{
Name: "apikey",
Value: "validkey",
}}, nil)
Process(w, r, maxMemory)
test.IsEqualInt(t, w.Code, 200)
keysAfter = countApiKeys()
test.IsEqualInt(t, keysAfter, keysBefore+2)
err = json.Unmarshal(w.Body.Bytes(), &result)
test.IsNil(t, err)
newKey, ok = database.GetApiKey(result.Id)
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, newKey.FriendlyName, "Unnamed key")
w, r = test.GetRecorder("GET", "/auth/create", nil, []test.Header{{
Name: "apikey",
Value: getNewKeyWithPermissionMissing(t, models.ApiPermApiMod).Id,
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
}
func TestProcess(t *testing.T) {
w, r := test.GetRecorder("GET", "/api/auth/friendlyname", nil, nil, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/api/auth/friendlyname", []test.Cookie{{
Name: "session_token",
Value: "validsession",
}}, nil, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
w, r = test.GetRecorder("GET", "/api/invalid", nil, nil, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "Invalid request")
@@ -87,12 +275,6 @@ func TestProcess(t *testing.T) {
}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "Invalid request")
w, r = test.GetRecorder("GET", "/api/invalid", []test.Cookie{{
Name: "session_token",
Value: "validsession",
}}, nil, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "Invalid request")
}
func TestAuthDisabledLogin(t *testing.T) {
@@ -134,6 +316,13 @@ func TestChangeFriendlyName(t *testing.T) {
w = httptest.NewRecorder()
Process(w, r, maxMemory)
test.IsEqualInt(t, w.Code, 200)
w, r = test.GetRecorder("GET", "/api/auth/friendlyname", nil, []test.Header{{
Name: "apikey", Value: getNewKeyWithPermissionMissing(t, models.ApiPermApiMod).Id}, {
Name: "apiKeyToModify", Value: "validkey"}, {
Name: "friendlyName", Value: "NewName2"}}, nil)
Process(w, r, maxMemory)
test.ResponseBodyContains(t, w, "{\"Result\":\"error\",\"ErrorMessage\":\"Unauthorized\"}")
}
func TestDeleteFile(t *testing.T) {

View File

@@ -13,13 +13,13 @@ import (
)
// Process processes a file upload request
func Process(w http.ResponseWriter, r *http.Request, isWeb bool, maxMemory int) error {
func Process(w http.ResponseWriter, r *http.Request, maxMemory int) error {
err := r.ParseMultipartForm(int64(maxMemory) * 1024 * 1024)
if err != nil {
return err
}
defer r.MultipartForm.RemoveAll()
config, err := parseConfig(r.Form, isWeb)
config, err := parseConfig(r.Form, false)
if err != nil {
return err
}

View File

@@ -80,12 +80,12 @@ func TestParseConfig(t *testing.T) {
func TestProcess(t *testing.T) {
w, r := test.GetRecorder("POST", "/upload", nil, nil, strings.NewReader("invalid§$%&%§"))
err := Process(w, r, false, 20)
err := Process(w, r, 20)
test.IsNotNil(t, err)
w = httptest.NewRecorder()
r = getFileUploadRecorder(false)
err = Process(w, r, false, 20)
err = Process(w, r, 20)
test.IsNil(t, err)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)

View File

@@ -15,9 +15,6 @@
{
"apikey": ["VIEW","UPLOAD","DELETE", "API_MANAGE"]
},
{
"session": []
}
],
"tags": [
{
@@ -40,9 +37,6 @@
{
"apikey": ["VIEW"]
},
{
"session": []
}
],
"responses": {
"200": {
@@ -80,9 +74,6 @@
{
"apikey": ["UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -126,9 +117,6 @@
{
"apikey": ["UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -172,9 +160,6 @@
{
"apikey": ["UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -218,9 +203,6 @@
{
"apikey": ["VIEW","UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -264,9 +246,6 @@
{
"apikey": ["API_EDIT"]
},
{
"session": []
}
],
"parameters": [
{
@@ -346,9 +325,6 @@
"security": [
{
"apikey": ["DELETE"]
},
{
"session": []
}
],
"parameters": [
@@ -383,14 +359,11 @@
"auth"
],
"summary": "Creates a new API key",
"description": "This API call returns a new API key. The new key does not have any permissions. Requires permission API_MOD",
"description": "This API call returns a new API key. The new key does not have any permissions, unless specified. Requires permission API_MOD",
"operationId": "create",
"security": [
{
"apikey": ["API_MANAGE"]
},
{
"session": []
}
],
"parameters": [
@@ -404,6 +377,17 @@
"schema": {
"type": "string"
}
},
{
"name": "basicPermissions",
"in": "header",
"description": "If true, basic permissions are automatically granted",
"required": false,
"style": "simple",
"explode": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
@@ -434,9 +418,6 @@
"security": [
{
"apikey": ["API_MANAGE"]
},
{
"session": []
}
],
"parameters": [
@@ -487,9 +468,6 @@
"security": [
{
"apikey": ["API_MANAGE"]
},
{
"session": []
}
],
"parameters": [
@@ -541,6 +519,45 @@
}
}
}
},
"/auth/delete": {
"delete": {
"tags": [
"auth"
],
"summary": "Deletes an API key",
"description": "This API call deletes the given API key. Requires permission API_MOD",
"operationId": "apidelete",
"security": [
{
"apikey": ["API_MANAGE"]
}
],
"parameters": [
{
"name": "apiKeyToModify",
"in": "header",
"description": "The API key to delete",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided or not logged in as admin"
}
}
}
}
},
"components": {
@@ -812,11 +829,6 @@
"type": "apiKey",
"name": "apikey",
"in": "header"
},
"session": {
"type": "apiKey",
"name": "session_token",
"in": "cookie"
}
}
}

View File

@@ -189,7 +189,7 @@ a:hover {
color: #7e7e7e;
}
.apiperm-processing {
color: #929611;
color: #e5eb00;
}
.gokapi-dialog {

View File

@@ -1 +1 @@
.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastnotification.show{opacity:1;pointer-events:auto}.apiperm-granted{cursor:pointer;color:#19b90e}.apiperm-notgranted{cursor:pointer;color:#7e7e7e}.apiperm-processing{color:#929611}.gokapi-dialog{background-color:#212529;color:#ddd}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden}
.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastnotification.show{opacity:1;pointer-events:auto}.apiperm-granted{cursor:pointer;color:#19b90e}.apiperm-notgranted{cursor:pointer;color:#7e7e7e}.apiperm-processing{color:#e5eb00}.gokapi-dialog{background-color:#212529;color:#ddd}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden}

View File

@@ -249,6 +249,7 @@ function editFile() {
headers: {
'Content-Type': 'application/json',
'id': button.getAttribute('data-fileid'),
'apikey': systemKey,
'allowedDownloads': allowedDownloads,
'expiryTimestamp': expiryTimestamp,
'password': password,
@@ -393,6 +394,7 @@ function changeApiPermission(apiKey, permission, buttonId) {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'apiKeyToModify': apiKey,
'permission': permission,
'permissionModifier': modifier
@@ -400,7 +402,6 @@ function changeApiPermission(apiKey, permission, buttonId) {
},
};
// Send the request
fetch(apiUrl, requestOptions)
.then(response => {
if (!response.ok) {
@@ -427,6 +428,93 @@ function changeApiPermission(apiKey, permission, buttonId) {
});
}
function deleteApiKey(apiKey) {
document.getElementById("delete-"+apiKey).disabled = true;
var apiUrl = './api/auth/delete';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'apiKeyToModify': apiKey,
},
};
fetch(apiUrl, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
})
.then(data => {
document.getElementById("row-"+apiKey).remove();
})
.catch(error => {
alert("Unable to delete API key: " + error);
console.error('Error:', error);
});
}
function newApiKey() {
document.getElementById("button-newapi").disabled = true;
var apiUrl = './api/auth/create';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'basicPermissions': 'true'
},
};
fetch(apiUrl, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
})
.then(data => {
location.reload();
})
.catch(error => {
alert("Unable to create API key: " + error);
console.error('Error:', error);
});
}
function deleteFile(id) {
document.getElementById("button-delete-"+id).disabled = true;
var apiUrl = './api/files/delete';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': systemKey,
'id': id
},
};
fetch(apiUrl, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
})
.then(data => {
location.reload();
})
.catch(error => {
alert("Unable to delete file: " + error);
console.error('Error:', error);
});
}
function checkBoxChanged(checkBox, correspondingInput) {
let disable = !checkBox.checked;
@@ -550,6 +638,7 @@ function addRow(jsonText) {
let item = jsonObject.FileInfo;
let table = document.getElementById("downloadtable");
let row = table.insertRow(0);
row.id = "row-"+ item.Id;
let cellFilename = row.insertCell(0);
let cellFileSize = row.insertCell(1);
let cellRemainingDownloads = row.insertCell(2);
@@ -586,7 +675,7 @@ function addRow(jsonText) {
}
buttons = buttons + '<button type="button" id="qrcode-'+item.Id+'" title="QR Code" class="btn btn-outline-light btn-sm" onclick="showQrCode(\'' + item.UrlDownload + '\');"><i class="bi bi-qr-code"></i></button> ';
buttons = buttons + '<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal(\'' + item.Name + '\',\'' + item.Id + '\', ' + item.DownloadsRemaining + ', ' + item.ExpireAt + ', ' + item.IsPasswordProtected + ', ' + item.UnlimitedDownloads + ', ' + item.UnlimitedTime + ');"><i class="bi bi-pencil"></i></button> ';
buttons = buttons + '<button type="button" title="Delete" class="btn btn-outline-danger btn-sm" onclick="window.location=\'./delete?id=' + item.Id + '\'"><i class="bi bi-trash3"></i></button>';
buttons = buttons + '<button type="button" id="button-delete-' + item.Id + '" title="Delete" class="btn btn-outline-danger btn-sm" onclick="deleteFile(\'' + item.Id + '\')"><i class="bi bi-trash3"></i></button>';
cellButtons.innerHTML = buttons;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -64,7 +64,7 @@
{{ range .Items }}
{{ if or (gt .ExpireAt $.TimeNow) (.UnlimitedTime) }}
{{ if or (gt .DownloadsRemaining 0) (.UnlimitedDownloads) }}
<tr>
<tr id="row-{{ .Id }}">
<td id="cell-name-{{ .Id }}">{{ .Name }}</td>
<td data-order="{{ .SizeBytes }}">{{ .Size }}</td>
{{ if .UnlimitedDownloads }}
@@ -87,7 +87,7 @@
{{ end }}
<button type="button" id="qrcode-{{ .Id }}" title="QR Code" class="btn btn-outline-light btn-sm" onclick="showQrCode('{{ .UrlDownload }}');"><i class="bi bi-qr-code"></i></button>
<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal('{{.Name }}','{{.Id}}', {{.DownloadsRemaining }}, {{.ExpireAt }}, {{.IsPasswordProtected}}, {{.UnlimitedDownloads }}, {{.UnlimitedTime}});"><i class="bi bi-pencil"></i></button>
<button type="button" title="Delete" class="btn btn-outline-danger btn-sm" onclick="window.location='./delete?id={{ .Id }}'"><i class="bi bi-trash3"></i></button></td>
<button id="button-delete-{{ .Id }}" type="button" title="Delete" class="btn btn-outline-danger btn-sm" onclick="deleteFile('{{ .Id }}')"><i class="bi bi-trash3"></i></button></td>
</tr>
{{ end }}
{{ end }}
@@ -176,6 +176,7 @@
});
});
registerChangeHandler();
var systemKey = "{{.SystemKey}}";
</script>
{{ if .EndToEndEncryption }}

View File

@@ -18,12 +18,13 @@
<th scope="col">Last Used</th>
<th scope="col">Permissions</th>
<th scope="col">Actions</th>
<th scope="col"><button type="button" class="btn btn-outline-light btn-sm" onclick="window.location='./apiNew'"><i class="bi bi-plus-circle-fill"></i> New Key</button></th>
<th scope="col"><button id="button-newapi" type="button" class="btn btn-outline-light btn-sm" onclick="newApiKey()"><i class="bi bi-plus-circle-fill"></i> New Key</button></th>
</tr>
</thead>
<tbody>
{{ range .ApiKeys }}
<tr>
{{ if not .IsSystemKey }}
<tr id="row-{{ .Id }}">
<td scope="col" id="{{ .Id }}" class="apiname">{{ .FriendlyName }}</td>
<td scope="col">{{ .Id }}</td>
<td scope="col">{{ .GetReadableDate }}</td>
@@ -31,19 +32,21 @@
<i id="perm_view_{{ .Id }}" class="bi bi-eye {{if not .HasPermissionView}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="List Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_VIEW", "perm_view_{{ .Id }}");'></i>
<i id="perm_upload_{{ .Id }}" class="bi bi-file-earmark-arrow-up {{if not .HasPermissionUpload}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Upload" onclick='changeApiPermission("{{ .Id }}","PERM_UPLOAD", "perm_upload_{{ .Id }}");'></i>
<i id="perm_edit_{{ .Id }}" class="bi bi-pencil {{if not .HasPermissionEdit}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Edit Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_EDIT", "perm_edit_{{ .Id }}");'></i>
<i id="perm_delete_{{ .Id }}" class="bi bi-trash3 {{if not .HasPermissionDelete}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Delete" onclick='changeApiPermission("{{ .Id }}","PERM_DELETE", "perm_delete_{{ .Id }}");'></i>
<i id="perm_delete_{{ .Id }}" class="bi bi-trash3 {{if not .HasPermissionDelete}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Delete Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_DELETE", "perm_delete_{{ .Id }}");'></i>
<i id="perm_api_{{ .Id }}" class="bi bi-sliders2 {{if not .HasPermissionApiMod}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Manage API Keys" onclick='changeApiPermission("{{ .Id }}","PERM_API_MOD", "perm_api_{{ .Id }}");'></i>
</td>
<td scope="col"><button type="button" data-clipboard-text="{{ .Id }}" title="Copy API Key" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i></button> <button type="button" class="btn btn-outline-danger btn-sm" onclick="window.location='./apiDelete?id={{ .Id }}'" title="Delete"><i class="bi bi-trash3"></i></button></td>
<td scope="col"><button type="button" data-clipboard-text="{{ .Id }}" onclick="showToast()" title="Copy API Key" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i></button> <button id="delete-{{ .Id }}" type="button" class="btn btn-outline-danger btn-sm" onclick="deleteApiKey('{{ .Id }}')" title="Delete"><i class="bi bi-trash3"></i></button></td>
<td scope="col"></td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div id="toastnotification" class="toastnotification">API key copied to clipboard</div>
</div>
</div>
<script src="./js/min/admin.min.{{ template "js_admin_version"}}.js"></script>
@@ -69,6 +72,7 @@
xmlhttp.open("GET", "./api/auth/friendlyname");
xmlhttp.setRequestHeader("apiKeyToModify", row.id);
xmlhttp.setRequestHeader("friendlyName", val);
xmlhttp.setRequestHeader("apikey", systemKey);
xmlhttp.send();
row.classList.remove("isBeingEdited");
@@ -88,6 +92,7 @@
input.focus();
}
});
var systemKey = "{{.SystemKey}}";
</script>
{{ template "footer" true }}
{{ end }}

View File

@@ -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"}}5{{end}}
{{define "js_admin_version"}}6{{end}}
{{define "js_dropzone_version"}}4{{end}}
{{define "js_e2eversion"}}3{{end}}
{{define "css_main"}}2{{end}}

View File

@@ -15,9 +15,6 @@
{
"apikey": ["VIEW","UPLOAD","DELETE", "API_MANAGE"]
},
{
"session": []
}
],
"tags": [
{
@@ -40,9 +37,6 @@
{
"apikey": ["VIEW"]
},
{
"session": []
}
],
"responses": {
"200": {
@@ -80,9 +74,6 @@
{
"apikey": ["UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -126,9 +117,6 @@
{
"apikey": ["UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -172,9 +160,6 @@
{
"apikey": ["UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -218,9 +203,6 @@
{
"apikey": ["VIEW","UPLOAD"]
},
{
"session": []
}
],
"requestBody": {
"content": {
@@ -264,9 +246,6 @@
{
"apikey": ["API_EDIT"]
},
{
"session": []
}
],
"parameters": [
{
@@ -346,9 +325,6 @@
"security": [
{
"apikey": ["DELETE"]
},
{
"session": []
}
],
"parameters": [
@@ -383,14 +359,11 @@
"auth"
],
"summary": "Creates a new API key",
"description": "This API call returns a new API key. The new key does not have any permissions. Requires permission API_MOD",
"description": "This API call returns a new API key. The new key does not have any permissions, unless specified. Requires permission API_MOD",
"operationId": "create",
"security": [
{
"apikey": ["API_MANAGE"]
},
{
"session": []
}
],
"parameters": [
@@ -404,6 +377,17 @@
"schema": {
"type": "string"
}
},
{
"name": "basicPermissions",
"in": "header",
"description": "If true, basic permissions are automatically granted",
"required": false,
"style": "simple",
"explode": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
@@ -434,9 +418,6 @@
"security": [
{
"apikey": ["API_MANAGE"]
},
{
"session": []
}
],
"parameters": [
@@ -487,9 +468,6 @@
"security": [
{
"apikey": ["API_MANAGE"]
},
{
"session": []
}
],
"parameters": [
@@ -541,6 +519,45 @@
}
}
}
},
"/auth/delete": {
"delete": {
"tags": [
"auth"
],
"summary": "Deletes an API key",
"description": "This API call deletes the given API key. Requires permission API_MOD",
"operationId": "apidelete",
"security": [
{
"apikey": ["API_MANAGE"]
}
],
"parameters": [
{
"name": "apiKeyToModify",
"in": "header",
"description": "The API key to delete",
"required": true,
"style": "simple",
"explode": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Operation successful"
},
"400": {
"description": "Invalid ID supplied"
},
"401": {
"description": "Invalid API key provided or not logged in as admin"
}
}
}
}
},
"components": {
@@ -812,11 +829,6 @@
"type": "apiKey",
"name": "apikey",
"in": "header"
},
"session": {
"type": "apiKey",
"name": "session_token",
"in": "cookie"
}
}
}