Compare commits

...

10 Commits
master ... main

Author SHA1 Message Date
efim c3d496c36c docs: readme and comments 2023-10-09 15:19:42 +00:00
efim 8842235372 feat: middleware to return error as html 2023-10-09 07:45:29 +00:00
efim ec47c9d610 fix: logout should set address to / and open / 2023-10-09 05:42:50 +00:00
efim a367ed9a34 fix: setting Secure behind nginx
bug was due to having explicit 'serve --http=address' when running
behind nginx on NixOS server
So either a more complicated check was required, or just setting
Secure=true unconditionally.

This seems to be a better way, because Firefox already allows secure
cookies beng sent and received from localhost for dev purposes, and
Chromium does too
2023-10-09 05:08:59 +00:00
efim 2a3d00839f feat: securing the cookies 2023-10-09 04:22:09 +00:00
efim e4c79b2155 refactor: utilizing cool options for nginx 2023-10-09 03:17:47 +00:00
efim eb2b170335 feat: attempted allowance for nixos ssl 2023-10-08 20:56:19 +00:00
efim bfee145b6c fix: removing printlns
todo - figure out good logging
2023-10-08 18:42:09 +00:00
efim c032987952 fix: removing hardcode of js init 2023-10-08 18:23:04 +00:00
efim ea8d1fab75 feat: adding a license for pushing to repo 2023-10-08 17:00:25 +00:00
13 changed files with 663 additions and 126 deletions

8
LICENSE Normal file
View File

@ -0,0 +1,8 @@
The MIT License (MIT) Copyright (c) 2023 - present, Efim Nefedov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

266
README.org Normal file
View 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

View File

