diff --git a/.gitignore b/.gitignore index 9559a22..1adb88f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ config.yaml config.dev.yaml blog/ .data/ +data/ diff --git a/go.mod b/go.mod index 62fa78a..f9dd9d4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.5 require ( github.com/coreos/go-oidc/v3 v3.11.0 + github.com/enescakir/emoji v1.0.0 github.com/gofiber/contrib/fiberzerolog v1.0.2 github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/template/html/v2 v2.1.2 @@ -20,11 +21,10 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/enescakir/emoji v1.0.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect @@ -33,21 +33,14 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/kr/text v0.2.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/shirou/gopsutil/v3 v3.24.5 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.55.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect diff --git a/go.sum b/go.sum index d073d00..ea043f4 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,6 @@ github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/contrib/fiberzerolog v1.0.2 h1:LMa/luarQVeINoRwZLHtLQYepLPDIwUNB5OmdZKk+s8= github.com/gofiber/contrib/fiberzerolog v1.0.2/go.mod h1:aTPsgArSgxRWcUeJ/K6PiICz3mbQENR1QOR426QwOoQ= @@ -42,9 +40,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -70,8 +67,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -92,8 +87,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -102,19 +95,11 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= -github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= @@ -125,8 +110,6 @@ github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= @@ -139,12 +122,8 @@ golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -152,7 +131,6 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/apps/flags/handlers.go b/internal/apps/flags/handlers.go index 2781fc6..e1adac3 100644 --- a/internal/apps/flags/handlers.go +++ b/internal/apps/flags/handlers.go @@ -12,7 +12,6 @@ import ( "git.tijl.dev/tijl/tijl.dev-core/internal/utils" "git.tijl.dev/tijl/tijl.dev-core/modules/db" "git.tijl.dev/tijl/tijl.dev-core/modules/i18n" - log "git.tijl.dev/tijl/tijl.dev-core/modules/logger" "git.tijl.dev/tijl/tijl.dev-core/modules/web" "github.com/enescakir/emoji" "github.com/gofiber/fiber/v2" @@ -148,8 +147,8 @@ func questionHandler(c *fiber.Ctx, newGame NullableUUID, prevError NullableStrin data["Answers"] = shuffledAnswers data["Flag"] = flag data["Errors"] = gameSession.QuestionsErrors - log.Debug().Msg(prevError.String) data["PreviousError"] = prevError.String + data["ShortcutKeys"] = []string{"d", "f", "h", "j", "k"} return c.Render("apps/flags/question", data, "layouts/base") } @@ -202,8 +201,14 @@ func startNewGameHandler(c *fiber.Ctx) error { } func createSharedGameData(c *fiber.Ctx, tags []string, maxQuestions int, seconds int) error { + uid, err := user.GetSession(c) + if err != nil { + return c.SendStatus(http.StatusUnauthorized) + } + shareKey := utils.RandString(4) - _, err := db.Queries.AppFlagsNewSharedData(context.TODO(), queries.AppFlagsNewSharedDataParams{ + _, err = db.Queries.AppFlagsNewSharedData(context.TODO(), queries.AppFlagsNewSharedDataParams{ + Uid: uid, ShareKey: shareKey, Tags: tags, Questions: int32(maxQuestions), diff --git a/internal/apps/uploader/crypto.go b/internal/apps/uploader/crypto.go new file mode 100644 index 0000000..e328e81 --- /dev/null +++ b/internal/apps/uploader/crypto.go @@ -0,0 +1,68 @@ +package uploader + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha512" + "encoding/hex" + "fmt" + "io" +) + +func generateEncryptionKey() ([]byte, error) { + key := make([]byte, 32) // AES-256 32-byte key + if _, err := rand.Read(key); err != nil { + return nil, err + } + return key, nil +} + +func encryptData(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func decryptData(encryptedData []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(encryptedData) < nonceSize { + return nil, fmt.Errorf("encrypted data is too short") + } + + nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +func hashKey(key []byte) string { + hash := sha512.Sum512(key) + return hex.EncodeToString(hash[:]) +} diff --git a/internal/apps/uploader/locales/en.json b/internal/apps/uploader/locales/en.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/internal/apps/uploader/locales/en.json @@ -0,0 +1 @@ +{} diff --git a/internal/apps/uploader/locales/nl.json b/internal/apps/uploader/locales/nl.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/internal/apps/uploader/locales/nl.json @@ -0,0 +1 @@ +{} diff --git a/internal/apps/uploader/main.go b/internal/apps/uploader/main.go new file mode 100644 index 0000000..6df68c0 --- /dev/null +++ b/internal/apps/uploader/main.go @@ -0,0 +1,196 @@ +package uploader + +import ( + "context" + "database/sql" + "embed" + "encoding/hex" + "encoding/json" + "net/http" + "os" + "path/filepath" + "strconv" + "time" + + "git.tijl.dev/tijl/tijl.dev-core/internal/config" + "git.tijl.dev/tijl/tijl.dev-core/internal/queries" + "git.tijl.dev/tijl/tijl.dev-core/internal/user" + "git.tijl.dev/tijl/tijl.dev-core/modules/db" + "git.tijl.dev/tijl/tijl.dev-core/modules/i18n" + "git.tijl.dev/tijl/tijl.dev-core/modules/web" + "github.com/gofiber/fiber/v2" +) + +//go:embed locales/* +var Embed embed.FS + +func Setup() { + i18n.RegisterTranslations(Embed, "locales") + + var uploadDir string = filepath.Join(config.Config.DataLocation, "./uploader") + + + web.RegisterAppSetupFunc(func(a *fiber.App) { + a.Get("/app/uploader", func(c *fiber.Ctx) error { + data := *web.Common(c) + data["Title"] = "tmp" + return c.Render("apps/uploader/index", data, "layouts/base") + }) + a.Get("/app/uploader/:key", func(c *fiber.Ctx) error { + encryptionKey, err := hex.DecodeString(c.Params("key")) + if err != nil { + return err + } + storageKey := hashKey(encryptionKey) + + row, err := db.Queries.AppUploaderGet(context.TODO(), storageKey) + if err != nil { + return c.Next() + } + + if row.Expire.UnixMilli() < time.Now().UnixMilli() { + return c.Next() + } + + accessCount, err := db.Queries.AppUploaderAccessCount(context.TODO(), row.ID) + if err != nil { + return err + } + + if accessCount >= int64(row.MaxVisits) { + return c.Next() + } + + createLog := queries.AppUploaderAccessCreateParams{ + FileID: row.ID, + IpAddress: c.IP(), + Agent: string(c.Context().UserAgent()), + } + + user, err := user.GetSession(c) + if err == nil { + createLog.Uid = sql.NullString{ + Valid: true, + String: user, + } + } + + db.Queries.AppUploaderAccessCreate(context.TODO(), createLog) + + encryptedContent, err := os.ReadFile(filepath.Join(uploadDir, row.ID.String())) + if err != nil { + return err + } + + encryptedMetadata, err := os.ReadFile(filepath.Join(uploadDir, row.ID.String()+".meta")) + if err != nil { + return err + } + + decryptedContent, err := decryptData(encryptedContent, encryptionKey) + if err != nil { + return err + } + + decryptedMetadata, err := decryptData(encryptedMetadata, encryptionKey) + if err != nil { + return err + } + + var metadata map[string]interface{} + if err := json.Unmarshal(decryptedMetadata, &metadata); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to parse metadata") + } + + c.Set("Content-Disposition", "attachment; filename="+metadata["filename"].(string)) + c.Set("Content-Type", metadata["content_type"].(string)) + + return c.Send(decryptedContent) + }) + + + a.Post("/app/uploader", func(c *fiber.Ctx) error { + uid, err := user.GetSession(c) + if err != nil { + return c.SendStatus(http.StatusUnauthorized) + } + + expireDays, err := strconv.Atoi(c.FormValue("expire_days")) + if err != nil {return err} + maxDownloads, err := strconv.Atoi(c.FormValue("max_downloads")) + if err != nil {return err} + + file, err := c.FormFile("file") + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString("Failed to read file") + } + + src, err := file.Open() + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to open file") + } + defer src.Close() + + // Read file content into a byte slice + fileContent := make([]byte, file.Size) + if _, err := src.Read(fileContent); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to read file content") + } + + // Generate encryption key + encryptionKey, err := generateEncryptionKey() + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to generate encryption key") + } + + // Encrypt the file content + encryptedContent, err := encryptData(fileContent, encryptionKey) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to encrypt file content") + } + + storageKey := hashKey(encryptionKey) + + id, err := db.Queries.AppUploaderCreate(context.TODO(), queries.AppUploaderCreateParams{ + Uid: uid, + FileCrypto: storageKey, + Expire: time.Now().AddDate(0, 0, expireDays), + MaxVisits: int32(maxDownloads), + }) + if err != nil { + return err + } + + // Save encrypted file content + if err := os.WriteFile(filepath.Join(uploadDir, id.String()), encryptedContent, 0644); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to save encrypted file") + } + + metadata := map[string]interface{}{ + "filename": file.Filename, + "content_type": file.Header.Get("Content-Type"), + } + + metadataBytes, err := json.Marshal(metadata) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to marshal metadata") + } + + encryptedMetadata, err := encryptData(metadataBytes, encryptionKey) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to encrypt metadata") + } + + if err := os.WriteFile(filepath.Join(uploadDir, id.String()+".meta"), encryptedMetadata, 0644); err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to save encrypted metadata") + } + + data := *web.Common(c) + data["Title"] = "tmp" + data["Key"] = hex.EncodeToString(encryptionKey) + return c.Render("apps/uploader/uploaded", data, "layouts/base") + }) + + + }, 1000) +} diff --git a/internal/config/config.go b/internal/config/config.go index 737661b..b7b22da 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,7 +31,7 @@ func Load() { type ConfigType struct { Host string `yaml:"host"` Port int `yaml:"port"` - BlogLocation string `yaml:"blog_location"` + DataLocation string `yaml:"data_location"` UrlBase string `yaml:"url_base"` JsonLogging bool `yaml:"log_json"` DB string `yaml:"database"` diff --git a/internal/handlers/blog.go b/internal/handlers/blog.go index d6fe84d..519854a 100644 --- a/internal/handlers/blog.go +++ b/internal/handlers/blog.go @@ -10,6 +10,7 @@ import ( "path/filepath" "time" + "git.tijl.dev/tijl/tijl.dev-core/internal/config" "git.tijl.dev/tijl/tijl.dev-core/modules/i18n" log "git.tijl.dev/tijl/tijl.dev-core/modules/logger" "git.tijl.dev/tijl/tijl.dev-core/modules/web" @@ -48,7 +49,7 @@ func LoadPosts() { ), ) - files, err := filepath.Glob("blog/*.md") + files, err := filepath.Glob(filepath.Join(config.Config.DataLocation, "blog/*.md")) if err != nil { log.Error().Err(err).Msg("error reading folder") } diff --git a/internal/queries/app_flags.sql b/internal/queries/app_flags.sql index 6e7465c..d1da69f 100644 --- a/internal/queries/app_flags.sql +++ b/internal/queries/app_flags.sql @@ -32,7 +32,7 @@ VALUES ($1, $2, $3) DO UPDATE SET errors = app_flags_games_answers.errors + EXCLUDED.errors; -- name: AppFlagsNewSharedData :one -INSERT INTO app_flags_games_shared_data (share_key, game_seed, tags, questions, seconds) VALUES ($1, $2, $3, $4, $5) RETURNING game_seed; +INSERT INTO app_flags_games_shared_data (uid, share_key, game_seed, tags, questions, seconds) VALUES ($1, $2, $3, $4, $5, $6) RETURNING game_seed; -- name: AppFlagsGetSharedData :one SELECT * FROM app_flags_games_shared_data WHERE share_key = $1 LIMIT 1; diff --git a/internal/queries/app_flags.sql.go b/internal/queries/app_flags.sql.go index 8133021..34323b3 100644 --- a/internal/queries/app_flags.sql.go +++ b/internal/queries/app_flags.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.25.0 // source: app_flags.sql package queries @@ -93,7 +93,7 @@ func (q *Queries) AppFlagsGetGame(ctx context.Context, gameID uuid.UUID) (AppFla } const appFlagsGetSharedData = `-- name: AppFlagsGetSharedData :one -SELECT id, share_key, game_seed, questions, tags, seconds, created_at FROM app_flags_games_shared_data WHERE share_key = $1 LIMIT 1 +SELECT id, uid, share_key, game_seed, questions, tags, seconds, created_at FROM app_flags_games_shared_data WHERE share_key = $1 LIMIT 1 ` func (q *Queries) AppFlagsGetSharedData(ctx context.Context, shareKey string) (AppFlagsGamesSharedDatum, error) { @@ -101,6 +101,7 @@ func (q *Queries) AppFlagsGetSharedData(ctx context.Context, shareKey string) (A var i AppFlagsGamesSharedDatum err := row.Scan( &i.ID, + &i.Uid, &i.ShareKey, &i.GameSeed, &i.Questions, @@ -112,10 +113,11 @@ func (q *Queries) AppFlagsGetSharedData(ctx context.Context, shareKey string) (A } const appFlagsNewSharedData = `-- name: AppFlagsNewSharedData :one -INSERT INTO app_flags_games_shared_data (share_key, game_seed, tags, questions, seconds) VALUES ($1, $2, $3, $4, $5) RETURNING game_seed +INSERT INTO app_flags_games_shared_data (uid, share_key, game_seed, tags, questions, seconds) VALUES ($1, $2, $3, $4, $5, $6) RETURNING game_seed ` type AppFlagsNewSharedDataParams struct { + Uid string ShareKey string GameSeed uuid.UUID Tags []string @@ -125,6 +127,7 @@ type AppFlagsNewSharedDataParams struct { func (q *Queries) AppFlagsNewSharedData(ctx context.Context, arg AppFlagsNewSharedDataParams) (uuid.UUID, error) { row := q.db.QueryRowContext(ctx, appFlagsNewSharedData, + arg.Uid, arg.ShareKey, arg.GameSeed, pq.Array(arg.Tags), diff --git a/internal/queries/app_uploader.sql b/internal/queries/app_uploader.sql new file mode 100644 index 0000000..92a6b00 --- /dev/null +++ b/internal/queries/app_uploader.sql @@ -0,0 +1,12 @@ +-- name: AppUploaderCreate :one +INSERT INTO app_uploader_files (uid, file_crypto, expire, max_visits) VALUES ($1, $2, $3, $4) RETURNING id; + +-- name: AppUploaderGet :one +SELECT * FROM app_uploader_files WHERE file_crypto = $1 LIMIT 1; + +-- name: AppUploaderAccessCreate :exec +INSERT INTO app_uploader_files_access (file_id, uid, ip_address, agent) VALUES ($1, $2, $3, $4); + +-- name: AppUploaderAccessCount :one +SELECT COUNT(*) AS file_accessors FROM app_uploader_files_access WHERE file_id = $1; + diff --git a/internal/queries/app_uploader.sql.go b/internal/queries/app_uploader.sql.go new file mode 100644 index 0000000..81dc7cc --- /dev/null +++ b/internal/queries/app_uploader.sql.go @@ -0,0 +1,87 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: app_uploader.sql + +package queries + +import ( + "context" + "database/sql" + "time" + + "github.com/google/uuid" +) + +const appUploaderAccessCount = `-- name: AppUploaderAccessCount :one +SELECT COUNT(*) AS file_accessors FROM app_uploader_files_access WHERE file_id = $1 +` + +func (q *Queries) AppUploaderAccessCount(ctx context.Context, fileID uuid.UUID) (int64, error) { + row := q.db.QueryRowContext(ctx, appUploaderAccessCount, fileID) + var file_accessors int64 + err := row.Scan(&file_accessors) + return file_accessors, err +} + +const appUploaderAccessCreate = `-- name: AppUploaderAccessCreate :exec +INSERT INTO app_uploader_files_access (file_id, uid, ip_address, agent) VALUES ($1, $2, $3, $4) +` + +type AppUploaderAccessCreateParams struct { + FileID uuid.UUID + Uid sql.NullString + IpAddress string + Agent string +} + +func (q *Queries) AppUploaderAccessCreate(ctx context.Context, arg AppUploaderAccessCreateParams) error { + _, err := q.db.ExecContext(ctx, appUploaderAccessCreate, + arg.FileID, + arg.Uid, + arg.IpAddress, + arg.Agent, + ) + return err +} + +const appUploaderCreate = `-- name: AppUploaderCreate :one +INSERT INTO app_uploader_files (uid, file_crypto, expire, max_visits) VALUES ($1, $2, $3, $4) RETURNING id +` + +type AppUploaderCreateParams struct { + Uid string + FileCrypto string + Expire time.Time + MaxVisits int32 +} + +func (q *Queries) AppUploaderCreate(ctx context.Context, arg AppUploaderCreateParams) (uuid.UUID, error) { + row := q.db.QueryRowContext(ctx, appUploaderCreate, + arg.Uid, + arg.FileCrypto, + arg.Expire, + arg.MaxVisits, + ) + var id uuid.UUID + err := row.Scan(&id) + return id, err +} + +const appUploaderGet = `-- name: AppUploaderGet :one +SELECT id, uid, file_crypto, created_at, expire, max_visits FROM app_uploader_files WHERE file_crypto = $1 LIMIT 1 +` + +func (q *Queries) AppUploaderGet(ctx context.Context, fileCrypto string) (AppUploaderFile, error) { + row := q.db.QueryRowContext(ctx, appUploaderGet, fileCrypto) + var i AppUploaderFile + err := row.Scan( + &i.ID, + &i.Uid, + &i.FileCrypto, + &i.CreatedAt, + &i.Expire, + &i.MaxVisits, + ) + return i, err +} diff --git a/internal/queries/db.go b/internal/queries/db.go index 1cbab90..ef8b0c2 100644 --- a/internal/queries/db.go +++ b/internal/queries/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.25.0 package queries diff --git a/internal/queries/models.go b/internal/queries/models.go index d9ca983..34aaeb2 100644 --- a/internal/queries/models.go +++ b/internal/queries/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.25.0 package queries @@ -33,6 +33,7 @@ type AppFlagsGamesAnswer struct { type AppFlagsGamesSharedDatum struct { ID int32 + Uid string ShareKey string GameSeed uuid.UUID Questions int32 @@ -41,6 +42,23 @@ type AppFlagsGamesSharedDatum struct { CreatedAt time.Time } +type AppUploaderFile struct { + ID uuid.UUID + Uid string + FileCrypto string + CreatedAt time.Time + Expire time.Time + MaxVisits int32 +} + +type AppUploaderFilesAccess struct { + FileID uuid.UUID + Uid sql.NullString + IpAddress string + Agent string + AccessTime time.Time +} + type Session struct { ID int32 Uid string diff --git a/internal/queries/sessions.sql.go b/internal/queries/sessions.sql.go index 4db8d36..c1ec560 100644 --- a/internal/queries/sessions.sql.go +++ b/internal/queries/sessions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.25.0 // source: sessions.sql package queries diff --git a/internal/queries/users.sql.go b/internal/queries/users.sql.go index 0a8a073..3f51e44 100644 --- a/internal/queries/users.sql.go +++ b/internal/queries/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.25.0 // source: users.sql package queries diff --git a/internal/queries/util.sql.go b/internal/queries/util.sql.go index a5cf809..936ebeb 100644 --- a/internal/queries/util.sql.go +++ b/internal/queries/util.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.25.0 // source: util.sql package queries diff --git a/internal/service/main.go b/internal/service/main.go index 7fe3f1f..8a342b9 100644 --- a/internal/service/main.go +++ b/internal/service/main.go @@ -6,6 +6,7 @@ import ( "net/http" "git.tijl.dev/tijl/tijl.dev-core/internal/apps/flags" + "git.tijl.dev/tijl/tijl.dev-core/internal/apps/uploader" "git.tijl.dev/tijl/tijl.dev-core/internal/assets" "git.tijl.dev/tijl/tijl.dev-core/internal/config" "git.tijl.dev/tijl/tijl.dev-core/internal/handlers" @@ -43,6 +44,7 @@ func Listen() { // setup apps flags.Setup() + uploader.Setup() // setup web webinternal.Load() diff --git a/migrations/00000001_init.up.sql b/migrations/00000001_init.up.sql index 4c69198..8f75f09 100644 --- a/migrations/00000001_init.up.sql +++ b/migrations/00000001_init.up.sql @@ -1,6 +1,6 @@ CREATE TABLE users ( id SERIAL PRIMARY KEY, - uid VARCHAR UNIQUE NOT NULL, -- username as unique identifier + uid VARCHAR NOT NULL UNIQUE, -- username as unique identifier email VARCHAR NOT NULL, email_verified BOOLEAN NOT NULL, full_name VARCHAR NOT NULL, diff --git a/migrations/00000002_flags.down.sql b/migrations/00000002_flags.down.sql index e69de29..dfee408 100644 --- a/migrations/00000002_flags.down.sql +++ b/migrations/00000002_flags.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS app_flags_games; +DROP TABLE IF EXISTS app_flags_games_answers; +DROP TABLE IF EXISTS app_flags_games_shared_data; diff --git a/migrations/00000002_flags.up.sql b/migrations/00000002_flags.up.sql index 9efa3da..9bd6703 100644 --- a/migrations/00000002_flags.up.sql +++ b/migrations/00000002_flags.up.sql @@ -13,13 +13,15 @@ CREATE TABLE app_flags_games ( ); CREATE TABLE app_flags_games_shared_data ( - id SERIAL PRIMARY KEY, + id SERIAL PRIMARY KEY UNIQUE, + uid VARCHAR NOT NULL, share_key VARCHAR NOT NULL, game_seed UUID NOT NULL DEFAULT gen_random_uuid(), questions INT DEFAULT 0 NOT NULL, tags VARCHAR[] NOT NULL, seconds INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (uid) REFERENCES users (uid) ); CREATE TABLE app_flags_games_answers ( diff --git a/migrations/00000003_uploader.up.sql b/migrations/00000003_uploader.up.sql new file mode 100644 index 0000000..54fc0c0 --- /dev/null +++ b/migrations/00000003_uploader.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE app_uploader_files ( + id UUID PRIMARY KEY UNIQUE NOT NULL DEFAULT gen_random_uuid(), + uid VARCHAR NOT NULL, + file_crypto VARCHAR NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + -- settings + expire TIMESTAMP NOT NULL, + max_visits INT NOT NULL, + FOREIGN KEY (uid) REFERENCES users (uid) +); + +CREATE TABLE app_uploader_files_access ( + file_id UUID NOT NULL, + -- info about user who accessed + uid VARCHAR DEFAULT NULL, -- link to logged in user + ip_address VARCHAR NOT NULL, + agent VARCHAR NOT NULL, + access_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (file_id) REFERENCES app_uploader_files (id), + FOREIGN KEY (uid) REFERENCES users (uid) +); + diff --git a/package-lock.json b/package-lock.json index a363cd9..9597b32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "tijldev-next", + "name": "tijldev-core", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tijldev-next", + "name": "tijldev-core", "dependencies": { "htmx.org": "^1.9.12" }, diff --git a/web/views/apps/flags/question.html b/web/views/apps/flags/question.html index 40b8613..41ac312 100644 --- a/web/views/apps/flags/question.html +++ b/web/views/apps/flags/question.html @@ -18,15 +18,17 @@ {{.Flag}} -
- {{range .Answers}} +
+ {{range $index, $answer := .Answers}}
{{if eq . $.PreviousError}} - + {{else}} - + {{end}}
{{end}} @@ -55,3 +57,32 @@ startCountdown(); {{end}} + + diff --git a/web/views/apps/flags/shared.html b/web/views/apps/flags/shared.html index 4b3c45a..9de8846 100644 --- a/web/views/apps/flags/shared.html +++ b/web/views/apps/flags/shared.html @@ -1 +1,7 @@ -
{{.ShareKey}}
+
+
{{.ShareKey}}
+ +
+ +
+
diff --git a/web/views/apps/flags/start.html b/web/views/apps/flags/start.html index 3b964fb..841cbc8 100644 --- a/web/views/apps/flags/start.html +++ b/web/views/apps/flags/start.html @@ -1,45 +1,40 @@ -
-
-
+
+ -
- {{range .SupportedTags}} - - {{end}} +
{{range .SupportedTags}} {{end}}
-