From d06c158f3a817b9c4bfb9e4dcf8c3ca751aa5835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Sun, 13 Oct 2024 12:40:40 +0200 Subject: [PATCH] feat(server): generate the REST using the OpenAPI spec With this change, the REST server is now generated based on the OpenAPI spec. This ties up both the definition and the implementation, preventing further deviations and invalid specifications (if it's invalid, it will fail to generate or won't be able to be used inside the server). With this, we can ensure that the OpenAPI consumers will always have the correct definition --- .gitignore | 2 + Dockerfile | 3 +- Makefile | 8 +- go.mod | 16 +- go.sum | 47 +++++- nix/common.nix | 16 +- nix/devShell.nix | 6 +- nix/frontend.nix | 3 +- nix/server.nix | 10 +- server/rest/.ogen.yaml | 5 + server/rest/container.go | 43 ++--- server/rest/handlers.go | 342 +++++++++++++++------------------------ server/rest/security.go | 16 ++ server/server.go | 2 +- 14 files changed, 267 insertions(+), 252 deletions(-) create mode 100644 server/rest/.ogen.yaml create mode 100644 server/rest/security.go diff --git a/.gitignore b/.gitignore index 912c21de..e6f20798 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ frontend/.pnp.loader.mjs frontend/.yarn/install-state.gz .db.lock livestreams.dat +# Generated by `make gen` +server/rest/ogen diff --git a/Dockerfile b/Dockerfile index 4c2d9093..56307937 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,8 @@ WORKDIR /usr/src/yt-dlp-webui COPY . . COPY --from=ui /usr/src/yt-dlp-webui/frontend /usr/src/yt-dlp-webui/frontend -RUN CGO_ENABLED=0 GOOS=linux go build -o yt-dlp-webui +RUN make gen && \ + CGO_ENABLED=0 GOOS=linux go build -o yt-dlp-webui # ----------------------------------------------------------------------------- # dependencies ---------------------------------------------------------------- diff --git a/Makefile b/Makefile index 02cbcd05..3f62d664 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,13 @@ fe: dev: cd frontend && pnpm dev -all: +gen: +ifeq (, $(shell which ogen)) + go install github.com/ogen-go/ogen/cmd/ogen@v1.4.1 +endif + go generate ./... + +all: gen $(MAKE) fe && cd .. CGO_ENABLED=0 go build -o yt-dlp-webui main.go diff --git a/go.mod b/go.mod index d7a65bfc..5e45657e 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,13 @@ require ( github.com/coreos/go-oidc/v3 v3.11.0 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 + github.com/go-faster/errors v0.7.1 + github.com/go-faster/jx v1.1.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/ogen-go/ogen v1.4.1 + go.uber.org/multierr v1.11.0 golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 golang.org/x/sys v0.25.0 @@ -18,16 +22,24 @@ require ( ) require ( + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-faster/yaml v0.4.6 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/segmentio/asm v1.2.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/text v0.18.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect modernc.org/libc v1.61.0 // indirect modernc.org/mathutil v1.6.0 // indirect diff --git a/go.sum b/go.sum index 7f0f38cd..c4f32b48 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,24 @@ github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/ github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= +github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg= +github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= +github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -24,39 +36,60 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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.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= 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 v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ogen-go/ogen v1.4.1 h1:uVZWKw80eZaMQu7LpTJxpHqwu6oMqYuh8ElqP1Mf2bI= +github.com/ogen-go/ogen v1.4.1/go.mod h1:YeliH7gAS6QToqDqIM5BrnEUOiXiqCnNqNwzEpebDsY= 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/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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 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= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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= +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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= diff --git a/nix/common.nix b/nix/common.nix index f0a67ae5..f5e72fe9 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -1,9 +1,21 @@ -{ lib }: { +{ lib, pkgs }: { version = "v3.1.2"; meta = { description = "A terrible web ui for yt-dlp. Designed to be self-hosted."; homepage = "https://github.com/marcopeocchi/yt-dlp-web-ui"; license = lib.licenses.mpl20; }; -} + ogen = pkgs.buildGo123Module { + pname = "ogen"; + version = "v1.4.1"; + + src = pkgs.fetchFromGitHub { + owner = "ogen-go"; + repo = "ogen"; + rev = "v1.4.1"; + sha256 = "sha256-SwJY9VQafclAxEQ/cbRJALvMLlnSIItIOz92XzuCoCk="; + }; + vendorHash = "sha256-IxG7y0Zy0DerCh5DRdSWSaD643BG/8Wj2wuYvkn+XzE="; + }; +} diff --git a/nix/devShell.nix b/nix/devShell.nix index 8bb9d96c..0ec1f9fb 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -1,9 +1,13 @@ -{ inputsFrom ? [ ], mkShell, yt-dlp, nodejs, go }: +{ inputsFrom ? [ ], mkShell, yt-dlp, nodejs, go, lib, pkgs }: +let + common = import ./common.nix { inherit lib; inherit pkgs; }; +in mkShell { inherit inputsFrom; packages = [ yt-dlp nodejs go + common.ogen ]; } diff --git a/nix/frontend.nix b/nix/frontend.nix index 17fc4371..7b52bb1c 100644 --- a/nix/frontend.nix +++ b/nix/frontend.nix @@ -1,9 +1,10 @@ { lib +, pkgs , stdenv , nodejs , pnpm }: -let common = import ./common.nix { inherit lib; }; in +let common = import ./common.nix { inherit lib; inherit pkgs; }; in stdenv.mkDerivation (finalAttrs: { pname = "yt-dlp-web-ui-frontend"; diff --git a/nix/server.nix b/nix/server.nix index b20a3cdd..d23d4b64 100644 --- a/nix/server.nix +++ b/nix/server.nix @@ -1,7 +1,7 @@ -{ yt-dlp-web-ui-frontend, buildGo123Module, lib, makeWrapper, yt-dlp, ... }: +{ pkgs, yt-dlp-web-ui-frontend, buildGo123Module, lib, makeWrapper, yt-dlp, ... }: let fs = lib.fileset; - common = import ./common.nix { inherit lib; }; + common = import ./common.nix { inherit lib; inherit pkgs; }; in buildGo123Module { pname = "yt-dlp-web-ui"; @@ -35,16 +35,18 @@ buildGo123Module { # https://github.com/golang/go/issues/44507 preBuild = '' cp -r ${yt-dlp-web-ui-frontend} frontend + + go generate ./... ''; - nativeBuildInputs = [ makeWrapper ]; + nativeBuildInputs = [ makeWrapper common.ogen ]; postInstall = '' wrapProgram $out/bin/yt-dlp-web-ui \ --prefix PATH : ${lib.makeBinPath [ yt-dlp ]} ''; - vendorHash = "sha256-c7IdCmYJEn5qJn3K8wt0qz3t0Nq9rbgWp1eONlCJOwM="; + vendorHash = "sha256-9QZRj3HBNJI0Iv/6QSFb8lbcsPyLXV3rPJ3Ige8Ww9o="; meta = common.meta // { mainProgram = "yt-dlp-web-ui"; diff --git a/server/rest/.ogen.yaml b/server/rest/.ogen.yaml new file mode 100644 index 00000000..1338b0bf --- /dev/null +++ b/server/rest/.ogen.yaml @@ -0,0 +1,5 @@ +generator: + features: + disable: + - 'paths/client' + - 'ogen/otel' diff --git a/server/rest/container.go b/server/rest/container.go index 1ced07f1..7f3b8019 100644 --- a/server/rest/container.go +++ b/server/rest/container.go @@ -1,10 +1,14 @@ package rest import ( - "github.com/go-chi/chi/v5" + "log/slog" + "net/http" + "os" + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware" "github.com/marcopeocchi/yt-dlp-web-ui/server/openid" + "github.com/marcopeocchi/yt-dlp-web-ui/server/rest/ogen" ) func Container(args *ContainerArgs) *Handler { @@ -15,26 +19,25 @@ func Container(args *ContainerArgs) *Handler { return handler } -func ApplyRouter(args *ContainerArgs) func(chi.Router) { +func ApplyRouter(args *ContainerArgs) http.Handler { h := Container(args) - return func(r chi.Router) { - if config.Instance().RequireAuth { - r.Use(middlewares.Authenticated) - } - if config.Instance().UseOpenId { - r.Use(openid.Middleware) - } - r.Post("/exec", h.Exec()) - r.Post("/execPlaylist", h.ExecPlaylist()) - r.Post("/execLivestream", h.ExecLivestream()) - r.Get("/running", h.Running()) - r.Get("/version", h.GetVersion()) - r.Get("/cookies", h.GetCookies()) - r.Post("/cookies", h.SetCookies()) - r.Delete("/cookies", h.DeleteCookies()) - r.Post("/template", h.AddTemplate()) - r.Get("/template/all", h.GetTemplates()) - r.Delete("/template/{id}", h.DeleteTemplate()) + srv, err := ogen.NewServer(h, &secHandler{}, ogen.WithPathPrefix("/api/v1")) + if err != nil { + slog.Error("create the REST server", + slog.String("err", err.Error())) + + os.Exit(1) + } + + var hand http.Handler = srv + if config.Instance().RequireAuth { + hand = middlewares.Authenticated(hand) } + + if config.Instance().UseOpenId { + hand = openid.Middleware(hand) + } + + return hand } diff --git a/server/rest/handlers.go b/server/rest/handlers.go index 95b14a44..86c27879 100644 --- a/server/rest/handlers.go +++ b/server/rest/handlers.go @@ -1,13 +1,15 @@ +//go:generate ogen --target ./ogen -package ogen --clean ../../openapi/openapi.json package rest import ( - "encoding/json" - "net/http" + "context" - "github.com/go-chi/chi/v5" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal" + "github.com/marcopeocchi/yt-dlp-web-ui/server/rest/ogen" ) +var _ ogen.Handler = &Handler{} + type Handler struct { service *Service } @@ -16,256 +18,172 @@ type Handler struct { REST version of the JSON-RPC interface */ -func (h *Handler) Exec() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - w.Header().Set("Content-Type", "application/json") - - var req internal.DownloadRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - id, err := h.service.Exec(req) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = json.NewEncoder(w).Encode(id) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } +func transformInternalDownloadRequest(req *ogen.DownloadRequest) internal.DownloadRequest { + var iReq internal.DownloadRequest + if req.URL.Set { + iReq.URL = req.URL.Value.String() } -} -func (h *Handler) ExecPlaylist() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - w.Header().Set("Content-Type", "application/json") - - var req internal.DownloadRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - err := h.service.ExecPlaylist(req) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if err := json.NewEncoder(w).Encode("ok"); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + if req.Path.Set { + iReq.Path = req.Path.Value } -} -func (h *Handler) ExecLivestream() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - w.Header().Set("Content-Type", "application/json") - - var req internal.DownloadRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - h.service.ExecLivestream(req) - - err := json.NewEncoder(w).Encode("ok") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + if req.Rename.Set { + iReq.Rename = req.Rename.Value } -} - -func (h *Handler) Running() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - w.Header().Set("Content-Type", "application/json") + iReq.Params = req.Params - res, err := h.service.Running(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = json.NewEncoder(w).Encode(res) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } + return iReq } -func (h *Handler) GetCookies() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - cookies, err := h.service.GetCookies(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *Handler) AddDownload(ctx context.Context, req *ogen.DownloadRequest) (ogen.AddDownloadRes, error) { + iReq := transformInternalDownloadRequest(req) - res := &internal.SetCookiesRequest{ - Cookies: string(cookies), - } - - if err := json.NewEncoder(w).Encode(res); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + id, err := h.service.Exec(iReq) + if err != nil { + return nil, err } -} - -func (h *Handler) SetCookies() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - w.Header().Set("Content-Type", "application/json") - - req := new(internal.SetCookiesRequest) - - err := json.NewDecoder(r.Body).Decode(req) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - err = h.service.SetCookies(r.Context(), req.Cookies) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = json.NewEncoder(w).Encode("ok") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } + res := ogen.AddDownloadOKApplicationJSON(id) + return &res, nil } -func (h *Handler) DeleteCookies() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - err := h.service.SetCookies(r.Context(), "") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *Handler) AddDownloadPlaylist(ctx context.Context, req *ogen.DownloadRequest) (ogen.AddDownloadPlaylistRes, error) { + iReq := transformInternalDownloadRequest(req) - err = json.NewEncoder(w).Encode("ok") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + if err := h.service.ExecPlaylist(iReq); err != nil { + return nil, err } + + ok := ogen.AddDownloadPlaylistOKOk + return &ok, nil } -func (h *Handler) AddTemplate() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() +func (h *Handler) AddDwonloadLivestream(ctx context.Context, req *ogen.DownloadRequest) (ogen.AddDwonloadLivestreamRes, error) { + iReq := transformInternalDownloadRequest(req) - w.Header().Set("Content-Type", "application/json") + h.service.ExecLivestream(iReq) - req := new(internal.CustomTemplate) + ok := ogen.AddDwonloadLivestreamOKOk + return &ok, nil +} - err := json.NewDecoder(r.Body).Decode(req) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } +func (h *Handler) Running(ctx context.Context) (ogen.RunningRes, error) { + iRes, err := h.service.Running(ctx) + if err != nil { + return nil, err + } - if req.Name == "" || req.Content == "" { - http.Error(w, "Invalid template", http.StatusBadRequest) - return - } + res := ogen.RunningOKApplicationJSON{} + for _, r := range *iRes { + res = append(res, ogen.ProcessResponse{ + Progress: ogen.DownloadProgress{ + ProcessStatus: r.Progress.Status, + Percentage: r.Progress.Percentage, + Speed: r.Progress.Speed, + Eta: r.Progress.ETA, + }, + Info: ogen.DownloadInfo{ + URL: r.Info.URL, + Title: r.Info.Title, + Thumbnail: r.Info.Thumbnail, + Resolution: r.Info.Resolution, + Size: int(r.Info.Size), + Vcodec: r.Info.VCodec, + Acodec: r.Info.ACodec, + Extension: r.Info.Extension, + OriginalURL: r.Info.OriginalURL, + CreatedAt: r.Info.CreatedAt, + }, + Output: ogen.DownloadOutput{ + Path: r.Output.Path, + Filename: r.Output.Filename, + SavedFilePath: r.Output.SavedFilePath, + }, + }) + } - err = h.service.SaveTemplate(r.Context(), req) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + return &res, nil +} - err = json.NewEncoder(w).Encode("ok") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } +func (h *Handler) GetVersion(ctx context.Context) (*ogen.GetVersionResponse, error) { + rpc, ytdlp, err := h.service.GetVersion(ctx) + if err != nil { + return nil, err } -} -func (h *Handler) GetTemplates() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() + return &ogen.GetVersionResponse{ + RpcVersion: rpc, + YtdlpVersion: ytdlp, + }, nil +} - w.Header().Set("Content-Type", "application/json") +func (h *Handler) GetCookies(ctx context.Context) (*ogen.SetCookiesRequest, error) { + cookies, err := h.service.GetCookies(ctx) + if err != nil { + return nil, err + } - templates, err := h.service.GetTemplates(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + return &ogen.SetCookiesRequest{ + Cookies: string(cookies), + }, nil +} - err = json.NewEncoder(w).Encode(templates) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } +func (h *Handler) SetCookies(ctx context.Context, req *ogen.SetCookiesRequest) (ogen.SetCookiesRes, error) { + if err := h.service.SetCookies(ctx, req.Cookies); err != nil { + return nil, err } -} -func (h *Handler) DeleteTemplate() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() + ok := ogen.SetCookiesOKOk + return &ok, nil +} - w.Header().Set("Content-Type", "application/json") +func (h *Handler) DeleteCookies(ctx context.Context) (ogen.DeleteCookiesOK, error) { + if err := h.service.SetCookies(ctx, ""); err != nil { + return "", err + } - id := chi.URLParam(r, "id") + return ogen.DeleteCookiesOKOk, nil +} - err := h.service.DeleteTemplate(r.Context(), id) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } +func (h *Handler) AddTemplate(ctx context.Context, req *ogen.CustomTemplate) (ogen.AddTemplateRes, error) { + var iReq internal.CustomTemplate + iReq.Id = req.ID + iReq.Name = req.Name + iReq.Content = req.Content - err = json.NewEncoder(w).Encode("ok") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + if err := h.service.SaveTemplate(ctx, &iReq); err != nil { + return nil, err } + + ok := ogen.AddTemplateOKOk + return &ok, nil } -func (h *Handler) GetVersion() http.HandlerFunc { - type Response struct { - RPCVersion string `json:"rpcVersion"` - YtdlpVersion string `json:"ytdlpVersion"` +func (h *Handler) TemplateAllGet(ctx context.Context) ([]ogen.CustomTemplate, error) { + templates, err := h.service.GetTemplates(ctx) + if err != nil { + return nil, err } - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - w.Header().Set("Content-Type", "application/json") - - rpcVersion, ytdlpVersion, err := h.service.GetVersion(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + res := []ogen.CustomTemplate{} + for _, t := range *templates { + res = append(res, ogen.CustomTemplate{ + ID: t.Id, + Name: t.Name, + Content: t.Content, + }) + } - res := Response{ - RPCVersion: rpcVersion, - YtdlpVersion: ytdlpVersion, - } + return res, nil +} - if err := json.NewEncoder(w).Encode(res); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } +func (h *Handler) TemplateIDDelete(ctx context.Context, params ogen.TemplateIDDeleteParams) (ogen.TemplateIDDeleteRes, error) { + if err := h.service.DeleteTemplate(ctx, params.ID); err != nil { + return nil, err } + + ok := ogen.TemplateIDDeleteOKOk + return &ok, nil } diff --git a/server/rest/security.go b/server/rest/security.go new file mode 100644 index 00000000..5b660015 --- /dev/null +++ b/server/rest/security.go @@ -0,0 +1,16 @@ +package rest + +import ( + "context" + + "github.com/marcopeocchi/yt-dlp-web-ui/server/rest/ogen" +) + +var _ ogen.SecurityHandler = &secHandler{} + +type secHandler struct{} + +func (s secHandler) HandleAPIKey(ctx context.Context, operationName string, t ogen.APIKey) (context.Context, error) { + // We ignore this, since is handled by the chi middleware + return ctx, nil +} diff --git a/server/server.go b/server/server.go index f757d135..50cae6b1 100644 --- a/server/server.go +++ b/server/server.go @@ -205,7 +205,7 @@ func newServer(c serverConfig) *http.Server { r.Route("/rpc", ytdlpRPC.ApplyRouter()) // REST API handlers - r.Route("/api/v1", rest.ApplyRouter(&rest.ContainerArgs{ + r.Mount("/api/v1", rest.ApplyRouter(&rest.ContainerArgs{ DB: c.db, MDB: c.mdb, MQ: c.mq,