diff --git a/services/pocketbase/.gitignore b/services/pocketbase/.gitignore
index e122e2de..fb09640c 100644
--- a/services/pocketbase/.gitignore
+++ b/services/pocketbase/.gitignore
@@ -2,4 +2,33 @@
node_modules
pb_data
.DS_Store
-.secrets
\ No newline at end of file
+.secrets
+
+
+# Created by https://www.toptal.com/developers/gitignore/api/go
+# Edit at https://www.toptal.com/developers/gitignore?templates=go
+
+### Go ###
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+# End of https://www.toptal.com/developers/gitignore/api/go
diff --git a/services/pocketbase/go.mod b/services/pocketbase/go.mod
new file mode 100644
index 00000000..fec7d6ad
--- /dev/null
+++ b/services/pocketbase/go.mod
@@ -0,0 +1,46 @@
+module futureporn
+
+go 1.25.1
+
+require github.com/pocketbase/pocketbase v0.34.0
+
+require (
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+ github.com/disintegration/imaging v1.6.2 // indirect
+ github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
+ github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect
+ github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 // indirect
+ github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/fatih/color v1.18.0 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.11 // indirect
+ github.com/ganigeorgiev/fexpr v0.5.0 // indirect
+ github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
+ github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
+ github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/ncruces/go-strftime v1.0.0 // indirect
+ github.com/pocketbase/dbx v1.11.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/spf13/cast v1.10.0 // indirect
+ github.com/spf13/cobra v1.10.1 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
+ golang.org/x/image v0.33.0 // indirect
+ golang.org/x/net v0.47.0 // indirect
+ golang.org/x/oauth2 v0.33.0 // indirect
+ golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
+ modernc.org/libc v1.66.10 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+ modernc.org/sqlite v1.40.1 // indirect
+)
diff --git a/services/pocketbase/go.sum b/services/pocketbase/go.sum
new file mode 100644
index 00000000..63a8d4be
--- /dev/null
+++ b/services/pocketbase/go.sum
@@ -0,0 +1,141 @@
+github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
+github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
+github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
+github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
+github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY=
+github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM=
+github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 h1:jxmXU5V9tXxJnydU5v/m9SG8TRUa/Z7IXODBpMs/P+U=
+github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
+github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0 h1:fuHXpEVTTk7TilRdfGRLHpiTD6tnT0ihEowCfWjlFvw=
+github.com/dop251/goja_nodejs v0.0.0-20250409162600-f7acab6894b0/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
+github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
+github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
+github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
+github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
+github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
+github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+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/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
+github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
+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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
+github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+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/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
+github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
+github.com/pocketbase/pocketbase v0.34.0 h1:5W80PrGvkRYIMAIK90F7w031/hXgZVz1KSuCJqSpgJo=
+github.com/pocketbase/pocketbase v0.34.0/go.mod h1:K/9z/Zb9PR9yW2Qyoc73jHV/EKT8cMTk9bQWyrzYlvI=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
+golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
+golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
+golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
+golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
+golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
+modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
+modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
+modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
+modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
+modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
+modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/services/pocketbase/main.go b/services/pocketbase/main.go
new file mode 100644
index 00000000..61316ab5
--- /dev/null
+++ b/services/pocketbase/main.go
@@ -0,0 +1,148 @@
+// @see https://github.com/pocketbase/pocketbase/blob/master/examples/base/main.go
+
+package main
+
+import (
+ "futureporn/src/handlers"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "github.com/pocketbase/pocketbase"
+ "github.com/pocketbase/pocketbase/apis"
+ "github.com/pocketbase/pocketbase/core"
+ "github.com/pocketbase/pocketbase/plugins/ghupdate"
+ "github.com/pocketbase/pocketbase/plugins/jsvm"
+ "github.com/pocketbase/pocketbase/plugins/migratecmd"
+ "github.com/pocketbase/pocketbase/tools/hook"
+ "github.com/pocketbase/pocketbase/tools/osutils"
+)
+
+func main() {
+ app := pocketbase.New()
+
+ // ---------------------------------------------------------------
+ // Optional plugin flags:
+ // ---------------------------------------------------------------
+
+ var hooksDir string
+ app.RootCmd.PersistentFlags().StringVar(
+ &hooksDir,
+ "hooksDir",
+ "",
+ "the directory with the JS app hooks",
+ )
+
+ var hooksWatch bool
+ app.RootCmd.PersistentFlags().BoolVar(
+ &hooksWatch,
+ "hooksWatch",
+ true,
+ "auto restart the app on pb_hooks file change; it has no effect on Windows",
+ )
+
+ var hooksPool int
+ app.RootCmd.PersistentFlags().IntVar(
+ &hooksPool,
+ "hooksPool",
+ 15,
+ "the total prewarm goja.Runtime instances for the JS app hooks execution",
+ )
+
+ var migrationsDir string
+ app.RootCmd.PersistentFlags().StringVar(
+ &migrationsDir,
+ "migrationsDir",
+ "",
+ "the directory with the user defined migrations",
+ )
+
+ var automigrate bool
+ app.RootCmd.PersistentFlags().BoolVar(
+ &automigrate,
+ "automigrate",
+ true,
+ "enable/disable auto migrations",
+ )
+
+ var publicDir string
+ app.RootCmd.PersistentFlags().StringVar(
+ &publicDir,
+ "publicDir",
+ defaultPublicDir(),
+ "the directory to serve static files",
+ )
+
+ var indexFallback bool
+ app.RootCmd.PersistentFlags().BoolVar(
+ &indexFallback,
+ "indexFallback",
+ true,
+ "fallback the request to index.html on missing static path, e.g. when pretty urls are used with SPA",
+ )
+
+ app.RootCmd.ParseFlags(os.Args[1:])
+
+ // ---------------------------------------------------------------
+ // Plugins and hooks:
+ // ---------------------------------------------------------------
+
+ // load jsvm (pb_hooks and pb_migrations)
+ jsvm.MustRegister(app, jsvm.Config{
+ MigrationsDir: migrationsDir,
+ HooksDir: hooksDir,
+ HooksWatch: hooksWatch,
+ HooksPoolSize: hooksPool,
+ })
+
+ // migrate command (with js templates)
+ migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
+ TemplateLang: migratecmd.TemplateLangJS,
+ Automigrate: automigrate,
+ Dir: migrationsDir,
+ })
+
+ // GitHub selfupdate
+ ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{})
+
+ // route for generating hls playlists with BunnyCDN signed URLs
+ app.OnServe().BindFunc(func(se *core.ServeEvent) error {
+ se.Router.GET("/api/hls/{collectionId}/{recordId}/hls/{fileName}", handlers.PackageHandler)
+ return se.Next()
+ })
+
+ // route for direct downloads (patrons only)
+ app.OnServe().BindFunc(func(se *core.ServeEvent) error {
+ se.Router.GET("/api/download/{collectionId}/{recordId}/{fileName}", handlers.S3Download)
+ return se.Next()
+ })
+
+ // static route to serves files from the provided public dir
+ // (if publicDir exists and the route path is not already defined)
+ app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
+ Func: func(e *core.ServeEvent) error {
+ if !e.Router.HasRoute(http.MethodGet, "/{path...}") {
+ e.Router.GET("/{path...}", apis.Static(os.DirFS(publicDir), indexFallback))
+ }
+
+ return e.Next()
+ },
+ Priority: 999, // execute as latest as possible to allow users to provide their own route
+ })
+
+ app.OnFileDownloadRequest().BindFunc(handlers.HandleFileDownload)
+
+ if err := app.Start(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+// the default pb_public dir location is relative to the executable
+func defaultPublicDir() string {
+ if osutils.IsProbablyGoRun() {
+ return "./pb_public"
+ }
+
+ return filepath.Join(os.Args[0], "../pb_public")
+}
diff --git a/services/pocketbase/package.json b/services/pocketbase/package.json
index 771b24aa..5d21b77f 100644
--- a/services/pocketbase/package.json
+++ b/services/pocketbase/package.json
@@ -1,6 +1,6 @@
{
"name": "futureporn",
- "version": "4.0.0",
+ "version": "4.2.0",
"private": true,
"description": "Dedication to the preservation of lewdtuber history",
"license": "Unlicense",
diff --git a/services/pocketbase/pb_hooks/cdn.pb.js b/services/pocketbase/pb_hooks/cdn.pb.js.noexec
similarity index 52%
rename from services/pocketbase/pb_hooks/cdn.pb.js
rename to services/pocketbase/pb_hooks/cdn.pb.js.noexec
index 21d0aa8b..209b0e5f 100644
--- a/services/pocketbase/pb_hooks/cdn.pb.js
+++ b/services/pocketbase/pb_hooks/cdn.pb.js.noexec
@@ -1,7 +1,5 @@
///
-
-
/**
* onFileDownloadRequest hook is triggered before each API File download request. Could be used to validate or modify the file response before returning it to the client.
* @see https://pocketbase.io/docs/js-event-hooks/#onfiledownloadrequest
@@ -13,7 +11,12 @@
onFileDownloadRequest((event) => {
- // console.log('event', JSON.stringify(event))
+ console.log('>>> We got a file download request event', JSON.stringify(event))
+
+
+ console.log('the below the following is event.request');
+ console.log(JSON.stringify(event.request));
+ console.log('the above the previous line is event.request');
// e.app
// e.collection
// e.record
@@ -24,7 +27,7 @@ onFileDownloadRequest((event) => {
const securityKey = process.env?.BUNNY_TOKEN_KEY;
const baseUrl = process.env?.BUNNY_ZONE_URL;
- // console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
+ console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
if (!securityKey) {
console.error('BUNNY_TOKEN_KEY was missing from env');
@@ -37,34 +40,52 @@ onFileDownloadRequest((event) => {
/**
* Generate a signed BunnyCDN URL.
+ *
+ * @see https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication
+ *
* @param {string} securityKey - Your BunnyCDN security token
- * @param {string} baseUrl - The base URL (protocol + host)
- * @param {string} path - Path to the file (starting with /)
- * @param {string} rawQuery - Raw query string, e.g., "width=500&quality=5"
+ * @param {string} inputUrl - The base URL (protocol + host + path + (optional) qs)
* @param {number} expires - Unix timestamp for expiration
+ * @param {string} tokenPath - (optional) create a token that has access to any file within that path
*/
- function signUrlCool(securityKey, baseUrl, path, rawQuery = "", expires) {
+ function signUrlCool(securityKey, inputUrl, expires, tokenPath = '') {
- if (!path.startsWith('/')) path = '/' + path;
- if (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
+ if (inputUrl.endsWith('/')) throw new Error(`url must not end with a slash. got inputUrl=${inputUrl}`);
+
+
+ // reading query parameters
+ // let search = e.request.url.query().get("search")
+ const URL = event.request.url
+
+
+
+ let parsedURL = URL.parse(inputUrl); // this is pre-parsed. no need to parse again
+ let params = (URL.parse(parsedURL)).searchParams; // these are pre-parsed as event.request.url.rawQuery. no need to parse again
+ let signaturePath = '';
+ let parameterData = '';
+ let parameterDataUrl = '';
+
+ console.log(`eyy we got parsedURL=${parsedURL}`);
+
+ throw new Error('@todo');
// Build parameter string (sort keys alphabetically)
- let parameterData = "";
- if (rawQuery) {
- const params = rawQuery
- .split("&")
- .map(p => p.split("="))
- .filter(([key]) => key && key !== "token" && key !== "expires")
- .sort(([a], [b]) => a.localeCompare(b));
+ // let parameterData = "";
+ // if (rawQuery) {
+ // const params = rawQuery
+ // .split("&")
+ // .map(p => p.split("="))
+ // .filter(([key]) => key && key !== "token" && key !== "expires")
+ // .sort(([a], [b]) => a.localeCompare(b));
- if (params.length) {
- parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
- }
- }
+ // if (params.length) {
+ // parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
+ // }
+ // }
// Build hashable base
const hashableBase = securityKey + path + expires + parameterData;
- // console.log(`hashableBase`, hashableBase)
+ console.log(`hashableBase`, hashableBase)
// Compute token using your $security.sha256 workflow
const tokenH = $security.sha256(hashableBase);
@@ -79,6 +100,7 @@ onFileDownloadRequest((event) => {
let tokenUrl = baseUrl + path + "?token=" + token;
if (parameterData) tokenUrl += "&" + parameterData;
tokenUrl += "&expires=" + expires;
+ if (tokenPath) tokenUrl += "&token_path=" + tokenPath;
return tokenUrl;
}
@@ -87,12 +109,12 @@ onFileDownloadRequest((event) => {
const rawQuery = event.requestEvent.request.url.rawQuery;
- // console.log(`record: ${JSON.stringify(event.record)}`)
- // // console.log(`collection: ${JSON.stringify(event.collection)}`)
- // console.log(`app: ${JSON.stringify(event.app)}`)
- // console.log(`fileField: ${JSON.stringify(event.fileField)}`)
- // console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
- // console.log(`servedName: ${JSON.stringify(event.servedName)}`)
+ console.log(`record: ${JSON.stringify(event.record)}`)
+ // console.log(`collection: ${JSON.stringify(event.collection)}`)
+ console.log(`app: ${JSON.stringify(event.app)}`)
+ console.log(`fileField: ${JSON.stringify(event.fileField)}`)
+ console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
+ console.log(`servedName: ${JSON.stringify(event.servedName)}`)
// Our job here is to take the servedPath, and sign it using bunnycdn method
// Then serve a 302 redirect instead of serving the file proxied thru PB
@@ -100,8 +122,8 @@ onFileDownloadRequest((event) => {
const path = event.servedPath;
const expires = Math.round(Date.now() / 1000) + 7 * 24 * 3600; // 7 days
const signedUrl = signUrlCool(securityKey, baseUrl, path, rawQuery, expires);
- // console.log(`rawQUery`, rawQuery, 'path', path);
- // console.log(`signedUrl=${signedUrl}`);
+ console.log(`rawQuery`, rawQuery, 'path', path);
+ console.log(`signedUrl=${signedUrl}`);
// This redirect is a tricky thing. We do this to avoid proxying file requests via our pocketbase origin server.
// The idea is to reduce load.
diff --git a/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs b/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs
index 2b966f64..e1c18d26 100644
--- a/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs
+++ b/services/pocketbase/pb_hooks/pages/(site)/+layout.ejs
@@ -71,8 +71,9 @@