diff --git a/README.org b/README.org new file mode 100644 index 0000000..8b99558 --- /dev/null +++ b/README.org @@ -0,0 +1,266 @@ +#+title: Readme +* Go Server Side Rendering attempt with Pocketbase +** Description +Code in this repo is an attempt to implement a Server Side Rendered (go, templates, htmx) website that would allow signup / authentication via social oauth2 providers. + +Using [[https://pocketbase.io/][pocketbase]] importing it as a framework, to have out-of-the box administration portal with user management, data api, logs. +With everything being compiled into a single binary which is [[https://pocketbase.io/docs/going-to-production/][very easy to deploy]] thanks to pocketbase architecture. + +Most common usage of pocketbase - to develop only front end and utilize api's, either json web api, or JS SDK \ Dark SDK. +But ability to extend the pocketbase by importing it into go project allows to use it also as a pre-built backend for a server that serves SSR html pages. +[[https://htmx.org/essays/][And it can feel really nice, especially if we can also make website feel responsive]]. + +* Building and deploying +** Without nix +1. Have required dependencies on PATH: + - gnumake + - go + - tailwindcss +2. use Makefile: + #+begin_src bash + make build + #+end_src + + It will build tailwindcss style, and then build a binary with all necessary files bundled. +3. Use deployment guide for pocketbase + https://pocketbase.io/docs/going-to-production/ + - either copy the binary to the production server + - or write up a Dockerfile +4. To run locally: + #+begin_src bash + auth-pocketbase-attempt serve + #+end_src + will start service on 127.0.0.1:8090 + can start on any port with argument `--http=127.0.0.1:9999` +** With [[https://nixos.org/][nix]] +*** nix build +Will build default application, the server. +Also packages all static resources directly into binary. +To run: +#+begin_src bash +./result/bin/auth-pocketbase-attempt serve --dir=./pb_data +#+end_src + +Specifying pb_data from the root of the dev project, otherwise will try to create database and other things inside of the /nix/store +*** deploy on [[https://www.tweag.io/blog/2020-05-25-flakes/][NixOS with flakes]]: +Flake contains a NixOS module, for deployment on server with NixOS +Module includes +- systemd job for starting server on machine reboot, automatically restarting on errors, logging. +- option to choose reverse proxy with nginx, or running pocketbase directly bound to port 443 and serving the domain name + Which should make pocketbase handle it's own certificates (see step 3 [[https://pocketbase.io/docs/going-to-production][here]]) + + +Deployment process: +1. taking flake as input + #+begin_src nix + inputs.go-ssr-oauth-attempt.url = "git+http://git.sunshine.industries/efim/go-ssr-pocketbase-oauth-attempt.git"; + #+end_src +2. importing in the server config: + #+begin_src nix + imports = [ + inputs.go-ssr-oauth-attempt.nixosModules.x86_64-linux.auth-pocketbase-attempt + ] + #+end_src +3. setting options + #+begin_src nix + services.auth-pocketbase-attempt = { + enable = true; + host = "go-ssr-oauth-attempt.sunshine.industries"; + port = 45001; + useHostTls = true; + }; + #+end_src +4. enabling TLS with lets encrypt for the server and opening https port: + #+begin_src nix + security.acme.acceptTerms = true; + security.acme.defaults.email = "your@email.net"; + networking.firewall.allowedTCPPorts = [ 80 443 ]; + #+end_src + see https://nixos.org/manual/nixos/stable/#module-security-acme-nginx + ( and also same here https://nixos.wiki/wiki/Nginx ) +5. Apply config to your server. + (i use [[https://github.com/serokell/deploy-rs][deploy-rs]], but simple nixos-rebuild switch via ssh is good) +* Running during development +** With nix +Flake contains dev shell with all required dependencies, +If you have +#+begin_src nix + direnv = { + enable = true; + nix-direnv.enable = true; + }; +#+end_src +on your machine, you can just `direnv allow` to have build dependencies automatically put on PATH when you enter the project directory. + +Otherwise `nix develop` will put you into shell with all dependencies. + +Then running: +#+begin_src bash +make run/live +#+end_src +will build and start the server, +and will trigger rebuild and restart when files change, +only rebuilding tailwindcss when templates or css input changes +** Without nix +You'll need to have all required dependencies: +- gnumake + to run Makefile that composes several build steps into single commands +- go + compiler +- wgo + for server recompilation and restart +- gopls + lsp server +- semgrep + some other lsp server which emacs asked me to install for go +- tailwindcss + to build output.css +- prettier + to format the .gohtml files + +Then running: +#+begin_src bash +make run/live +#+end_src +will build and start the server, +and will trigger rebuild and restart when files change, +only rebuilding tailwindcss when templates or css input changes +* Main parts: +** Authentication middleware: +Registering hooks: +1) after successful auth to set the token into secure cookie: + #+begin_src go + // fires for every auth collection + app.OnRecordAuthRequest().Add(func(e *core.RecordAuthEvent) error { + e.HttpContext.SetCookie(&http.Cookie{ + Name: AuthCookieName, + Value: e.Token, + Path: "/", + Secure: true, + HttpOnly: true, + }) + return nil + }) + #+end_src +2) on call to any pocketbase endpoint to populate request context with parsed auth info: + #+begin_src go +app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.Use(loadAuthContextFromCookie(app)) + return nil +}) + +func loadAuthContextFromCookie(app core.App) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + tokenCookie, err := c.Request().Cookie(AuthCookieName) + if err != nil || tokenCookie.Value == "" { + return next(c) // no token cookie + } + + token := tokenCookie.Value + + claims, _ := security.ParseUnverifiedJWT(token) + tokenType := cast.ToString(claims["type"]) + + switch tokenType { + case tokens.TypeAdmin: + admin, err := app.Dao().FindAdminByToken( + token, + app.Settings().AdminAuthToken.Secret, + ) + if err == nil && admin != nil { + // "authenticate" the admin + c.Set(apis.ContextAdminKey, admin) + } + + case tokens.TypeAuthRecord: + record, err := app.Dao().FindAuthRecordByToken( + token, + app.Settings().RecordAuthToken.Secret, + ) + if err == nil && record != nil { + // "authenticate" the app user + c.Set(apis.ContextAuthRecordKey, record) + } + } + + return next(c) + } + } +} + #+end_src + + as taken from discussion: https://github.com/pocketbase/pocketbase/discussions/989#discussioncomment-4109411 + +Now when pages are served from routes registered in pocketbase - they will be able to access the auth info from context. +And also any other json api request will be automatically authenticated with correct user. +** Front end side of authentication +Since on go side we hook into 'post successful auth' we can use "All in one (recommended)" [[https://pocketbase.io/docs/authentication/][way for oauth2 in the guide]]. + +In the js script we initialize pocketbase SDK, +and for each existing oauth provider rendering a button that calls a method: +#+begin_src js +const authData = await pb.collection('users').authWithOAuth2({ provider: 'google' }); +#+end_src +*** Authentication with passwords can also be coded +With form making POST directly to existing json api (closer to HATEOAS) +or js calls through SDK. +And any successful auth should also call our middleware and set the cookie. + +** Pages package +With templates and static files (out.css and htmx.min.js) bundled into resulting binary with `embed.FS` + +Having +#+begin_src go + app.OnBeforeServe().Add(getIndexPageRoute(app)) +#+end_src +adds route that renders html and returns on some path of our site. +Passing in `app` gives access to things like `app.DAO` for data querying. + +** Changing error responses to html +Registering `OnBeforeApiError` to change json response into html. +This way errors are displayed to end users in a more friendly manner, good idea because usual usage of pocketbase is to have front-end application that would translate error jsons into human readable view. +* Useful parts of documentation: +** Things available in backend through `app` +- [[https://pocketbase.io/docs/go-routing/][Routing]] + registering new routes, reading path / query parameters +- [[https://pocketbase.io/docs/go-event-hooks/][Database]] + querying data +- [[https://pocketbase.io/docs/go-migrations/][Migrations]] + Current project doesn't include migrations, + I didn't understand it all, but it seems that for the project that uses pocketbase as a framework migrations are generated in form of .go files. + If you change tables in admin portal, the changes to them will be encoded as migration path. + Which need to be imported somewhere in 'main' package, included into binary during compilation and automatically applied to production database after updated binary first runs. +** [[https://pocketbase.io/docs/authentication/][Overview of authentication from the front end side]] +** Tips on [[Things required for produ][going to production]] +* Things which are not good right now +** I'd like to figure out a better way to load js scripts +Having them in 'base.gohtml' is ok, but it seems to much, maybe another template or something. +Same with