docs: readme and comments
This commit is contained in:
parent
8842235372
commit
c3d496c36c
|
@ -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 <nav> which has 2 scripts and seem big and unpleasant.
|
||||||
|
Maybe hypersript would achieve same with couple lines of code, maybe there's some other fine art of adding js into htmx projects on go.
|
||||||
|
** Building with both Makefile and nix derivation
|
||||||
|
Having a Makefile is awesome for run/live
|
||||||
|
which only triggers tailwind step if tailwind inputs have changed.
|
||||||
|
|
||||||
|
My previous attempt was to run 'wgo' directly:
|
||||||
|
#+begin_src bash
|
||||||
|
wgo -verbose -file=.go -file=.gohtml -file=tailwind.config.js tailwindcss -i ./pages/input.css -o pages/static/public/out.css :: go run . serve
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
But this triggered tailwind on change of .go files with business logic, which took up time before service is available.
|
||||||
|
|
||||||
|
Unfortunately building go with dependencies is easy in nix (with 'buildGoModule'), but not trivial, because dependencies has to be pre-downloaded and set up as 'fixed output derivation'.
|
||||||
|
|
||||||
|
So I don't know of a way to just reuse Makefile for nix derivation.
|
||||||
|
Thus build is described in two places independently.
|
||||||
|
|
||||||
|
And if any new build step is added, so that Makefile has to change - then i have to not to forget that nix derivation should also be changed.
|
||||||
|
** Error pages
|
||||||
|
Currently 'before error' makes error return a page that redirects to error page
|
||||||
|
and error pages are in 'pages' module
|
||||||
|
|
||||||
|
But putting middleware into module 'pages/errors' and make it directly render error pages will be better.
|
||||||
|
** Currently have all pages in one file
|
||||||
|
and don't have separate rotues to return only <main> part of template for HX reqeusts that do 'hx-boost' switches.
|
||||||
|
** Would be nice to somehow set up JS SDK dependency locally and serve it from static files
|
||||||
|
This would reduce dependency on cdn
|
|
@ -197,7 +197,24 @@ https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/web-ser
|
||||||
|
|
||||||
** TODO add docker image from nix
|
** TODO add docker image from nix
|
||||||
*** CANCELLED add cli for port and host
|
*** CANCELLED add cli for port and host
|
||||||
** TODO add readme and comments
|
** DONE add readme and comments
|
||||||
|
*** DONE pupose of the code
|
||||||
|
*** DONE how to build, install
|
||||||
|
with and without nix
|
||||||
|
*** DONE development things 'make run/live'
|
||||||
|
*** DONE main parts:
|
||||||
|
**** DONE auth middleware
|
||||||
|
**** DONE using js auth
|
||||||
|
**** DONE pages
|
||||||
|
**** DONE error pages
|
||||||
|
*** DONE links to main documentation:
|
||||||
|
- [X] adding new auth providers
|
||||||
|
- [X] adding middlewares and working with collections
|
||||||
|
*** DONE things which aren't good here:
|
||||||
|
- [X] error pages, i guess module in pages, but exposing before error hook themselves
|
||||||
|
- [X] rendering full pages, not doing 'just main' for hx requests
|
||||||
|
- [X] maybe serving js pocketbase from own static files?
|
||||||
|
*** DONE comments on all main modules
|
||||||
** DONE configure tls / ssl / https on franzk deployment
|
** DONE configure tls / ssl / https on franzk deployment
|
||||||
https://nixos.org/manual/nixos/stable/#module-security-acme-nginx
|
https://nixos.org/manual/nixos/stable/#module-security-acme-nginx
|
||||||
( and also same here https://nixos.wiki/wiki/Nginx )
|
( and also same here https://nixos.wiki/wiki/Nginx )
|
||||||
|
|
2
main.go
2
main.go
|
@ -14,6 +14,8 @@ func main() {
|
||||||
middleware.AddErrorsMiddleware(app)
|
middleware.AddErrorsMiddleware(app)
|
||||||
pages.AddPageRoutes(app)
|
pages.AddPageRoutes(app)
|
||||||
|
|
||||||
|
// starts the pocketbase backend
|
||||||
|
// parses cli arguments for hostname and data dir
|
||||||
if err := app.Start(); err != nil {
|
if err := app.Start(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,28 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const AuthCookieName = "Auth"
|
const AuthCookieName = "Auth"
|
||||||
|
// front end side of authentication:
|
||||||
|
// in base.gohtml template, in <nav> bar
|
||||||
|
// js code uses SDK for pocketbase to handle oauth calls to backend.
|
||||||
|
// Also custom event
|
||||||
|
// in oauth js code
|
||||||
|
// document.body.dispatchEvent(new Event("auth-change-event"));
|
||||||
|
// and in logout route
|
||||||
|
// c.Response().Header().Add("HX-Trigger", "auth-change-event")
|
||||||
|
// trigger hx-get on <body>
|
||||||
|
// so that on successful auth and logout the page would refresh
|
||||||
|
// This is suboptimal in that 3 places:
|
||||||
|
// <body> with hx-get, js code with `dispatchEvent` and logout route with
|
||||||
|
// HX-Trigger share responsibility for this piece of logic. For some reason
|
||||||
|
// returning HX-Trigger from auth routes via middleware doesn't trigger event on
|
||||||
|
// htmx side, maybe because these reqeusts are done through js and not directly
|
||||||
|
// by user in browser. Or maybe this would be considered a bug on htmx side and
|
||||||
|
// system could be simplified to just use HX-Trigger response header. Or some
|
||||||
|
// other way to simplify
|
||||||
|
|
||||||
|
|
||||||
|
// registeres on pocketbase middleware that
|
||||||
|
// Sets and Reads session data into a secure cookie
|
||||||
func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
|
func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
e.Router.Use(loadAuthContextFromCookie(app))
|
e.Router.Use(loadAuthContextFromCookie(app))
|
||||||
|
@ -35,6 +56,7 @@ func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
// fires for admin authentication
|
||||||
app.OnAdminAuthRequest().Add(func(e *core.AdminAuthEvent) error {
|
app.OnAdminAuthRequest().Add(func(e *core.AdminAuthEvent) error {
|
||||||
e.HttpContext.SetCookie(&http.Cookie{
|
e.HttpContext.SetCookie(&http.Cookie{
|
||||||
Name: AuthCookieName,
|
Name: AuthCookieName,
|
||||||
|
|
|
@ -13,13 +13,24 @@ import (
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
)
|
)
|
||||||
|
// template files are bundled with binary
|
||||||
|
// for worry free deployment that needs to copy a single file
|
||||||
|
|
||||||
//go:embed templates
|
//go:embed templates
|
||||||
var templatesFS embed.FS
|
var templatesFS embed.FS
|
||||||
|
|
||||||
|
// static files are bundled into separate FS
|
||||||
|
// because full content of that embed.FS is available
|
||||||
|
// under http://127.0.0.1:8090/static/static/public/
|
||||||
|
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var staticFilesFS embed.FS
|
var staticFilesFS embed.FS
|
||||||
|
|
||||||
|
// registers site pages, to be served by pocketbase
|
||||||
|
// passes `app` to allow access to `DAO` and other apis
|
||||||
|
// each page will get auth data in request context
|
||||||
|
// and will be able to create all necessary info for page render:
|
||||||
|
// user data, external api calls, calculations
|
||||||
func AddPageRoutes(app *pocketbase.PocketBase) {
|
func AddPageRoutes(app *pocketbase.PocketBase) {
|
||||||
app.OnBeforeServe().Add(getIndexPageRoute(app))
|
app.OnBeforeServe().Add(getIndexPageRoute(app))
|
||||||
app.OnBeforeServe().Add(getSomePageRoute(app))
|
app.OnBeforeServe().Add(getSomePageRoute(app))
|
||||||
|
@ -155,6 +166,10 @@ func getErrorPageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initializing data which is used by any page that has <nav> bar
|
||||||
|
// - whether user is already authenticated
|
||||||
|
// - which authentication methods are available
|
||||||
|
// this is used in templates/base.gohtml
|
||||||
func initNavInfoData(app *pocketbase.PocketBase, c echo.Context) navInfo {
|
func initNavInfoData(app *pocketbase.PocketBase, c echo.Context) navInfo {
|
||||||
// first collect data
|
// first collect data
|
||||||
info := apis.RequestInfo(c)
|
info := apis.RequestInfo(c)
|
||||||
|
|
Loading…
Reference in New Issue