From 992ad5799ffc9368e6960d412ae1be5972c9ad56 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Thu, 13 Jun 2024 23:01:08 +0200 Subject: [PATCH 01/14] TODO v2 --- TODO.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..751fea72 --- /dev/null +++ b/TODO.md @@ -0,0 +1,22 @@ +# TODO + +## Feature: Guest Uploading +- [ ] New admin page to manage guest uploading + - [ ] Generate guest token with properties + - [ ] Max no. of files + - [ ] Upload quota + - [ ] View active tokens + their uploads + - [ ] Remaining files and quota + - [ ] Get links for guest uploads + - [ ] Also allow Updating/Deleting +- [ ] New guest upload page /guestupload + - [ ] First, enter token + - [ ] Display remaining no. of files and quota + - [ ] Upload like admin + - [ ] Only accept if file isn't too big + +## Tasks +- [ ] Add guest tokens to database +- [ ] Add webserver endpoint for uploading +- [ ] Create page to upload +- [ ] Create admin panel page to make tokens From b602f9335e4f1660c363aebaed75eaa07be6a741 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sat, 15 Jun 2024 17:15:42 +0200 Subject: [PATCH 02/14] Add upload tokens to database --- internal/configuration/database/Database.go | 13 ++- .../configuration/database/Database_test.go | 50 ++++++++++-- .../configuration/database/guestuploads.go | 79 +++++++++++++++++++ internal/models/GuestUpload.go | 9 +++ 4 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 internal/configuration/database/guestuploads.go create mode 100644 internal/models/GuestUpload.go diff --git a/internal/configuration/database/Database.go b/internal/configuration/database/Database.go index 6e35e1cf..ac07db5f 100644 --- a/internal/configuration/database/Database.go +++ b/internal/configuration/database/Database.go @@ -3,12 +3,15 @@ package database import ( "database/sql" "fmt" - "github.com/forceu/gokapi/internal/helper" "log" + + "github.com/forceu/gokapi/internal/helper" + // Required for sqlite driver - _ "modernc.org/sqlite" "os" "path/filepath" + + _ "modernc.org/sqlite" ) var sqliteDb *sql.DB @@ -99,6 +102,12 @@ func createNewDatabase() { "Permissions" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY("Id") ) WITHOUT ROWID; + CREATE TABLE "UploadTokens" ( + "Id" TEXT NOT NULL UNIQUE, + "LastUsed" INTEGER NOT NULL, + "LastUsedString" TEXT NOT NULL, + PRIMARY KEY("Id") + ) WITHOUT ROWID; CREATE TABLE "E2EConfig" ( "id" INTEGER NOT NULL UNIQUE, "Config" BLOB NOT NULL, diff --git a/internal/configuration/database/Database_test.go b/internal/configuration/database/Database_test.go index 56f65d5e..48cd1a28 100644 --- a/internal/configuration/database/Database_test.go +++ b/internal/configuration/database/Database_test.go @@ -5,17 +5,19 @@ package database import ( "database/sql" "errors" - "github.com/DATA-DOG/go-sqlmock" - "github.com/forceu/gokapi/internal/helper" - "github.com/forceu/gokapi/internal/models" - "github.com/forceu/gokapi/internal/test" - "golang.org/x/exp/slices" "math" "os" "regexp" "sync" "testing" "time" + + "github.com/DATA-DOG/go-sqlmock" + "golang.org/x/exp/slices" + + "github.com/forceu/gokapi/internal/helper" + "github.com/forceu/gokapi/internal/models" + "github.com/forceu/gokapi/internal/test" ) func TestMain(m *testing.M) { @@ -152,6 +154,44 @@ func TestApiKey(t *testing.T) { test.IsEqualString(t, key.FriendlyName, "Old Key") } +func TestGuestUploadToken(t *testing.T) { + SaveUploadToken(models.UploadToken{ + Id: "newtoken", + LastUsedString: "LastUsed", + LastUsed: 100, + }) + SaveUploadToken(models.UploadToken{ + Id: "newtoken2", + LastUsedString: "LastUsed2", + LastUsed: 200, + }) + + tokens := GetAllUploadTokens() + test.IsEqualInt(t, len(tokens), 2) + test.IsEqualString(t, tokens["newtoken"].Id, "newtoken") + test.IsEqualString(t, tokens["newtoken"].LastUsedString, "LastUsed") + test.IsEqualInt64(t, tokens["newtoken"].LastUsed, 100) + + test.IsEqualInt(t, len(GetAllUploadTokens()), 2) + DeleteUploadToken("newtoken2") + test.IsEqualInt(t, len(GetAllUploadTokens()), 1) + + token, ok := GetUploadToken("newtoken") + test.IsEqualBool(t, ok, true) + test.IsEqualString(t, token.Id, "newtoken") + _, ok = GetUploadToken("newtoken2") + test.IsEqualBool(t, ok, false) + + SaveUploadToken(models.UploadToken{ + Id: "newtoken", + LastUsed: 100, + LastUsedString: "RecentlyUsed", + }) + token, ok = GetUploadToken("newtoken") + test.IsEqualBool(t, ok, true) + test.IsEqualString(t, token.LastUsedString, "RecentlyUsed") +} + func TestSession(t *testing.T) { renewAt := time.Now().Add(1 * time.Hour).Unix() SaveSession("newsession", models.Session{ diff --git a/internal/configuration/database/guestuploads.go b/internal/configuration/database/guestuploads.go new file mode 100644 index 00000000..2247f7ec --- /dev/null +++ b/internal/configuration/database/guestuploads.go @@ -0,0 +1,79 @@ +package database + +import ( + "database/sql" + "errors" + + "github.com/forceu/gokapi/internal/helper" + "github.com/forceu/gokapi/internal/models" +) + +type schemaUploadTokens struct { + Id string + FriendlyName string + LastUsed int64 + LastUsedString string + Permissions int +} + +// GetAllUploadTokens returns a map with all upload tokens +func GetAllUploadTokens() map[string]models.UploadToken { + result := make(map[string]models.UploadToken) + + rows, err := sqliteDb.Query("SELECT * FROM UploadTokens") + helper.Check(err) + defer rows.Close() + for rows.Next() { + rowData := schemaUploadTokens{} + err = rows.Scan(&rowData.Id, &rowData.LastUsed, &rowData.LastUsedString) + helper.Check(err) + result[rowData.Id] = models.UploadToken{ + Id: rowData.Id, + LastUsed: rowData.LastUsed, + LastUsedString: rowData.LastUsedString, + } + } + return result +} + +// GetUploadToken returns a models.UploadToken if valid or false if the ID is not valid +func GetUploadToken(id string) (models.UploadToken, bool) { + var rowResult schemaUploadTokens + row := sqliteDb.QueryRow("SELECT * FROM UploadTokens WHERE Id = ?", id) + err := row.Scan(&rowResult.Id, &rowResult.LastUsed, &rowResult.LastUsedString) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return models.UploadToken{}, false + } + helper.Check(err) + return models.UploadToken{}, false + } + + result := models.UploadToken{ + Id: rowResult.Id, + LastUsed: rowResult.LastUsed, + LastUsedString: rowResult.LastUsedString, + } + + return result, true +} + +// SaveUploadToken saves the upload token to the database +func SaveUploadToken(uploadToken models.UploadToken) { + _, err := sqliteDb.Exec("INSERT OR REPLACE INTO UploadTokens (Id, LastUsed, LastUsedString) VALUES (?, ?, ?)", + uploadToken.Id, uploadToken.LastUsed, uploadToken.LastUsedString) + helper.Check(err) +} + +// UpdateTimeUploadToken writes the content of LastUsage to the database +func UpdateTimeUploadToken(uploadToken models.UploadToken) { + _, err := sqliteDb.Exec("UPDATE UploadTokens SET LastUsed = ?, LastUsedString = ? WHERE Id = ?", + uploadToken.LastUsed, uploadToken.LastUsedString, uploadToken.Id) + helper.Check(err) +} + +// DeleteUploadToken deletes an upload token with the given ID +func DeleteUploadToken(id string) { + _, err := sqliteDb.Exec("DELETE FROM UploadTokens WHERE Id = ?", id) + helper.Check(err) +} diff --git a/internal/models/GuestUpload.go b/internal/models/GuestUpload.go new file mode 100644 index 00000000..3a06d46b --- /dev/null +++ b/internal/models/GuestUpload.go @@ -0,0 +1,9 @@ +package models + +// UploadToken contains data of a single guest upload token. +// It is essntially a single-use API key for uploading one item. +type UploadToken struct { + Id string `json:"Id"` + LastUsedString string `json:"LastUsedString"` + LastUsed int64 `json:"LastUsed"` +} From ec67e47b20725b64de98525f4d93465a7158c393 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sat, 15 Jun 2024 17:55:53 +0200 Subject: [PATCH 03/14] Add guest upload admin page --- internal/webserver/Webserver.go | 53 ++++++++-- .../web/templates/html_guestuploads.tmpl | 96 +++++++++++++++++++ .../webserver/web/templates/html_header.tmpl | 1 + 3 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 internal/webserver/web/templates/html_guestuploads.tmpl diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 2c70e5b9..9c9ffa79 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -12,6 +12,17 @@ import ( "encoding/json" "errors" "fmt" + "html/template" + "io" + "io/fs" + "log" + "net/http" + "os" + "sort" + "strings" + templatetext "text/template" + "time" + "github.com/NYTimes/gziphandler" "github.com/forceu/gokapi/internal/configuration" "github.com/forceu/gokapi/internal/configuration/database" @@ -27,16 +38,6 @@ import ( "github.com/forceu/gokapi/internal/webserver/fileupload" "github.com/forceu/gokapi/internal/webserver/sse" "github.com/forceu/gokapi/internal/webserver/ssl" - "html/template" - "io" - "io/fs" - "log" - "net/http" - "os" - "sort" - "strings" - templatetext "text/template" - "time" ) // TODO add 404 handler @@ -105,6 +106,7 @@ func Start() { mux.HandleFunc("/error-auth", showErrorAuth) mux.HandleFunc("/error-oauth", showErrorIntOAuth) mux.HandleFunc("/forgotpw", forgotPassword) + mux.HandleFunc("/guestUploads", showGuestUploadMenu) mux.HandleFunc("/hotlink/", showHotlink) mux.HandleFunc("/index", showIndex) mux.HandleFunc("/login", showLogin) @@ -523,6 +525,13 @@ func showAdminMenu(w http.ResponseWriter, r *http.Request) { helper.CheckIgnoreTimeout(err) } +// Handling of /guestUploads +// If use is authenticated, this menu lists and allows creating guest upload tokens +func showGuestUploadMenu(w http.ResponseWriter, r *http.Request) { + err := templateFolder.ExecuteTemplate(w, "guestuploads", (&UploadView{}).convertGlobalConfig(ViewGuestUploads)) + helper.CheckIgnoreTimeout(err) +} + // Handling of /logs // If user is authenticated, this menu shows the stored logs func showLogs(w http.ResponseWriter, r *http.Request) { @@ -568,6 +577,10 @@ type UploadView struct { Items []models.FileApiOutput ApiKeys []models.ApiKey ServerUrl string + UploadTokens []models.UploadToken + Url string + HotlinkUrl string + GenericHotlinkUrl string DefaultPassword string Logs string PublicName string @@ -597,11 +610,15 @@ const ViewLogs = 1 // ViewAPI is the identifier for the API menu const ViewAPI = 2 +// ViewGuestUploads is the identifier for the Guest Upload menu +const ViewGuestUploads = 3 + // Converts the globalConfig variable to an UploadView struct to pass the infos to // the admin template func (u *UploadView) convertGlobalConfig(view int) *UploadView { var result []models.FileApiOutput var resultApi []models.ApiKey + var resultUploadTokens []models.UploadToken config := configuration.Get() switch view { @@ -640,12 +657,28 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView { } else { u.Logs = "Warning: Log file not found!" } + case ViewGuestUploads: + for _, element := range database.GetAllUploadTokens() { + if element.LastUsed == 0 { + element.LastUsedString = "Never" + } else { + element.LastUsedString = time.Unix(element.LastUsed, 0).Format("2006-01-02 15:04:05") + } + resultUploadTokens = append(resultUploadTokens, element) + } + sort.Slice(resultUploadTokens[:], func(i, j int) bool { + if resultUploadTokens[i].LastUsed == resultUploadTokens[j].LastUsed { + return resultUploadTokens[i].Id < resultUploadTokens[j].Id + } + return resultUploadTokens[i].LastUsed > resultUploadTokens[j].LastUsed + }) } u.ServerUrl = config.ServerUrl u.Items = result u.PublicName = config.PublicName u.ApiKeys = resultApi + u.UploadTokens = resultUploadTokens u.TimeNow = time.Now().Unix() u.IsAdminView = true u.ActiveView = view diff --git a/internal/webserver/web/templates/html_guestuploads.tmpl b/internal/webserver/web/templates/html_guestuploads.tmpl new file mode 100644 index 00000000..45726ef6 --- /dev/null +++ b/internal/webserver/web/templates/html_guestuploads.tmpl @@ -0,0 +1,96 @@ +{{ define "guestuploads" }} +{{ template "header" . }} + +
+
+
+
+

Guest Upload Tokens

+
+ New Upload Token + +

+
+ + + + + + + + + + {{ range .UploadTokens }} + {{ if or (gt .ExpireAt $.TimeNow) }} + + + + + + + + {{ end }} + {{ end }} + +
TokenLast usedActions
{{ .Id }}{{ .LastUsedString }} + + + +
+
+
+
+
+
+ + +
URL copied to clipboard
+
+
+
+ + + + +{{ if .EndToEndEncryption }} + + + +{{ end }} + + + +{{ template "footer" }} +{{ end }} diff --git a/internal/webserver/web/templates/html_header.tmpl b/internal/webserver/web/templates/html_header.tmpl index 1a6b144b..d5dec00e 100644 --- a/internal/webserver/web/templates/html_header.tmpl +++ b/internal/webserver/web/templates/html_header.tmpl @@ -61,6 +61,7 @@

{{.PublicName}}