Compare commits
6 Commits
eb2b170335
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3d496c36c | ||
|
|
8842235372 | ||
|
|
ec47c9d610 | ||
|
|
a367ed9a34 | ||
|
|
2a3d00839f | ||
|
|
e4c79b2155 |
266
README.org
Normal file
266
README.org
Normal file
@@ -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
|
||||
@@ -184,7 +184,7 @@ now works
|
||||
because front-end is setting up js 'new PocketBase' with 127.0.0.1 connection
|
||||
*** adding a custom flag:
|
||||
https://github.com/pocketbase/pocketbase/discussions/1900
|
||||
** TODO change some additional config to option :
|
||||
** DONE change some additional config to option :
|
||||
${optionalString config.proxyWebsockets ''
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@@ -192,14 +192,79 @@ https://github.com/pocketbase/pocketbase/discussions/1900
|
||||
''}
|
||||
( also in planning poker repo )
|
||||
|
||||
https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/web-servers/nginx/default.nix#L428
|
||||
|
||||
|
||||
** TODO add docker image from nix
|
||||
*** CANCELLED add cli for port and host
|
||||
** TODO add readme and comments
|
||||
** TODO configure tls / ssl / https on franzk deployment
|
||||
** 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
|
||||
https://nixos.org/manual/nixos/stable/#module-security-acme-nginx
|
||||
( and also same here https://nixos.wiki/wiki/Nginx )
|
||||
|
||||
can it be configured on render.com?
|
||||
** TODO maybe add middleware so that 401 would be a page, and not json
|
||||
omg
|
||||
line 112 & 113 in project config:
|
||||
http://git.sunshine.industries/efim/go-ssr-pocketbase-oauth-attempt/commit/875de35177462f21732e3ba108a94d77a543da05
|
||||
|
||||
and this in my server config:
|
||||
https://github.com/efim/dotfiles/commit/b3695148082d8c9850a781aaa7a88920bdb1fa7f
|
||||
|
||||
this is all that's needed to enable tls
|
||||
mind blown
|
||||
** DONE somehow set cookie to httpOnly & secure
|
||||
with ability to disable for development session
|
||||
*** a complication
|
||||
since i'm under the nginx, i can't just match on the serving address :
|
||||
#+begin_src
|
||||
[efim@franzk:~]$ systemctl status pb-auth-example-app.service
|
||||
● pb-auth-example-app.service - Exercise app auth-pocketbase-attempt
|
||||
Loaded: loaded (/etc/systemd/system/pb-auth-example-app.service; enabled; preset: enabled)
|
||||
Active: active (running) since Mon 2023-10-09 04:29:20 UTC; 1min 17s ago
|
||||
Main PID: 411857 (auth-pocketbase)
|
||||
Tasks: 13 (limit: 629145)
|
||||
Memory: 28.3M
|
||||
CPU: 148ms
|
||||
CGroup: /system.slice/pb-auth-example-app.service
|
||||
└─411857 /nix/store/czq95bjhwszasncp8f04d9yn4m0xf4kw-auth-pocketbase-attempt-0.0.1/bin/auth-pocketbase-attempt serve --http 127.0.0.1:45001 --dir=/home/pb-auth-example-app-user
|
||||
|
||||
Oct 09 04:29:20 franzk systemd[1]: Started Exercise app auth-pocketbase-attempt.
|
||||
Oct 09 04:29:20 franzk auth-pocketbase-attempt[411857]: 2023/10/09 04:29:20 Warning: starting server with cookie Secure = false!
|
||||
Oct 09 04:29:20 franzk auth-pocketbase-attempt[411857]: 2023/10/09 04:29:20 Server started at http://127.0.0.1:45001
|
||||
Oct 09 04:29:20 franzk auth-pocketbase-attempt[411857]: ├─ REST API: http://127.0.0.1:45001/api/
|
||||
Oct 09 04:29:20 franzk auth-pocketbase-attempt[411857]: └─ Admin UI: http://127.0.0.1:45001/_/
|
||||
#+end_src
|
||||
*** so, custom arg is required, hello
|
||||
https://github.com/pocketbase/pocketbase/discussions/1900
|
||||
*** holy cow, Firefox and later Chrome will accept Secure cookie on localhost
|
||||
https://stackoverflow.com/questions/62307431/firefox-sends-secure-cookies-to-localhost
|
||||
|
||||
see: except on localhost : https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
|
||||
|
||||
** DONE prettying up server responses for "we show html" land
|
||||
let's do this also, yes
|
||||
*** DONE logout should push root url in htmx
|
||||
*** DONE lets make 404 page and return it
|
||||
*** DONE lets make 401 page and return it
|
||||
*** DONE and let's make NavInfo init common for reuse
|
||||
** TODO get icons for the auth providers. surely they are accessible from the pocketbase itself?
|
||||
http://localhost:8090/_/images/oauth2/apple.svg
|
||||
yes.
|
||||
@@ -210,3 +275,6 @@ https://pocketbase.io/docs/go-migrations/#enable-go-migrations
|
||||
if i understood correctly, when i enable migration generation
|
||||
i would be able to modify locally run instance via admin interface,
|
||||
go files with migration would be generated, i'll have to import them somewhere in my main module, and then after building/packaging when i run `serve` on production the migrations would run on the production data
|
||||
** adding google oauth
|
||||
support article : https://developers.google.com/identity/sign-in/web/sign-in
|
||||
settings are in : https://console.cloud.google.com/apis/credentials
|
||||
|
||||
@@ -114,10 +114,9 @@
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
# taken from https://pocketbase.io/docs/going-to-production/
|
||||
proxyWebsockets = true;
|
||||
extraConfig = ''
|
||||
# check http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
|
||||
proxy_set_header Connection ''';
|
||||
proxy_http_version 1.1;
|
||||
proxy_read_timeout 360s;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
|
||||
15
main.go
15
main.go
@@ -1,19 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"log"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"sunshine.industries/auth-pocketbase-attempt/middleware"
|
||||
"sunshine.industries/auth-pocketbase-attempt/pages"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := pocketbase.New()
|
||||
|
||||
middleware.AddCookieSessionMiddleware(app)
|
||||
middleware.AddErrorsMiddleware(app)
|
||||
pages.AddPageRoutes(app)
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// starts the pocketbase backend
|
||||
// parses cli arguments for hostname and data dir
|
||||
if err := app.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
@@ -12,7 +13,28 @@ import (
|
||||
)
|
||||
|
||||
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) {
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
e.Router.Use(loadAuthContextFromCookie(app))
|
||||
@@ -25,6 +47,8 @@ func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
|
||||
Name: AuthCookieName,
|
||||
Value: e.Token,
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
e.HttpContext.SetCookie(&http.Cookie{
|
||||
Name: "username",
|
||||
@@ -32,11 +56,14 @@ func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
|
||||
})
|
||||
return nil
|
||||
})
|
||||
// fires for admin authentication
|
||||
app.OnAdminAuthRequest().Add(func(e *core.AdminAuthEvent) error {
|
||||
e.HttpContext.SetCookie(&http.Cookie{
|
||||
Name: AuthCookieName,
|
||||
Value: e.Token,
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
@@ -92,9 +119,11 @@ func getLogoutRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error {
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
c.Response().Header().Add("HX-Trigger", "auth-change-event")
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": "session cookie removed"})
|
||||
return c.NoContent(http.StatusOK)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
54
middleware/httpErrors.go
Normal file
54
middleware/httpErrors.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func AddErrorsMiddleware(app *pocketbase.PocketBase) {
|
||||
app.OnBeforeApiError().Add(func(e *core.ApiErrorEvent) error {
|
||||
log.Printf("in before api error with %+v with response %v and error %+v", e, e.HttpContext.Response(), e.Error)
|
||||
// oh, i guess i could do redirect?
|
||||
return renderErrorPage(e)
|
||||
})
|
||||
}
|
||||
|
||||
var redirectTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url='/error/{{ . }}'" />
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to error page</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
var tmpl = template.Must( template.New("redirect-to-error").Parse(redirectTemplate) )
|
||||
|
||||
func renderErrorPage(e *core.ApiErrorEvent) error {
|
||||
errorMessage := e.Error.Error()
|
||||
log.Printf("in error to html middleware for %s with status %+v", errorMessage, e)
|
||||
|
||||
errorCode := 500
|
||||
switch errorMessage {
|
||||
case "Not Found.":
|
||||
// not authorized
|
||||
errorCode = 404
|
||||
case "The request requires admin or record authorization token to be set.":
|
||||
// not found
|
||||
errorCode = 401
|
||||
}
|
||||
|
||||
var instantiatedTemplate bytes.Buffer
|
||||
if err := tmpl.Execute(&instantiatedTemplate, errorCode); err != nil {
|
||||
// couldn't execute the template
|
||||
return e.HttpContext.HTML(200, "Error 500")
|
||||
}
|
||||
|
||||
return e.HttpContext.HTML(200, instantiatedTemplate.String())
|
||||
}
|
||||
@@ -6,22 +6,35 @@ import (
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"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
|
||||
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
|
||||
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) {
|
||||
app.OnBeforeServe().Add(getIndexPageRoute(app))
|
||||
app.OnBeforeServe().Add(somePageRoute)
|
||||
app.OnBeforeServe().Add(getSomePageRoute(app))
|
||||
app.OnBeforeServe().Add(getErrorPageRoute(app))
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
e.Router.StaticFS("/static", staticFilesFS)
|
||||
@@ -41,40 +54,13 @@ func getIndexPageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error
|
||||
return func(e *core.ServeEvent) error {
|
||||
e.Router.GET("/", func(c echo.Context) error {
|
||||
// first collect data
|
||||
info := apis.RequestInfo(c)
|
||||
admin := info.Admin // nil if not authenticated as admin
|
||||
record := info.AuthRecord // nil if not authenticated as regular auth record
|
||||
|
||||
isGuest := admin == nil && record == nil
|
||||
|
||||
username := ""
|
||||
switch {
|
||||
case admin != nil:
|
||||
username = admin.Email
|
||||
case record != nil:
|
||||
username = record.Username()
|
||||
}
|
||||
|
||||
oauthProviders := app.Settings().NamedAuthProviderConfigs()
|
||||
oauthProviderNames := make([]string, 0, len(oauthProviders))
|
||||
for name, config := range oauthProviders {
|
||||
if config.Enabled {
|
||||
oauthProviderNames = append(oauthProviderNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
navInfoData := initNavInfoData(app, c)
|
||||
indexPageData := struct {
|
||||
IsGuest, IsAdmin bool
|
||||
Username string
|
||||
EnabledOauthProviders []string
|
||||
NavInfo navInfo
|
||||
BackendMessage string
|
||||
NavInfo navInfo
|
||||
}{
|
||||
IsAdmin: admin != nil,
|
||||
NavInfo: navInfo{
|
||||
IsGuest: isGuest,
|
||||
Username: username,
|
||||
EnabledOauthProviders: oauthProviderNames,
|
||||
},
|
||||
BackendMessage: "Hello from the backend!",
|
||||
NavInfo: navInfoData,
|
||||
}
|
||||
|
||||
// then render template with it
|
||||
@@ -101,44 +87,116 @@ func stringWithCharset(length int, charset string) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
func somePageRoute(e *core.ServeEvent) error {
|
||||
e.Router.GET("/somepage", func(c echo.Context) error {
|
||||
// get data
|
||||
// and since i'm using 'base.gohtml' with Nav, i'll need Nav info
|
||||
func getSomePageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error {
|
||||
return func(e *core.ServeEvent) error {
|
||||
e.Router.GET("/somepage", func(c echo.Context) error {
|
||||
// get data
|
||||
// and since i'm using 'base.gohtml' with Nav, i'll need Nav info
|
||||
navInfoData := initNavInfoData(app, c)
|
||||
|
||||
info := apis.RequestInfo(c)
|
||||
admin := info.Admin // nil if not authenticated as admin
|
||||
record := info.AuthRecord // nil if not authenticated as regular auth record
|
||||
somePageData := struct {
|
||||
RandomNumber int
|
||||
RandomString string
|
||||
NavInfo navInfo
|
||||
}{
|
||||
RandomNumber: rand.Int(),
|
||||
RandomString: stringWithCharset(25, charset),
|
||||
NavInfo: navInfoData,
|
||||
}
|
||||
|
||||
username := ""
|
||||
switch {
|
||||
case admin != nil:
|
||||
username = admin.Email
|
||||
case record != nil:
|
||||
username = record.Username()
|
||||
}
|
||||
// then render template with it
|
||||
templateName := "templates/somepage.gohtml"
|
||||
tmpl := template.Must(template.ParseFS(templatesFS, "templates/base.gohtml", templateName))
|
||||
var instantiatedTemplate bytes.Buffer
|
||||
if err := tmpl.Execute(&instantiatedTemplate, somePageData); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"message": "error parsing template"})
|
||||
}
|
||||
|
||||
somePageData := struct {
|
||||
RandomNumber int
|
||||
RandomString string
|
||||
NavInfo navInfo
|
||||
}{
|
||||
RandomNumber: rand.Int(),
|
||||
RandomString: stringWithCharset(25, charset),
|
||||
NavInfo: navInfo{
|
||||
Username: username,
|
||||
},
|
||||
}
|
||||
|
||||
// then render template with it
|
||||
templateName := "templates/somepage.gohtml"
|
||||
tmpl := template.Must(template.ParseFS(templatesFS, "templates/base.gohtml", templateName))
|
||||
var instantiatedTemplate bytes.Buffer
|
||||
if err := tmpl.Execute(&instantiatedTemplate, somePageData); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"message": "error parsing template"})
|
||||
}
|
||||
|
||||
return c.HTML(http.StatusOK, instantiatedTemplate.String())
|
||||
}, apis.RequireAdminOrRecordAuth())
|
||||
return nil
|
||||
return c.HTML(http.StatusOK, instantiatedTemplate.String())
|
||||
}, apis.RequireAdminOrRecordAuth())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getErrorPageRoute(app *pocketbase.PocketBase) func(*core.ServeEvent) error {
|
||||
return func(e *core.ServeEvent) error {
|
||||
e.Router.GET("/error/:code", func(c echo.Context) error {
|
||||
// get data
|
||||
code := c.PathParam("code")
|
||||
codeNum, err := strconv.ParseInt(code, 10, 64)
|
||||
if err != nil {
|
||||
codeNum = 500
|
||||
}
|
||||
errorText := http.StatusText(int(codeNum))
|
||||
if errorText == "" {
|
||||
codeNum = 500
|
||||
errorText = http.StatusText(500)
|
||||
}
|
||||
|
||||
// and since i'm using 'base.gohtml' with Nav, i'll need Nav info
|
||||
navInfoData := initNavInfoData(app, c)
|
||||
|
||||
somePageData := struct {
|
||||
NavInfo navInfo
|
||||
ErrorCode int64
|
||||
ErrorText string
|
||||
}{
|
||||
NavInfo: navInfoData,
|
||||
ErrorCode: codeNum,
|
||||
ErrorText: errorText,
|
||||
}
|
||||
|
||||
// then render template with it
|
||||
templateName := "templates/errors/error.gohtml"
|
||||
switch codeNum {
|
||||
case 404:
|
||||
templateName = "templates/errors/404.gohtml"
|
||||
case 401:
|
||||
templateName = "templates/errors/401.gohtml"
|
||||
}
|
||||
tmpl := template.Must(template.ParseFS(templatesFS, "templates/base.gohtml", templateName))
|
||||
var instantiatedTemplate bytes.Buffer
|
||||
if err := tmpl.Execute(&instantiatedTemplate, somePageData); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"message": "error parsing template"})
|
||||
}
|
||||
|
||||
return c.HTML(int(codeNum), instantiatedTemplate.String())
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// first collect data
|
||||
info := apis.RequestInfo(c)
|
||||
admin := info.Admin // nil if not authenticated as admin
|
||||
record := info.AuthRecord // nil if not authenticated as regular auth record
|
||||
|
||||
isGuest := admin == nil && record == nil
|
||||
|
||||
username := ""
|
||||
switch {
|
||||
case admin != nil:
|
||||
username = admin.Email
|
||||
case record != nil:
|
||||
username = record.Username()
|
||||
}
|
||||
|
||||
oauthProviders := app.Settings().NamedAuthProviderConfigs()
|
||||
oauthProviderNames := make([]string, 0, len(oauthProviders))
|
||||
for name, config := range oauthProviders {
|
||||
if config.Enabled {
|
||||
oauthProviderNames = append(oauthProviderNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
return navInfo{
|
||||
IsGuest: isGuest,
|
||||
Username: username,
|
||||
EnabledOauthProviders: oauthProviderNames,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
></script>
|
||||
<script defer src="/static/static/public/htmx.min.js"></script>
|
||||
</head>
|
||||
<body hx-get="/" hx-trigger="auth-change-event">
|
||||
<body hx-get="/" hx-trigger="auth-change-event" hx-push-url="true">
|
||||
<!--[if lt IE 8]>
|
||||
<p class="browserupgrade">
|
||||
You are using an <strong>outdated</strong> browser. Please
|
||||
|
||||
10
pages/templates/errors/401.gohtml
Normal file
10
pages/templates/errors/401.gohtml
Normal file
@@ -0,0 +1,10 @@
|
||||
{{ define "title" }}
|
||||
Not Authorized
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main hx-boost="true" class="px-10 pt-10 flex flex-col gap-y-10">
|
||||
The page you are trying to access requires authorization.
|
||||
Please log in.
|
||||
</main>
|
||||
{{ end }}
|
||||
9
pages/templates/errors/404.gohtml
Normal file
9
pages/templates/errors/404.gohtml
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ define "title" }}
|
||||
Page Not Found
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main hx-boost="true" class="px-10 pt-10 flex flex-col gap-y-10">
|
||||
Error 404 means the page was not found
|
||||
</main>
|
||||
{{ end }}
|
||||
10
pages/templates/errors/error.gohtml
Normal file
10
pages/templates/errors/error.gohtml
Normal file
@@ -0,0 +1,10 @@
|
||||
{{ define "title" }}
|
||||
Error occurred
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main hx-boost="true" class="px-10 pt-10 flex flex-col gap-y-10">
|
||||
<p> Error {{ .ErrorCode }} occurred! </p>
|
||||
<p> {{ .ErrorText }} </p>
|
||||
</main>
|
||||
{{ end }}
|
||||
@@ -13,6 +13,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
{{ else }}
|
||||
<p>Rendering this on the backend, passing values from the code: {{ .BackendMessage }}</p>
|
||||
<p>There will be some content only for authorized users</p>
|
||||
{{ end }}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user