@ -148,7 +148,7 @@ and
#+end_src
is what i need for it to pick up pb_data from work directory, cool
** TODO write nixos module
** DONE write nixos module
need to pass data and migration location as params
and address on which to serve, cool
i suppose
@ -174,19 +174,107 @@ cool
oh, but if i'm using nginx i'll need my own certificate, that makes sence
*** maybe things are ok?
let's try to plaintext deploy?
*** quoting of the '' in multiline string
https://nixos.org/manual/nix/stable/language/values.html
*** not accessible still
sudo journalctl -u nginx --since "1 day ago"
*** oh, i forgot to add subname in gandi ui
now works
*** now i need a way to pass in the hostname
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
** DONE change some additional config to option :
${optionalString config.proxyWebsockets ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
''}
( 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
*** TODO add cli for port and host
** TODO add readme and comments
** TODO configure tls / ssl / https on franzk deployment
*** CANCELLED add cli for port and host
** 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.
** TODO read and add ok logging
** TODO figure out and enbale migrations
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

View File

@ -42,8 +42,8 @@
nixosModules.auth-pocketbase-attempt = { config, pkgs, ... }:
let
cfg = config.services.${pname};
lib = pkgs.lib;
shortName = "pb-auth-example-group";
lib = nixpkgs.lib;
shortName = "pb-auth-example-app";
in {
options.services.${pname} = {
enable = lib.mkEnableOption
@ -70,17 +70,27 @@
description =
"Whether pocketbase should serve on https and issue own certs. Main case for true - when not under nginx";
};
useHostTls = lib.mkOption {
type = lib.types.bool;
default = false;
description =
"Whether virtual host should enable NixOS ACME certs";
};
config = lib.mkIf cfg.enable {
users.groups."${shortName}-group" = { };
users.users."${shortName}-user" = {
isSystemUser = true;
group = "${shortName}-group";
};
config = let
username = "${shortName}-user";
groupname = "${shortName}-group";
in lib.mkIf cfg.enable {
users.groups."${groupname}" = { };
users.users."${username}" = {
isNormalUser = true; # needed to allow for home dir
group = "${groupname}";
};
systemd.services.${shortName} = let
protocol = if cfg.usePbTls then "https" else "http";
serverHost = if cfg.useNginx then "127.0.0.1" else cfg.host;
servedAddress = "${protocol}://${serverHost}:${cfg.port}";
serveCliArg =
"--${protocol} ${serverHost}:${toString cfg.port}";
in {
description = "Exercise app ${pname}";
wantedBy = [ "multi-user.target" ];
@ -89,12 +99,32 @@
startLimitBurst = 10;
serviceConfig = {
ExecStart =
"${packages.auth-pocketbase-attempt}/bin/${pname} serve ${servedAddress} --dir=/home/${
config.users.users."${shortName}-user"
"${packages.auth-pocketbase-attempt}/bin/${pname} serve ${serveCliArg} --dir=/home/${
"${username}"
}";
Restart = "on-failure";
User = "${shortName}-user";
Group = "${shortName}-group";
User = "${username}";
Group = "${groupname}";
};
};
services.nginx = lib.mkIf cfg.useNginx {
virtualHosts.${cfg.host} = {
forceSSL = cfg.useHostTls;
enableACME = cfg.useHostTls;
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_read_timeout 360s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
'';
};
};
};
};

View File

@ -2,7 +2,6 @@ package main
import (
"log"
"github.com/pocketbase/pocketbase"
"sunshine.industries/auth-pocketbase-attempt/middleware"
"sunshine.industries/auth-pocketbase-attempt/pages"
@ -10,9 +9,13 @@ import (
func main() {
app := pocketbase.New()
middleware.AddCookieSessionMiddleware(app)
middleware.AddErrorsMiddleware(app)
pages.AddPageRoutes(app)
// starts the pocketbase backend
// parses cli arguments for hostname and data dir
if err := app.Start(); err != nil {
log.Fatal(err)
}

View File

@ -1,9 +1,8 @@
package middleware
import (
"fmt"
"log"
"net/http"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
@ -14,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))
@ -23,14 +43,12 @@ func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
// fires for every auth collection
app.OnRecordAuthRequest().Add(func(e *core.RecordAuthEvent) error {
log.Println(e.HttpContext)
log.Println(e.Record)
log.Println(e.Token)
log.Println(e.Meta)
e.HttpContext.SetCookie(&http.Cookie{
Name: AuthCookieName,
Value: e.Token,
Path: "/",
Secure: true,
HttpOnly: true,
})
e.HttpContext.SetCookie(&http.Cookie{
Name: "username",
@ -38,14 +56,14 @@ func AddCookieSessionMiddleware(app *pocketbase.PocketBase) {
})
return nil
})
// fires for admin authentication
app.OnAdminAuthRequest().Add(func(e *core.AdminAuthEvent) error {
log.Println(e.HttpContext)
log.Println(e.Admin)
log.Println(e.Token)
e.HttpContext.SetCookie(&http.Cookie{
Name: AuthCookieName,
Value: e.Token,
Path: "/",
Secure: true,
HttpOnly: true,
})
return nil
})
@ -74,14 +92,6 @@ func loadAuthContextFromCookie(app core.App) echo.MiddlewareFunc {
if err == nil && admin != nil {
// "authenticate" the admin
c.Set(apis.ContextAdminKey, admin)
someData := struct {
username string
email string
} {
admin.Email,
admin.Created.String(),
}
fmt.Printf("triggering the middlewar for cookie %v and err %v\n", someData, err)
}
case tokens.TypeAuthRecord:
@ -92,15 +102,6 @@ func loadAuthContextFromCookie(app core.App) echo.MiddlewareFunc {
if err == nil && record != nil {
// "authenticate" the app user
c.Set(apis.ContextAuthRecordKey, record)
someData := struct {
username string
email string
} {
record.Username(),
record.Email(),
}
fmt.Printf("triggering the middlewar for cookie %v and err %v\n", someData, err)
}
}
@ -118,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
View 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())
}

View File

@ -3,26 +3,38 @@ package pages
import (
"bytes"
"embed"
"fmt"
"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)
@ -42,43 +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
coolMessage := fmt.Sprintf("got admin %v and record %v. is guest: %t", admin, record, isGuest)
fmt.Print(coolMessage)
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)
}
}
fmt.Printf(">> enabled providers names %+v\n", oauthProviderNames)
navInfoData := initNavInfoData(app, c)
indexPageData := struct {
IsGuest, IsAdmin bool
Username string
EnabledOauthProviders []string
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
@ -105,22 +87,12 @@ func stringWithCharset(length int, charset string) string {
}
return string(b)
}
func somePageRoute(e *core.ServeEvent) error {
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
info := apis.RequestInfo(c)
admin := info.Admin // nil if not authenticated as admin
record := info.AuthRecord // nil if not authenticated as regular auth record
username := ""
switch {
case admin != nil:
username = admin.Email
case record != nil:
username = record.Username()
}
navInfoData := initNavInfoData(app, c)
somePageData := struct {
RandomNumber int
@ -129,9 +101,7 @@ func somePageRoute(e *core.ServeEvent) error {
}{
RandomNumber: rand.Int(),
RandomString: stringWithCharset(25, charset),
NavInfo: navInfo{
Username: username,
},
NavInfo: navInfoData,
}
// then render template with it
@ -145,4 +115,88 @@ func somePageRoute(e *core.ServeEvent) error {
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,
}
}

View File

@ -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
@ -46,7 +46,8 @@
</dialog>
<script defer type="text/javascript">
async function callOauth(providerName) {
const pb = new PocketBase("http://127.0.0.1:8090");
const baseUrl = window.location.protocol + "//" + window.location.host;
const pb = new PocketBase(baseUrl);
// This method initializes a one-off realtime subscription and will
// open a popup window with the OAuth2 vendor page to authenticate.

View 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 }}

View 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 }}

View 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 }}

View File

@ -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>