changes + add uploader
This commit is contained in:
parent
52b91be870
commit
7e1c36c966
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,4 +8,5 @@ config.yaml
|
||||
config.dev.yaml
|
||||
blog/
|
||||
.data/
|
||||
data/
|
||||
|
||||
|
11
go.mod
11
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
|
||||
|
26
go.sum
26
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=
|
||||
|
@ -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),
|
||||
|
68
internal/apps/uploader/crypto.go
Normal file
68
internal/apps/uploader/crypto.go
Normal file
@ -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[:])
|
||||
}
|
1
internal/apps/uploader/locales/en.json
Normal file
1
internal/apps/uploader/locales/en.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
1
internal/apps/uploader/locales/nl.json
Normal file
1
internal/apps/uploader/locales/nl.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
196
internal/apps/uploader/main.go
Normal file
196
internal/apps/uploader/main.go
Normal file
@ -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)
|
||||
}
|
@ -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"`
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
12
internal/queries/app_uploader.sql
Normal file
12
internal/queries/app_uploader.sql
Normal file
@ -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;
|
||||
|
87
internal/queries/app_uploader.sql.go
Normal file
87
internal/queries/app_uploader.sql.go
Normal file
@ -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
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// sqlc v1.25.0
|
||||
|
||||
package queries
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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 (
|
||||
|
22
migrations/00000003_uploader.up.sql
Normal file
22
migrations/00000003_uploader.up.sql
Normal file
@ -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)
|
||||
);
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -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"
|
||||
},
|
||||
|
@ -18,15 +18,17 @@
|
||||
<span style="font-size: 105px;">{{.Flag}}</span>
|
||||
</div>
|
||||
|
||||
<div hx-boost="true" class="flex justify-center flex-wrap">
|
||||
{{range .Answers}}
|
||||
<div hx-boost="true" id="answers" class="flex justify-center flex-wrap">
|
||||
{{range $index, $answer := .Answers}}
|
||||
<form method="post" class="m-1">
|
||||
<input class="hidden" name="type" value="answer" />
|
||||
<input class="hidden" name="answer" value="{{.}}" />
|
||||
{{if eq . $.PreviousError}}
|
||||
<button disabled type="submit" class="btn btn-lg btn-error rounded-2xl">{{index $.T .}}</button>
|
||||
<button disabled type="submit" class="btn btn-lg btn-error rounded-2xl">{{index $.T $answer}}<kbd
|
||||
class="hidden lg:block kbd kbd-sm">{{index $.ShortcutKeys $index}}</kbd></button>
|
||||
{{else}}
|
||||
<button type="submit" class="btn btn-lg rounded-2xl">{{index $.T .}}</button>
|
||||
<button type="submit" class="btn btn-lg rounded-2xl">{{index $.T $answer}} <kbd
|
||||
class="hidden lg:block kbd kbd-sm">{{index $.ShortcutKeys $index}}</kbd></button>
|
||||
{{end}}
|
||||
</form>
|
||||
{{end}}
|
||||
@ -55,3 +57,32 @@
|
||||
startCountdown();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const keyMap = {
|
||||
'd': 0,
|
||||
'f': 1,
|
||||
'h': 2,
|
||||
'j': 3,
|
||||
'k': 4
|
||||
};
|
||||
|
||||
function triggerButton(key) {
|
||||
const index = keyMap[key];
|
||||
if (index !== undefined) {
|
||||
const buttons = document.querySelectorAll('#answers button');
|
||||
if (buttons[index]) {
|
||||
buttons[index].click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
const key = e.key.toLowerCase();
|
||||
if (keyMap.hasOwnProperty(key)) {
|
||||
triggerButton(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -1 +1,7 @@
|
||||
<div>{{.ShareKey}}</div>
|
||||
<div>
|
||||
<div class="text-center text-2xl my-4">{{.ShareKey}}</div>
|
||||
|
||||
<form hx-boost="true" method="get" class="justify-center flex mt-4">
|
||||
<button type="submit" class="btn text-center">OK</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,45 +1,40 @@
|
||||
<div hx-boost="true">
|
||||
<div id="error-message" class="text-red-500 mb-4"></div>
|
||||
<form method="post" hx-on="htmx:responseError:
|
||||
document.getElementById('error-message').innerHTML = '{{.T.error_long}}: ' + event.detail.xhr.responseText;
|
||||
htmx.trigger(this, 'htmx:swap', { target: '#error-message', swap: 'innerHTML' });">
|
||||
<div hx-boost="true"> <div id="error-message" class="text-red-500 mb-4"></div>
|
||||
<form method="post" hx-on="htmx:responseError:
|
||||
document.getElementById('error-message').innerHTML = '{{.T.error_long}}:
|
||||
' + event.detail.xhr.responseText; htmx.trigger(this, 'htmx:swap', {
|
||||
target: '#error-message', swap: 'innerHTML' });">
|
||||
<input type="text" class="hidden" name="type" value="start" />
|
||||
|
||||
<div>
|
||||
{{range .SupportedTags}}
|
||||
<label class="cursor-pointer label">
|
||||
<span class="label-text">{{index $.T .}}</span>
|
||||
<input name="tags" value="{{.}}" type="checkbox" class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
{{end}}
|
||||
<div> {{range .SupportedTags}} <label class="cursor-pointer label">
|
||||
<span class="label-text">{{index $.T .}}</span> <input
|
||||
name="tags" value="{{.}}" type="checkbox" class="checkbox
|
||||
checkbox-primary" />
|
||||
</label> {{end}}
|
||||
</div>
|
||||
|
||||
<label class="cursor-pointer label mt-5">
|
||||
<span class="label-text">Share</span>
|
||||
<input name="share" type="checkbox" class="checkbox checkbox-primary" />
|
||||
{{if .SignedIn}}
|
||||
<label class="cursor-pointer label mt-5"> <span
|
||||
class="label-text">Share</span> <input name="share" type="checkbox"
|
||||
class="checkbox checkbox-primary" />
|
||||
</label>
|
||||
{{end}}
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<span>max questions</span>
|
||||
<input value="0" placeholder="max questions" name="max_questions" type="number"
|
||||
class="input input-bordered" />
|
||||
</label>
|
||||
<div> <label>
|
||||
<span>max questions</span> <input value="0" placeholder="max
|
||||
questions" name="max_questions" type="number"
|
||||
class="input input-bordered" /> </label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<span>time limit</span>
|
||||
<input value="0" placeholder="seconds time limit" name="seconds" type="number"
|
||||
class="input input-bordered" />
|
||||
</label>
|
||||
<div> <label>
|
||||
<span>time limit</span> <input value="0" placeholder="seconds
|
||||
time limit" name="seconds" type="number"
|
||||
class="input input-bordered" /> </label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
<form class="mt-4" method="post">
|
||||
<input type="text" class="hidden" name="type" value="shared" />
|
||||
<input type="text" class="input input-bordered" name="sharekey" placeholder="sharekey" />
|
||||
<button type="submit" class="btn btn-primary">Shared</button>
|
||||
<button type="submit" class="btn btn-primary">Submit</button> </form>
|
||||
<form class="mt-4" method="post"> <input type="text" class="hidden"
|
||||
name="type" value="shared" /> <input type="text" class="input
|
||||
input-bordered" name="sharekey" placeholder="sharekey" /> <button
|
||||
type="submit" class="btn btn-primary">Shared</button>
|
||||
</form>
|
||||
</div>
|
||||
|
12
web/views/apps/uploader/index.html
Normal file
12
web/views/apps/uploader/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<form enctype="multipart/form-data" method="post" hx-boost="true">
|
||||
<label>
|
||||
<span>Max downloaders</span>
|
||||
<input name="max_downloads" required type="number" value="1" placeholder="Max downloaders" class="input input-bordered" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Expire in (days)</span>
|
||||
<input name="expire_days" required type="number" value="1" placeholder="Expire in days from now" class="input input-bordered" />
|
||||
</label>
|
||||
<input required type="file" name="file" class="file-input file-input-bordered w-full max-w-xs" />
|
||||
<input class="btn" type="submit" value="Upload">
|
||||
</form>
|
1
web/views/apps/uploader/uploaded.html
Normal file
1
web/views/apps/uploader/uploaded.html
Normal file
@ -0,0 +1 @@
|
||||
<div>{{.Key}}</div>
|
Loading…
Reference in New Issue
Block a user