about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEmile <git@emile.space>2025-02-19 19:53:25 +0100
committerEmile <git@emile.space>2025-02-19 19:53:25 +0100
commitae39f02812bcfe903e956220c890bfb7b9bb9ff4 (patch)
treedff7028627665a7d2cb7cd64533ac74ec8919379
parent07425c679f7399284c0fe3dcbee54f45b23d07a0 (diff)
removed the backend, added the frontend with oidc support
So I've added oidc support which is nice, yet I have to test this
with some https foo, so I'm pushing this.
-rw-r--r--nix/hosts/corrino/www/templates/goapp/default.nix30
-rw-r--r--nix/templates/goapp/backend/default.nix16
-rw-r--r--nix/templates/goapp/backend/go.mod25
-rw-r--r--nix/templates/goapp/backend/go.sum59
-rw-r--r--nix/templates/goapp/backend/main.go80
-rw-r--r--nix/templates/goapp/flake.nix1
-rw-r--r--nix/templates/goapp/frontend/db.go37
-rw-r--r--nix/templates/goapp/frontend/default.nix4
-rw-r--r--nix/templates/goapp/frontend/go.mod13
-rw-r--r--nix/templates/goapp/frontend/go.sum26
-rw-r--r--nix/templates/goapp/frontend/log.go34
-rw-r--r--nix/templates/goapp/frontend/main.dbbin0 -> 8192 bytes
-rw-r--r--nix/templates/goapp/frontend/main.go80
-rwxr-xr-xnix/templates/goapp/frontend/run.sh9
-rw-r--r--nix/templates/goapp/frontend/server.log179
-rw-r--r--nix/templates/goapp/frontend/sessions.dbbin0 -> 24576 bytes
-rw-r--r--nix/templates/goapp/frontend/sqlitestore.go284
-rw-r--r--nix/templates/goapp/frontend/src/db.go (renamed from nix/templates/goapp/backend/db.go)0
-rw-r--r--nix/templates/goapp/frontend/src/handlers.go236
-rw-r--r--nix/templates/goapp/frontend/src/init.go76
-rw-r--r--nix/templates/goapp/frontend/src/log.go (renamed from nix/templates/goapp/backend/log.go)0
-rw-r--r--nix/templates/goapp/frontend/src/main.go96
-rw-r--r--nix/templates/goapp/frontend/src/sqlitestore.go (renamed from nix/templates/goapp/backend/sqlitestore.go)1
-rw-r--r--nix/templates/goapp/frontend/src/templates.go42
-rw-r--r--nix/templates/goapp/frontend/src/types.go65
-rw-r--r--nix/templates/goapp/frontend/src/util.go58
-rw-r--r--nix/templates/goapp/frontend/templates/footer.html8
-rw-r--r--nix/templates/goapp/frontend/templates/head.html153
-rw-r--r--nix/templates/goapp/frontend/templates/index.html81
-rw-r--r--nix/templates/goapp/frontend/templates/login.html41
-rw-r--r--nix/templates/goapp/frontend/templates/nav.html41
31 files changed, 1150 insertions, 625 deletions
diff --git a/nix/hosts/corrino/www/templates/goapp/default.nix b/nix/hosts/corrino/www/templates/goapp/default.nix
new file mode 100644
index 0000000..716d6ab
--- /dev/null
+++ b/nix/hosts/corrino/www/templates/goapp/default.nix
@@ -0,0 +1,30 @@
+
+{
+  services.authelia.instances.main.settings.identity_providers.oidc.clients = [
+    {
+      id = "goapp";
+
+      # ; nix run nixpkgs#authelia -- crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
+      secret = "$pbkdf2-sha512$310000$WUai4pp1ZVJDrJ8j6ICLiQ$NOMMaCZ3gt.x.a09MWatMkJWQIaH0QeWgRXSbuD2iWRwR.N6MWmJA6QO.LIKcxn6l.zHZN4bO1Ztsrbo9010Tw";
+      public = false;
+      authorization_policy = "two_factor";
+      redirect_uris = [ "https://127.0.0.1:8080/auth/oauth2/callback" ];
+      scopes = [
+        "openid"
+        "email"
+        "profile"
+      ];
+      grant_types = [
+        "refresh_token"
+        "authorization_code"
+      ];
+      response_types = [ "code" ];
+      response_modes = [
+        "form_post"
+        "query"
+        "fragment"
+      ];
+      token_endpoint_auth_method = "client_secret_post";
+    }
+  ];
+}
diff --git a/nix/templates/goapp/backend/default.nix b/nix/templates/goapp/backend/default.nix
deleted file mode 100644
index f4ed3a4..0000000
--- a/nix/templates/goapp/backend/default.nix
+++ /dev/null
@@ -1,16 +0,0 @@
-{ pkgs, name, ... }:
-
-let
-  version = "0.0.1";
-in
-pkgs.buildGoModule {
-  name = "${name}-${version}";
-  pname = "${name}";
-  version = "${version}";
-
-  src = ./.;
-  subPackages = [ "" ];
-  vendorHash = "sha256-tIk8lmyuVETrOW7fA7K7uNNXAAtJAYSM4uH+xZaMWqc=";
-
-  doCheck = true;
-}
diff --git a/nix/templates/goapp/backend/go.mod b/nix/templates/goapp/backend/go.mod
deleted file mode 100644
index 40a7d7f..0000000
--- a/nix/templates/goapp/backend/go.mod
+++ /dev/null
@@ -1,25 +0,0 @@
-module github.com/hanemile/goapp/backend
-
-go 1.23.5
-
-require (
-	github.com/gorilla/handlers v1.5.2
-	github.com/gorilla/mux v1.8.1
-	github.com/gorilla/securecookie v1.1.2
-	github.com/gorilla/sessions v1.4.0
-	modernc.org/sqlite v1.34.5
-)
-
-require (
-	github.com/dustin/go-humanize v1.0.1 // indirect
-	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/google/uuid v1.6.0 // indirect
-	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-sqlite3 v1.14.24 // indirect
-	github.com/ncruces/go-strftime v0.1.9 // indirect
-	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	golang.org/x/sys v0.22.0 // indirect
-	modernc.org/libc v1.55.3 // indirect
-	modernc.org/mathutil v1.6.0 // indirect
-	modernc.org/memory v1.8.0 // indirect
-)
diff --git a/nix/templates/goapp/backend/go.sum b/nix/templates/goapp/backend/go.sum
deleted file mode 100644
index 53d3f31..0000000
--- a/nix/templates/goapp/backend/go.sum
+++ /dev/null
@@ -1,59 +0,0 @@
-github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
-github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
-github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
-github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
-github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
-github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
-github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
-github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
-github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
-github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
-github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
-github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
-golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
-modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
-modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
-modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
-modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
-modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
-modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
-modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
-modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
-modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
-modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
-modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
-modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
-modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
-modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
-modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
-modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
-modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
-modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/nix/templates/goapp/backend/main.go b/nix/templates/goapp/backend/main.go
deleted file mode 100644
index d793869..0000000
--- a/nix/templates/goapp/backend/main.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"net/http"
-	"os"
-	"time"
-
-	"github.com/gorilla/mux"
-)
-
-var (
-	host          string
-	port          int
-	logFilePath   string
-	databasePath  string
-	sessiondbPath string
-	globalState   *State
-)
-
-func initFlags() {
-	flag.StringVar(&host, "host", "127.0.0.1", "The host to listen on")
-	flag.StringVar(&host, "h", "127.0.0.1", "The host to listen on (shorthand)")
-
-	flag.IntVar(&port, "port", 8080, "The port to listen on")
-	flag.IntVar(&port, "p", 8080, "The port to listen on (shorthand)")
-
-	flag.StringVar(&logFilePath, "logfilepath", "./server.log", "The path to the log file")
-	flag.StringVar(&databasePath, "databasepath", "./main.db", "The path to the main database")
-	flag.StringVar(&sessiondbPath, "sessiondbpath", "./sessions.db", "The path to the session database")
-}
-
-func indexHandler(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintf(w, "Hello World from the backend")
-}
-
-func main() {
-	initFlags()
-	flag.Parse()
-
-	// log init
-	log.Println("[i] Setting up logging...")
-	logFile, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
-	if err != nil {
-		log.Fatal("Error opening the server.log file: ", err)
-	}
-	logger := loggingMiddleware{logFile}
-
-	// db init
-	log.Println("[i] Setting up Global State Struct...")
-	s, err := NewState()
-	if err != nil {
-		log.Fatal("Error creating the NewState(): ", err)
-	}
-	globalState = s
-
-	// session init
-	log.Println("[i] Setting up Session Storage...")
-	store, err := NewSqliteStore(sessiondbPath, "sessions", "/", 3600, []byte(os.Getenv("SESSION_KEY")))
-	if err != nil {
-		panic(err)
-	}
-	globalState.sessions = store
-
-	r := mux.NewRouter()
-	r.Use(logger.Middleware)
-	r.HandleFunc("/", indexHandler)
-
-	srv := &http.Server{
-		Handler:      r,
-		Addr:         ":8080",
-		WriteTimeout: 15 * time.Second,
-		ReadTimeout:  15 * time.Second,
-	}
-
-	log.Printf("[i] Running the server on %s", srv.Addr)
-	log.Fatal(srv.ListenAndServe())
-}
diff --git a/nix/templates/goapp/flake.nix b/nix/templates/goapp/flake.nix
index 8303eb3..1ca876f 100644
--- a/nix/templates/goapp/flake.nix
+++ b/nix/templates/goapp/flake.nix
@@ -30,7 +30,6 @@
       in
       {
         packages = { }
-                   // (package-and-docker "backend")
                    // (package-and-docker "frontend");
 
         devShells.default = pkgs.mkShell {
diff --git a/nix/templates/goapp/frontend/db.go b/nix/templates/goapp/frontend/db.go
deleted file mode 100644
index fd3605a..0000000
--- a/nix/templates/goapp/frontend/db.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package main
-
-import (
-	"database/sql"
-	"log"
-
-	_ "github.com/mattn/go-sqlite3"
-)
-
-const create string = `
-CREATE TABLE IF NOT EXISTS users (
-	id INTEGER NOT NULL PRIMARY KEY,
-	created_at DATETIME NOT NULL,
-	name TEXT,
-	passwordHash TEXT
-);
-`
-
-type State struct {
-	db       *sql.DB      // the database storing the "business data"
-	sessions *SqliteStore // the database storing sessions
-}
-
-func NewState() (*State, error) {
-	db, err := sql.Open("sqlite3", databasePath)
-	if err != nil {
-		log.Println("Error opening the db: ", err)
-		return nil, err
-	}
-	if _, err := db.Exec(create); err != nil {
-		log.Println("Error creating the tables: ", err)
-		return nil, err
-	}
-	return &State{
-		db: db,
-	}, nil
-}
diff --git a/nix/templates/goapp/frontend/default.nix b/nix/templates/goapp/frontend/default.nix
index f4ed3a4..42ccb79 100644
--- a/nix/templates/goapp/frontend/default.nix
+++ b/nix/templates/goapp/frontend/default.nix
@@ -9,8 +9,8 @@ pkgs.buildGoModule {
   version = "${version}";
 
   src = ./.;
-  subPackages = [ "" ];
-  vendorHash = "sha256-tIk8lmyuVETrOW7fA7K7uNNXAAtJAYSM4uH+xZaMWqc=";
+  subPackages = [ "src" ];
+  vendorHash = "sha256-VXuhsXejduIcthawj4qu7hruBEDegj27YY0ym5srMQY=";
 
   doCheck = true;
 }
diff --git a/nix/templates/goapp/frontend/go.mod b/nix/templates/goapp/frontend/go.mod
index 1532a66..fecf4ac 100644
--- a/nix/templates/goapp/frontend/go.mod
+++ b/nix/templates/goapp/frontend/go.mod
@@ -1,4 +1,4 @@
-module github.com/hanemile/goapp/frontend
+module github.com/hanemile/goapp/backend
 
 go 1.23.5
 
@@ -7,18 +7,25 @@ require (
 	github.com/gorilla/mux v1.8.1
 	github.com/gorilla/securecookie v1.1.2
 	github.com/gorilla/sessions v1.4.0
+	github.com/mattn/go-sqlite3 v1.14.24
+	golang.org/x/crypto v0.33.0
+	golang.org/x/oauth2 v0.21.0
 	modernc.org/sqlite v1.34.5
 )
 
 require (
+	github.com/coreos/go-oidc/v3 v3.12.0 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.2 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-sqlite3 v1.14.24 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	golang.org/x/sys v0.22.0 // indirect
+	github.com/spf13/cobra v1.9.1 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	golang.org/x/sys v0.30.0 // indirect
 	modernc.org/libc v1.55.3 // indirect
 	modernc.org/mathutil v1.6.0 // indirect
 	modernc.org/memory v1.8.0 // indirect
diff --git a/nix/templates/goapp/frontend/go.sum b/nix/templates/goapp/frontend/go.sum
index 53d3f31..365e2c5 100644
--- a/nix/templates/goapp/frontend/go.sum
+++ b/nix/templates/goapp/frontend/go.sum
@@ -1,9 +1,14 @@
+github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
+github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
+github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
@@ -18,6 +23,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
@@ -26,13 +33,24 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
 golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
 golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
-golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
 golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
 modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
 modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
diff --git a/nix/templates/goapp/frontend/log.go b/nix/templates/goapp/frontend/log.go
deleted file mode 100644
index 5af719a..0000000
--- a/nix/templates/goapp/frontend/log.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package main
-
-import (
-	"net/http"
-	"os"
-
-	"github.com/gorilla/handlers"
-)
-
-// Defines a middleware containing a logfile
-//
-// This is done to combine gorilla/handlers with gorilla/mux middlewares to
-// just use r.Use(logger.Middleware) once instead of adding this to all
-// handlers manually (Yes, I'm really missing macros in Go...)
-type loggingMiddleware struct {
-	logFile *os.File
-}
-
-func (l *loggingMiddleware) Middleware(next http.Handler) http.Handler {
-	return handlers.LoggingHandler(l.logFile, next)
-}
-
-func authMiddleware(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		session, _ := globalState.sessions.Get(r, "session")
-		username := session.Values["username"]
-
-		if username == nil {
-			http.Redirect(w, r, "/login", http.StatusSeeOther)
-		} else {
-			next.ServeHTTP(w, r)
-		}
-	})
-}
diff --git a/nix/templates/goapp/frontend/main.db b/nix/templates/goapp/frontend/main.db
new file mode 100644
index 0000000..da9d88e
--- /dev/null
+++ b/nix/templates/goapp/frontend/main.db
Binary files differdiff --git a/nix/templates/goapp/frontend/main.go b/nix/templates/goapp/frontend/main.go
deleted file mode 100644
index f6605be..0000000
--- a/nix/templates/goapp/frontend/main.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"net/http"
-	"os"
-	"time"
-
-	"github.com/gorilla/mux"
-)
-
-var (
-	host          string
-	port          int
-	logFilePath   string
-	databasePath  string
-	sessiondbPath string
-	globalState   *State
-)
-
-func initFlags() {
-	flag.StringVar(&host, "host", "127.0.0.1", "The host to listen on")
-	flag.StringVar(&host, "h", "127.0.0.1", "The host to listen on (shorthand)")
-
-	flag.IntVar(&port, "port", 8080, "The port to listen on")
-	flag.IntVar(&port, "p", 8080, "The port to listen on (shorthand)")
-
-	flag.StringVar(&logFilePath, "logfilepath", "./server.log", "The path to the log file")
-	flag.StringVar(&databasePath, "databasepath", "./main.db", "The path to the main database")
-	flag.StringVar(&sessiondbPath, "sessiondbpath", "./sessions.db", "The path to the session database")
-}
-
-func indexHandler(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintf(w, "Hello World from the frontend")
-}
-
-func main() {
-	initFlags()
-	flag.Parse()
-
-	// log init
-	log.Println("[i] Setting up logging...")
-	logFile, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
-	if err != nil {
-		log.Fatal("Error opening the server.log file: ", err)
-	}
-	logger := loggingMiddleware{logFile}
-
-	// db init
-	log.Println("[i] Setting up Global State Struct...")
-	s, err := NewState()
-	if err != nil {
-		log.Fatal("Error creating the NewState(): ", err)
-	}
-	globalState = s
-
-	// session init
-	log.Println("[i] Setting up Session Storage...")
-	store, err := NewSqliteStore(sessiondbPath, "sessions", "/", 3600, []byte(os.Getenv("SESSION_KEY")))
-	if err != nil {
-		panic(err)
-	}
-	globalState.sessions = store
-
-	r := mux.NewRouter()
-	r.Use(logger.Middleware)
-	r.HandleFunc("/", indexHandler)
-
-	srv := &http.Server{
-		Handler:      r,
-		Addr:         ":8080",
-		WriteTimeout: 15 * time.Second,
-		ReadTimeout:  15 * time.Second,
-	}
-
-	log.Printf("[i] Running the server on %s", srv.Addr)
-	log.Fatal(srv.ListenAndServe())
-}
diff --git a/nix/templates/goapp/frontend/run.sh b/nix/templates/goapp/frontend/run.sh
new file mode 100755
index 0000000..fb3c7b3
--- /dev/null
+++ b/nix/templates/goapp/frontend/run.sh
@@ -0,0 +1,9 @@
+export CLIENT_ID=goapp
+export CLIENT_SECRET=KGFO5LQnUxu1Zs.35gOem3MaG8odthg1U0v0.kScVPS6TPTWVRnAdT_nj4PYYSfuU6jdzTM6
+export CLIENT_CALLBACK_URL=http://localhost:8080/oauth2/callback
+export VERSION=0.0.1
+export SESSION_KEY=aes1Itheich4aeQu9Ouz7ahcaiVoogh9
+go run ./... \
+  --id goapp \
+  --issuer "https://sso.emile.space" \
+  --secret "KGFO5LQnUxu1Zs.35gOem3MaG8odthg1U0v0.kScVPS6TPTWVRnAdT_nj4PYYSfuU6jdzTM6"
diff --git a/nix/templates/goapp/frontend/server.log b/nix/templates/goapp/frontend/server.log
new file mode 100644
index 0000000..4b6cff5
--- /dev/null
+++ b/nix/templates/goapp/frontend/server.log
@@ -0,0 +1,179 @@
+::1 - - [16/Feb/2025:23:03:46 +0100] "GET / HTTP/1.1" 200 5445
+::1 - - [16/Feb/2025:23:04:16 +0100] "GET / HTTP/1.1" 200 5445
+::1 - - [16/Feb/2025:23:04:17 +0100] "GET / HTTP/1.1" 200 5445
+::1 - - [16/Feb/2025:23:04:58 +0100] "GET / HTTP/1.1" 200 5509
+::1 - - [16/Feb/2025:23:05:03 +0100] "GET / HTTP/1.1" 200 5509
+::1 - - [16/Feb/2025:23:05:23 +0100] "GET / HTTP/1.1" 200 5573
+::1 - - [16/Feb/2025:23:05:33 +0100] "GET / HTTP/1.1" 200 5573
+::1 - - [16/Feb/2025:23:06:07 +0100] "GET / HTTP/1.1" 200 5614
+::1 - - [16/Feb/2025:23:06:17 +0100] "GET / HTTP/1.1" 200 5630
+::1 - - [16/Feb/2025:23:06:37 +0100] "GET / HTTP/1.1" 200 5651
+::1 - - [16/Feb/2025:23:06:59 +0100] "GET / HTTP/1.1" 200 5652
+::1 - - [16/Feb/2025:23:07:28 +0100] "GET / HTTP/1.1" 200 5676
+::1 - - [16/Feb/2025:23:08:26 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:30 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:42 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:43 +0100] "GET / HTTP/1.1" 200 5662
+::1 - - [16/Feb/2025:23:08:54 +0100] "GET / HTTP/1.1" 200 5677
+::1 - - [16/Feb/2025:23:09:25 +0100] "GET / HTTP/1.1" 200 5614
+::1 - - [16/Feb/2025:23:09:44 +0100] "GET / HTTP/1.1" 200 5639
+::1 - - [16/Feb/2025:23:10:13 +0100] "GET / HTTP/1.1" 200 5661
+::1 - - [16/Feb/2025:23:10:48 +0100] "GET / HTTP/1.1" 200 5672
+::1 - - [16/Feb/2025:23:10:49 +0100] "GET / HTTP/1.1" 200 5672
+::1 - - [16/Feb/2025:23:11:32 +0100] "GET / HTTP/1.1" 200 5648
+::1 - - [16/Feb/2025:23:11:32 +0100] "GET / HTTP/1.1" 200 5648
+::1 - - [16/Feb/2025:23:11:57 +0100] "GET / HTTP/1.1" 200 5655
+::1 - - [16/Feb/2025:23:12:11 +0100] "GET / HTTP/1.1" 200 5675
+::1 - - [16/Feb/2025:23:12:13 +0100] "GET / HTTP/1.1" 200 5675
+::1 - - [16/Feb/2025:23:12:46 +0100] "GET / HTTP/1.1" 200 5724
+::1 - - [16/Feb/2025:23:12:54 +0100] "GET / HTTP/1.1" 200 5722
+::1 - - [16/Feb/2025:23:13:03 +0100] "GET / HTTP/1.1" 200 5721
+::1 - - [16/Feb/2025:23:13:11 +0100] "GET / HTTP/1.1" 200 5721
+::1 - - [16/Feb/2025:23:13:42 +0100] "GET / HTTP/1.1" 200 5719
+::1 - - [17/Feb/2025:10:27:07 +0100] "GET / HTTP/1.1" 200 5719
+::1 - - [17/Feb/2025:10:27:09 +0100] "GET / HTTP/1.1" 200 5719
+::1 - - [17/Feb/2025:10:28:32 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:28:40 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:28:46 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:28:52 +0100] "GET / HTTP/1.1" 200 5854
+::1 - - [17/Feb/2025:10:29:22 +0100] "GET / HTTP/1.1" 200 5858
+::1 - - [17/Feb/2025:10:29:54 +0100] "GET / HTTP/1.1" 200 5841
+::1 - - [17/Feb/2025:10:30:09 +0100] "GET / HTTP/1.1" 200 5802
+::1 - - [17/Feb/2025:10:30:10 +0100] "GET / HTTP/1.1" 200 5802
+::1 - - [17/Feb/2025:10:30:20 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:14 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:26 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:31 +0100] "GET / HTTP/1.1" 200 5866
+::1 - - [17/Feb/2025:10:31:47 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:34:25 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:35:22 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:36:10 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:37:10 +0100] "POST /submit HTTP/1.1" 200 10
+::1 - - [17/Feb/2025:10:37:51 +0100] "POST /submit HTTP/1.1" 200 15
+::1 - - [17/Feb/2025:10:37:55 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:37:59 +0100] "POST /submit HTTP/1.1" 200 15
+::1 - - [17/Feb/2025:10:38:26 +0100] "POST /submit HTTP/1.1" 200 35
+::1 - - [17/Feb/2025:10:38:46 +0100] "POST /submit HTTP/1.1" 200 21
+::1 - - [17/Feb/2025:10:40:03 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:10:40:28 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:10:41:20 +0100] "GET / HTTP/1.1" 200 5786
+::1 - - [17/Feb/2025:10:41:38 +0100] "GET / HTTP/1.1" 200 5806
+::1 - - [17/Feb/2025:10:41:44 +0100] "GET / HTTP/1.1" 200 5806
+::1 - - [17/Feb/2025:10:41:53 +0100] "GET / HTTP/1.1" 200 6331
+::1 - - [17/Feb/2025:10:42:24 +0100] "GET / HTTP/1.1" 200 6238
+::1 - - [17/Feb/2025:10:43:00 +0100] "GET / HTTP/1.1" 200 6268
+::1 - - [17/Feb/2025:10:43:10 +0100] "GET / HTTP/1.1" 200 6260
+::1 - - [17/Feb/2025:10:44:54 +0100] "GET / HTTP/1.1" 200 6260
+::1 - - [17/Feb/2025:10:46:05 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:49:07 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:15 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:16 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:26 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:27 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:51:45 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:51:59 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:52:56 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:52:58 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:52:59 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:11 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:13 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:29 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:53:30 +0100] "GET /login HTTP/1.1" 200 0
+::1 - - [17/Feb/2025:10:54:04 +0100] "GET /login HTTP/1.1" 200 6974
+::1 - - [17/Feb/2025:10:54:11 +0100] "GET /login HTTP/1.1" 200 6917
+::1 - - [17/Feb/2025:10:54:28 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:28 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:30 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:33 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:35 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:39 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:10:54:42 +0100] "GET / HTTP/1.1" 200 6398
+::1 - - [17/Feb/2025:10:54:55 +0100] "GET / HTTP/1.1" 200 6388
+::1 - - [17/Feb/2025:10:54:58 +0100] "POST /submit HTTP/1.1" 200 21
+::1 - - [17/Feb/2025:10:58:19 +0100] "GET / HTTP/1.1" 200 6388
+::1 - - [17/Feb/2025:10:58:42 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:10:58:43 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:10:58:43 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:13:44:10 +0100] "GET / HTTP/1.1" 200 6316
+::1 - - [17/Feb/2025:13:44:11 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:13:44:23 +0100] "GET /login HTTP/1.1" 200 6724
+::1 - - [17/Feb/2025:14:37:43 +0100] "GET /login HTTP/1.1" 500 56
+::1 - - [17/Feb/2025:14:38:57 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:40:38 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:43:24 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:45:41 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:46:07 +0100] "GET / HTTP/1.1" 200 5683
+::1 - - [17/Feb/2025:14:46:14 +0100] "GET / HTTP/1.1" 200 5894
+::1 - - [17/Feb/2025:14:46:23 +0100] "GET / HTTP/1.1" 200 5734
+::1 - - [17/Feb/2025:14:47:02 +0100] "GET / HTTP/1.1" 200 5835
+::1 - - [17/Feb/2025:14:47:14 +0100] "GET / HTTP/1.1" 200 5873
+::1 - - [17/Feb/2025:14:47:22 +0100] "GET / HTTP/1.1" 200 5899
+::1 - - [17/Feb/2025:14:48:10 +0100] "GET / HTTP/1.1" 200 5790
+::1 - - [17/Feb/2025:14:48:15 +0100] "GET / HTTP/1.1" 200 5899
+::1 - - [17/Feb/2025:14:48:47 +0100] "GET / HTTP/1.1" 200 5899
+::1 - - [17/Feb/2025:14:48:53 +0100] "GET / HTTP/1.1" 200 5790
+::1 - - [17/Feb/2025:14:49:07 +0100] "GET / HTTP/1.1" 200 5927
+::1 - - [17/Feb/2025:14:49:16 +0100] "GET / HTTP/1.1" 200 5790
+::1 - - [17/Feb/2025:14:49:24 +0100] "GET / HTTP/1.1" 200 5919
+::1 - - [17/Feb/2025:14:51:08 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:51:48 +0100] "GET / HTTP/1.1" 200 6285
+::1 - - [17/Feb/2025:14:52:24 +0100] "GET / HTTP/1.1" 200 6224
+::1 - - [17/Feb/2025:14:53:46 +0100] "GET / HTTP/1.1" 200 6242
+::1 - - [17/Feb/2025:14:54:34 +0100] "GET / HTTP/1.1" 200 6222
+::1 - - [17/Feb/2025:14:55:14 +0100] "GET / HTTP/1.1" 200 6284
+::1 - - [17/Feb/2025:14:55:18 +0100] "GET / HTTP/1.1" 200 6284
+::1 - - [17/Feb/2025:14:55:45 +0100] "GET / HTTP/1.1" 200 6360
+::1 - - [17/Feb/2025:14:57:06 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:57:43 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:14:58:27 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:15:01:47 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:15:02:03 +0100] "GET / HTTP/1.1" 500 33
+::1 - - [17/Feb/2025:15:03:49 +0100] "GET / HTTP/1.1" 200 6360
+::1 - - [17/Feb/2025:15:06:07 +0100] "GET / HTTP/1.1" 200 6412
+::1 - - [17/Feb/2025:15:07:19 +0100] "GET / HTTP/1.1" 200 6412
+::1 - - [17/Feb/2025:15:07:38 +0100] "GET / HTTP/1.1" 200 6412
+::1 - - [17/Feb/2025:15:07:46 +0100] "GET / HTTP/1.1" 200 6514
+::1 - - [17/Feb/2025:15:07:58 +0100] "GET / HTTP/1.1" 200 6491
+::1 - - [17/Feb/2025:15:08:04 +0100] "GET /login HTTP/1.1" 500 56
+::1 - - [17/Feb/2025:15:08:56 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:08:56 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6491
+::1 - - [17/Feb/2025:15:08:58 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:08:58 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6491
+::1 - - [17/Feb/2025:15:20:07 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6586
+::1 - - [17/Feb/2025:15:20:41 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:20:45 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:20:45 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:20:53 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:20:53 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:24:15 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [17/Feb/2025:15:24:16 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [17/Feb/2025:15:24:16 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:57:18 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:57:19 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [19/Feb/2025:18:57:19 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:57:44 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [19/Feb/2025:18:57:44 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:58:54 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:18:58:55 +0100] "GET /login HTTP/1.1" 302 87
+::1 - - [19/Feb/2025:18:58:55 +0100] "GET /?client_id=&response_type=code&state=random-string-here HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:06:37 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:10:24 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:10:36 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:14:14 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:14:16 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:15:01 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:16:08 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:16:10 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:18:51 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:18:53 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:23:35 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:23:37 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:25:20 +0100] "GET /oauth2/callback HTTP/1.1" 500 314
+::1 - - [19/Feb/2025:19:25:37 +0100] "GET /oauth2/callback?code=authelia_ac_lA-8rLxGY4flmo-_DerONxfFPIVk2vpMiaCYZh_6ke0.FBoivMumLtPFauH9sWNVRz51S0FqWjwlFtqKO5sEA88&iss=https%3A%2F%2Fsso.emile.space&scope=openid+profile+email+groups&state=random-string-here HTTP/1.1" 500 314
+::1 - - [19/Feb/2025:19:27:27 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:27:28 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:43:04 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:43:05 +0100] "GET /login HTTP/1.1" 302 242
+::1 - - [19/Feb/2025:19:43:29 +0100] "GET /oauth2/callback?code=authelia_ac_8UdV__GJCN9gxJrYa629TC3FToyDDhsbacPbJzhvcJ4.uPw2-_N4jQr7xf7JNZ_IZBNHEq-eeOFoZup7Vwjx1Y0&iss=https%3A%2F%2Fsso.emile.space&scope=openid+profile+email+groups&state=random-string-here HTTP/1.1" 500 142
+::1 - - [19/Feb/2025:19:49:22 +0100] "GET / HTTP/1.1" 200 6587
+::1 - - [19/Feb/2025:19:49:23 +0100] "GET /login HTTP/1.1" 302 242
diff --git a/nix/templates/goapp/frontend/sessions.db b/nix/templates/goapp/frontend/sessions.db
new file mode 100644
index 0000000..04d6727
--- /dev/null
+++ b/nix/templates/goapp/frontend/sessions.db
Binary files differdiff --git a/nix/templates/goapp/frontend/sqlitestore.go b/nix/templates/goapp/frontend/sqlitestore.go
deleted file mode 100644
index 6f59d15..0000000
--- a/nix/templates/goapp/frontend/sqlitestore.go
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
-	Gorilla Sessions backend for SQLite.
-
-Copyright (c) 2013 Contributors. See the list of contributors in the CONTRIBUTORS file for details.
-
-This software is licensed under a MIT style license available in the LICENSE file.
-*/
-package main
-
-import (
-	"database/sql"
-	"encoding/gob"
-	"errors"
-	"fmt"
-	"log"
-	"net/http"
-	"strings"
-	"time"
-
-	"github.com/gorilla/securecookie"
-	"github.com/gorilla/sessions"
-	_ "modernc.org/sqlite"
-)
-
-type SqliteStore struct {
-	db         DB
-	stmtInsert *sql.Stmt
-	stmtDelete *sql.Stmt
-	stmtUpdate *sql.Stmt
-	stmtSelect *sql.Stmt
-
-	Codecs  []securecookie.Codec
-	Options *sessions.Options
-	table   string
-}
-
-type sessionRow struct {
-	id         string
-	data       string
-	createdOn  time.Time
-	modifiedOn time.Time
-	expiresOn  time.Time
-}
-
-type DB interface {
-	Exec(query string, args ...interface{}) (sql.Result, error)
-	Prepare(query string) (*sql.Stmt, error)
-	Close() error
-}
-
-func init() {
-	gob.Register(time.Time{})
-}
-
-func NewSqliteStore(endpoint string, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
-	db, err := sql.Open("sqlite3", endpoint)
-	if err != nil {
-		return nil, err
-	}
-
-	return NewSqliteStoreFromConnection(db, tableName, path, maxAge, keyPairs...)
-}
-
-func NewSqliteStoreFromConnection(db DB, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
-	// Make sure table name is enclosed.
-	tableName = "`" + strings.Trim(tableName, "`") + "`"
-
-	cTableQ := "CREATE TABLE IF NOT EXISTS " +
-		tableName + " (id INTEGER PRIMARY KEY, " +
-		"session_data LONGBLOB, " +
-		"created_on TIMESTAMP DEFAULT 0, " +
-		"modified_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
-		"expires_on TIMESTAMP DEFAULT 0);"
-	if _, err := db.Exec(cTableQ); err != nil {
-		return nil, err
-	}
-
-	insQ := "INSERT INTO " + tableName +
-		"(id, session_data, created_on, modified_on, expires_on) VALUES (NULL, ?, ?, ?, ?)"
-	stmtInsert, stmtErr := db.Prepare(insQ)
-	if stmtErr != nil {
-		return nil, stmtErr
-	}
-
-	delQ := "DELETE FROM " + tableName + " WHERE id = ?"
-	stmtDelete, stmtErr := db.Prepare(delQ)
-	if stmtErr != nil {
-		return nil, stmtErr
-	}
-
-	updQ := "UPDATE " + tableName + " SET session_data = ?, created_on = ?, expires_on = ? " +
-		"WHERE id = ?"
-	stmtUpdate, stmtErr := db.Prepare(updQ)
-	if stmtErr != nil {
-		return nil, stmtErr
-	}
-
-	selQ := "SELECT id, session_data, created_on, modified_on, expires_on from " +
-		tableName + " WHERE id = ?"
-	stmtSelect, stmtErr := db.Prepare(selQ)
-	if stmtErr != nil {
-		return nil, stmtErr
-	}
-
-	return &SqliteStore{
-		db:         db,
-		stmtInsert: stmtInsert,
-		stmtDelete: stmtDelete,
-		stmtUpdate: stmtUpdate,
-		stmtSelect: stmtSelect,
-		Codecs:     securecookie.CodecsFromPairs(keyPairs...),
-		Options: &sessions.Options{
-			Path:   path,
-			MaxAge: maxAge,
-		},
-		table: tableName,
-	}, nil
-}
-
-func (m *SqliteStore) Close() {
-	m.stmtSelect.Close()
-	m.stmtUpdate.Close()
-	m.stmtDelete.Close()
-	m.stmtInsert.Close()
-	m.db.Close()
-}
-
-func (m *SqliteStore) Get(r *http.Request, name string) (*sessions.Session, error) {
-	return sessions.GetRegistry(r).Get(m, name)
-}
-
-func (m *SqliteStore) New(r *http.Request, name string) (*sessions.Session, error) {
-	session := sessions.NewSession(m, name)
-	session.Options = &sessions.Options{
-		Path:   m.Options.Path,
-		MaxAge: m.Options.MaxAge,
-	}
-	session.IsNew = true
-	var err error
-	if cook, errCookie := r.Cookie(name); errCookie == nil {
-		err = securecookie.DecodeMulti(name, cook.Value, &session.ID, m.Codecs...)
-		if err == nil {
-			err = m.load(session)
-			if err == nil {
-				session.IsNew = false
-			} else {
-				err = nil
-			}
-		}
-	}
-	return session, err
-}
-
-func (m *SqliteStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
-	var err error
-	if session.ID == "" {
-		if err = m.insert(session); err != nil {
-			return err
-		}
-	} else if err = m.save(session); err != nil {
-		return err
-	}
-	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, m.Codecs...)
-	if err != nil {
-		return err
-	}
-	http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
-	return nil
-}
-
-func (m *SqliteStore) insert(session *sessions.Session) error {
-	var createdOn time.Time
-	var modifiedOn time.Time
-	var expiresOn time.Time
-	crOn := session.Values["created_on"]
-	if crOn == nil {
-		createdOn = time.Now()
-	} else {
-		createdOn = crOn.(time.Time)
-	}
-	modifiedOn = createdOn
-	exOn := session.Values["expires_on"]
-	if exOn == nil {
-		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
-	} else {
-		expiresOn = exOn.(time.Time)
-	}
-	delete(session.Values, "created_on")
-	delete(session.Values, "expires_on")
-	delete(session.Values, "modified_on")
-
-	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
-	if encErr != nil {
-		return encErr
-	}
-	res, insErr := m.stmtInsert.Exec(encoded, createdOn, modifiedOn, expiresOn)
-	if insErr != nil {
-		return insErr
-	}
-	lastInserted, lInsErr := res.LastInsertId()
-	if lInsErr != nil {
-		return lInsErr
-	}
-	session.ID = fmt.Sprintf("%d", lastInserted)
-	return nil
-}
-
-func (m *SqliteStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
-
-	// Set cookie to expire.
-	options := *session.Options
-	options.MaxAge = -1
-	http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options))
-	// Clear session values.
-	for k := range session.Values {
-		delete(session.Values, k)
-	}
-
-	_, delErr := m.stmtDelete.Exec(session.ID)
-	if delErr != nil {
-		return delErr
-	}
-	return nil
-}
-
-func (m *SqliteStore) save(session *sessions.Session) error {
-	if session.IsNew == true {
-		return m.insert(session)
-	}
-	var createdOn time.Time
-	var expiresOn time.Time
-	crOn := session.Values["created_on"]
-	if crOn == nil {
-		createdOn = time.Now()
-	} else {
-		createdOn = crOn.(time.Time)
-	}
-
-	exOn := session.Values["expires_on"]
-	if exOn == nil {
-		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
-		log.Print("nil")
-	} else {
-		expiresOn = exOn.(time.Time)
-		if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
-			expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
-		}
-	}
-
-	delete(session.Values, "created_on")
-	delete(session.Values, "expires_on")
-	delete(session.Values, "modified_on")
-	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
-	if encErr != nil {
-		return encErr
-	}
-	_, updErr := m.stmtUpdate.Exec(encoded, createdOn, expiresOn, session.ID)
-	if updErr != nil {
-		return updErr
-	}
-	return nil
-}
-
-func (m *SqliteStore) load(session *sessions.Session) error {
-	row := m.stmtSelect.QueryRow(session.ID)
-	sess := sessionRow{}
-	scanErr := row.Scan(&sess.id, &sess.data, &sess.createdOn, &sess.modifiedOn, &sess.expiresOn)
-	if scanErr != nil {
-		return scanErr
-	}
-	if sess.expiresOn.Sub(time.Now()) < 0 {
-		log.Printf("Session expired on %s, but it is %s now.", sess.expiresOn, time.Now())
-		return errors.New("Session expired")
-	}
-	err := securecookie.DecodeMulti(session.Name(), sess.data, &session.Values, m.Codecs...)
-	if err != nil {
-		return err
-	}
-	session.Values["created_on"] = sess.createdOn
-	session.Values["modified_on"] = sess.modifiedOn
-	session.Values["expires_on"] = sess.expiresOn
-	return nil
-
-}
diff --git a/nix/templates/goapp/backend/db.go b/nix/templates/goapp/frontend/src/db.go
index fd3605a..fd3605a 100644
--- a/nix/templates/goapp/backend/db.go
+++ b/nix/templates/goapp/frontend/src/db.go
diff --git a/nix/templates/goapp/frontend/src/handlers.go b/nix/templates/goapp/frontend/src/handlers.go
new file mode 100644
index 0000000..8fdd325
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/handlers.go
@@ -0,0 +1,236 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"log"
+	"net/http"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gorilla/sessions"
+	"golang.org/x/oauth2"
+)
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+	session, err := globalState.sessions.Get(r, "session")
+	if err != nil {
+		log.Println("error getting the session")
+	}
+
+	tpl := indexTplData{
+		Error: r.FormValue("error"),
+	}
+
+	tpl.Breadcrumbs = []Breadcrumb{
+		{
+			Link{"a", "b"},
+			[]Link{
+				{"c", "d"},
+				{"e", "f"},
+			},
+		},
+		{
+			Link{"g", "h"},
+			[]Link{
+				{"i", "j"},
+				{"k", "l"},
+			},
+		},
+	}
+	tpl.NextLinks = []Link{
+		{"Login", "/login"},
+	}
+
+	if logged, ok := session.Values["logged"].(bool); ok && logged {
+		tpl.LoggedIn = true
+		tpl.Claims.IDToken = session.Values["id_token"].(Claims)
+		tpl.Claims.UserInfo = session.Values["userinfo"].(Claims)
+
+		if len(options.GroupsFilter) >= 1 {
+			for _, group := range tpl.Claims.UserInfo.Groups {
+				if isStringInSlice(group, options.GroupsFilter) {
+					tpl.Groups = append(tpl.Groups, filterText(group, options.Filters))
+				}
+			}
+		} else {
+			tpl.Groups = filterSliceOfText(tpl.Claims.UserInfo.Groups, options.Filters)
+		}
+
+		tpl.Claims.IDToken.PreferredUsername = filterText(tpl.Claims.IDToken.PreferredUsername, options.Filters)
+		tpl.Claims.UserInfo.PreferredUsername = filterText(tpl.Claims.UserInfo.PreferredUsername, options.Filters)
+		tpl.Claims.IDToken.Audience = filterSliceOfText(tpl.Claims.IDToken.Audience, options.Filters)
+		tpl.Claims.UserInfo.Audience = filterSliceOfText(tpl.Claims.UserInfo.Audience, options.Filters)
+		tpl.Claims.IDToken.Issuer = filterText(tpl.Claims.IDToken.Issuer, options.Filters)
+		tpl.Claims.UserInfo.Issuer = filterText(tpl.Claims.UserInfo.Issuer, options.Filters)
+		tpl.Claims.IDToken.Email = filterText(tpl.Claims.IDToken.Email, options.Filters)
+		tpl.Claims.UserInfo.Email = filterText(tpl.Claims.UserInfo.Email, options.Filters)
+		tpl.Claims.IDToken.Name = filterText(tpl.Claims.IDToken.Name, options.Filters)
+		tpl.Claims.UserInfo.Name = filterText(tpl.Claims.UserInfo.Name, options.Filters)
+		tpl.RawToken = rawTokens[tpl.Claims.IDToken.JWTIdentifier]
+		tpl.AuthorizeCodeURL = acURLs[tpl.Claims.IDToken.JWTIdentifier].String()
+	}
+
+	w.Header().Add("Content-Type", "text/html")
+
+	// get the template
+	t, err := template.New("index").Funcs(templateFuncMap).ParseGlob(fmt.Sprintf("%s/*.html", options.TemplatesPath))
+	if err != nil {
+		log.Printf("Error reading the template Path: %s/*.html", options.TemplatesPath)
+		log.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Error reading template file"))
+		return
+	}
+
+	// exec!
+	err = t.ExecuteTemplate(w, "index", tpl)
+	if err != nil {
+		log.Println(err)
+	}
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+	log.Println("[ ] Getting the global session from the session cookie:")
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		log.Println("[ ] Error getting the cookie")
+		writeErr(w, nil, "error getting cookie", http.StatusInternalServerError)
+		return
+	}
+
+	log.Println("[ ] Setting the redirect URL")
+	session.Values["redirect-url"] = "/"
+
+	log.Println("[ ] Saving the session")
+	if err = session.Save(r, w); err != nil {
+		writeErr(w, err, "error saving session", http.StatusInternalServerError)
+		return
+	}
+
+	log.Printf("[ ] Redirecting to %s", oauth2Config.AuthCodeURL("random-string"))
+	http.Redirect(w, r, oauth2Config.AuthCodeURL("random-string-here"), http.StatusFound)
+}
+
+func logoutHandler(w http.ResponseWriter, r *http.Request) {
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		writeErr(w, err, "error getting cookie", http.StatusInternalServerError)
+		return
+	}
+
+	// wet the session
+	session.Values = make(map[interface{}]interface{})
+
+	if err = session.Save(r, w); err != nil {
+		writeErr(w, err, "error saving session", http.StatusInternalServerError)
+		return
+	}
+
+	http.Redirect(w, r, "/", http.StatusFound)
+}
+
+func oauthCallbackHandler(res http.ResponseWriter, req *http.Request) {
+	log.Println("hit the oauth callback handler")
+	if req.FormValue("error") != "" {
+		log.Printf("got an error from the idp: %s", req.FormValue("error"))
+		http.Redirect(res, req, fmt.Sprintf("/error?%s", req.Form.Encode()), http.StatusFound)
+
+		return
+	}
+
+	var (
+		token      *oauth2.Token
+		idToken    *oidc.IDToken
+		err        error
+		idTokenRaw string
+		ok         bool
+	)
+
+	// The state should be checked here in production
+	if token, err = oauth2Config.Exchange(req.Context(), req.URL.Query().Get("code")); err != nil {
+		log.Println("Unable to exchange authorization code for tokens")
+		writeErr(res, err, "unable to exchange authorization code for tokens", http.StatusInternalServerError)
+		return
+	}
+
+	// Extract the ID Token from OAuth2 token.
+	if idTokenRaw, ok = token.Extra("id_token").(string); !ok {
+		log.Println("missing id token")
+		writeErr(res, nil, "missing id token", http.StatusInternalServerError)
+		return
+	}
+
+	// Parse and verify ID Token payload.
+	if idToken, err = verifier.Verify(req.Context(), idTokenRaw); err != nil {
+		log.Printf("unable to verify id token or token is invalid: %+v", idTokenRaw)
+		writeErr(res, err, "unable to verify id token or token is invalid", http.StatusInternalServerError)
+		return
+	}
+
+	// Extract custom claims
+	claimsIDToken := Claims{}
+
+	if err = idToken.Claims(&claimsIDToken); err != nil {
+		log.Printf("unable to decode id token claims: %+v", &claimsIDToken)
+		writeErr(res, err, "unable to decode id token claims", http.StatusInternalServerError)
+		return
+	}
+
+	var userinfo *oidc.UserInfo
+
+	if userinfo, err = provider.UserInfo(req.Context(), oauth2.StaticTokenSource(token)); err != nil {
+		log.Printf("unable to retreive userinfo claims")
+		writeErr(res, err, "unable to retrieve userinfo claims", http.StatusInternalServerError)
+		return
+	}
+
+	claimsUserInfo := Claims{}
+
+	if err = userinfo.Claims(&claimsUserInfo); err != nil {
+		log.Printf("unable to decode userinfo claims")
+		writeErr(res, err, "unable to decode userinfo claims", http.StatusInternalServerError)
+		return
+	}
+
+	var session *sessions.Session
+
+	if session, err = globalState.sessions.Get(req, options.CookieName); err != nil {
+		log.Printf("unable to get session from cookie")
+		writeErr(res, err, "unable to get session from cookie", http.StatusInternalServerError)
+		return
+	}
+
+	session.Values["id_token"] = claimsIDToken
+	session.Values["userinfo"] = claimsUserInfo
+	session.Values["logged"] = true
+	rawTokens[claimsIDToken.JWTIdentifier] = idTokenRaw
+	acURLs[claimsIDToken.JWTIdentifier] = req.URL
+
+	if err = session.Save(req, res); err != nil {
+		log.Printf("unable to save session")
+		writeErr(res, err, "unable to save session", http.StatusInternalServerError)
+		return
+	}
+
+	var redirectUrl string
+
+	if redirectUrl, ok = session.Values["redirect-url"].(string); ok {
+		log.Printf("all fine!")
+		http.Redirect(res, req, redirectUrl, http.StatusFound)
+		return
+	}
+
+	http.Redirect(res, req, "/", http.StatusFound)
+}
+
+func writeErr(w http.ResponseWriter, err error, msg string, statusCode int) {
+	switch {
+	case err == nil:
+		log.Println(msg)
+		http.Error(w, msg, statusCode)
+	default:
+		log.Println(msg)
+		log.Println(err)
+		http.Error(w, fmt.Errorf("%s: %w", msg, err).Error(), statusCode)
+	}
+}
diff --git a/nix/templates/goapp/frontend/src/init.go b/nix/templates/goapp/frontend/src/init.go
new file mode 100644
index 0000000..97e58f0
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/init.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"golang.org/x/oauth2"
+)
+
+func logInit() loggingMiddleware {
+	log.Println("[i] Setting up logging...")
+	logFile, err := os.OpenFile(options.LogFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
+	if err != nil {
+		log.Fatal("Error opening the server.log file: ", err)
+	}
+	return loggingMiddleware{logFile}
+}
+
+func dbInit() {
+	log.Println("[i] Setting up Global State Struct...")
+	s, err := NewState()
+	if err != nil {
+		log.Fatal("Error creating the NewState(): ", err)
+	}
+	globalState = s
+}
+
+func sessionInit() {
+	log.Println("[i] Setting up Session Storage...")
+	store, err := NewSqliteStore(
+		sessiondbPath,
+		"sessions",
+		"/",
+		3600,
+		[]byte(os.Getenv("SESSION_KEY")))
+	if err != nil {
+		panic(err)
+	}
+	globalState.sessions = store
+}
+
+func oauth2Init() (err error) {
+	log.Println("[i] Setting up oauth2...")
+	var redirectURL *url.URL
+	if _, redirectURL, err = getURLs(options.PublicURL); err != nil {
+		return fmt.Errorf("could not parse public url: %w", err)
+	}
+
+	log.Printf("[ ] provider_url: %s", options.Issuer)
+	log.Printf("[ ] redirect_url: %s", redirectURL.String())
+
+	if provider, err = oidc.NewProvider(context.Background(), options.Issuer); err != nil {
+		log.Println("Error init oidc provider: ", err)
+		return fmt.Errorf("error initializing oidc provider: %w", err)
+	}
+
+	verifier = provider.Verifier(&oidc.Config{ClientID: options.ClientID})
+	log.Printf("[ ] ClientID: %s", options.ClientID)
+	log.Printf("[ ] ClientSecret: %s", options.ClientSecret)
+	log.Printf("[ ] redirectURL: %s", redirectURL.String())
+	log.Printf("[ ] providerEndpoint: %+v", provider.Endpoint())
+	log.Printf("[ ] Scopes: %s", options.Scopes)
+	oauth2Config = oauth2.Config{
+		ClientID:     options.ClientID,
+		ClientSecret: options.ClientSecret,
+		RedirectURL:  redirectURL.String(),
+		Endpoint:     provider.Endpoint(),
+		Scopes:       strings.Split(options.Scopes, ","),
+	}
+	return nil
+}
diff --git a/nix/templates/goapp/backend/log.go b/nix/templates/goapp/frontend/src/log.go
index 5af719a..5af719a 100644
--- a/nix/templates/goapp/backend/log.go
+++ b/nix/templates/goapp/frontend/src/log.go
diff --git a/nix/templates/goapp/frontend/src/main.go b/nix/templates/goapp/frontend/src/main.go
new file mode 100644
index 0000000..fcf4224
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/main.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+	"crypto/tls"
+	"fmt"
+	"log"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gorilla/mux"
+	"github.com/spf13/cobra"
+	"golang.org/x/oauth2"
+)
+
+var (
+	host          string
+	port          int
+	databasePath  string
+	logFilePath   string
+	sessiondbPath string
+	templatesPath string
+	globalState   *State
+
+	options      Options
+	oauth2Config oauth2.Config
+	provider     *oidc.Provider
+	verifier     *oidc.IDTokenVerifier
+
+	rawTokens = make(map[string]string)
+	acURLs    = make(map[string]*url.URL)
+)
+
+func main() {
+
+	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+	rootCmd := &cobra.Command{Use: "goapp", RunE: root}
+
+	rootCmd.Flags().StringVar(&options.Host, "host", "0.0.0.0", "Specifies the tcp host to listen on")
+	rootCmd.Flags().IntVar(&options.Port, "port", 8080, "Specifies the port to listen on")
+	rootCmd.Flags().StringVar(&options.PublicURL, "public-url", "http://localhost:8080/", "Specifies the root URL to generate the redirect URI")
+	rootCmd.Flags().StringVar(&options.ClientID, "id", "", "Specifies the OpenID Connect Client ID")
+	rootCmd.Flags().StringVarP(&options.ClientSecret, "secret", "s", "", "Specifies the OpenID Connect Client Secret")
+	rootCmd.Flags().StringVarP(&options.Issuer, "issuer", "i", "", "Specifies the URL for the OpenID Connect OP")
+	rootCmd.Flags().StringVar(&options.Scopes, "scopes", "openid,profile,email,groups", "Specifies the OpenID Connect scopes to request")
+	rootCmd.Flags().StringVar(&options.CookieName, "cookie-name", "oidc-client", "Specifies the storage cookie name to use")
+	rootCmd.Flags().StringSliceVar(&options.Filters, "filters", []string{}, "If specified filters the specified text from html output (not json) out of the email addresses, display names, audience, etc")
+	rootCmd.Flags().StringSliceVar(&options.GroupsFilter, "groups-filter", []string{}, "If specified only shows the groups in this list")
+	rootCmd.Flags().StringVar(&options.LogFilePath, "logpath", "./server.log", "Specifies the path to store the server logs at")
+	rootCmd.Flags().StringVar(&options.TemplatesPath, "templatespath", "./templates", "Specifies the path to where the templates are stored")
+
+	_ = rootCmd.MarkFlagRequired("id")
+	_ = rootCmd.MarkFlagRequired("secret")
+	_ = rootCmd.MarkFlagRequired("issuer")
+
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func root(cmd *cobra.Command, args []string) (err error) {
+
+	logger := logInit()
+	oauth2Init()
+	dbInit()
+	sessionInit()
+
+	r := mux.NewRouter()
+	r.Use(logger.Middleware)
+	r.HandleFunc("/", indexHandler)
+	r.HandleFunc("/login", loginHandler)
+	//  r.HandleFunc("/logout", )
+	//  r.HandleFunc("/error", loginHandler)
+	r.HandleFunc("/oauth2/callback", oauthCallbackHandler)
+	//  r.HandleFunc("/json", loginHandler)
+	//  r.HandleFunc("/jwt.json", loginHandler)
+
+	// endpoints with auth needed
+	auth_needed := r.PathPrefix("/").Subrouter()
+	auth_needed.Use(authMiddleware)
+	auth_needed.HandleFunc("/logout", logoutHandler)
+
+	serverAddress := fmt.Sprintf("%s:%d", options.Host, options.Port)
+	srv := &http.Server{
+		Handler:      r,
+		Addr:         serverAddress,
+		WriteTimeout: 15 * time.Second,
+		ReadTimeout:  15 * time.Second,
+	}
+
+	log.Printf("[i] Running the server on %s", serverAddress)
+	log.Fatal(srv.ListenAndServe())
+	return
+}
diff --git a/nix/templates/goapp/backend/sqlitestore.go b/nix/templates/goapp/frontend/src/sqlitestore.go
index 6f59d15..34e31e4 100644
--- a/nix/templates/goapp/backend/sqlitestore.go
+++ b/nix/templates/goapp/frontend/src/sqlitestore.go
@@ -50,6 +50,7 @@ type DB interface {
 
 func init() {
 	gob.Register(time.Time{})
+	gob.Register(Claims{})
 }
 
 func NewSqliteStore(endpoint string, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
diff --git a/nix/templates/goapp/frontend/src/templates.go b/nix/templates/goapp/frontend/src/templates.go
new file mode 100644
index 0000000..5ae9397
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/templates.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"html/template"
+	"strings"
+)
+
+var (
+	templateFuncMap = template.FuncMap{
+		"stringsJoin":      strings.Join,
+		"stringsEqualFold": strings.EqualFold,
+		"isStringInSlice":  isStringInSlice,
+	}
+)
+
+type indexTplData struct {
+	Title, Description, RawToken string
+
+	Breadcrumbs []Breadcrumb
+	NextLinks   []Link
+
+	Error            string
+	LoggedIn         bool
+	Claims           tplClaims
+	Groups           []string
+	AuthorizeCodeURL string
+}
+
+type Link struct {
+	Name   string
+	Target string
+}
+
+type Breadcrumb struct {
+	Main    Link
+	Options []Link
+}
+
+type tplClaims struct {
+	IDToken  Claims
+	UserInfo Claims
+}
diff --git a/nix/templates/goapp/frontend/src/types.go b/nix/templates/goapp/frontend/src/types.go
new file mode 100644
index 0000000..7efcc70
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/types.go
@@ -0,0 +1,65 @@
+package main
+
+type Claims struct {
+	JWTIdentifier                       string   `json:"jti"`
+	Issuer                              string   `json:"iss"`
+	Subject                             string   `json:"sub"`
+	Nonce                               string   `json:"nonce"`
+	Expires                             int64    `json:"exp"`
+	IssueTime                           int64    `json:"iat"`
+	RequestedAt                         int64    `json:"rat"`
+	AuthorizeTime                       int64    `json:"auth_time"`
+	NotBefore                           int64    `json:"nbf"`
+	Audience                            []string `json:"aud"`
+	Scope                               []string `json:"scp"`
+	ScopeString                         string   `json:"scope"`
+	AccessTokenHash                     string   `json:"at_hash"`
+	CodeHash                            string   `json:"c_hash"`
+	AuthenticationContextClassReference string   `json:"acr"`
+	AuthenticationMethodsReference      []string `json:"amr"`
+
+	Name                string       `json:"name"`
+	GivenName           string       `json:"given_name"`
+	FamilyName          string       `json:"family_name"`
+	MiddleName          string       `json:"middle_name"`
+	Nickname            string       `json:"nickname"`
+	PreferredUsername   string       `json:"preferred_username"`
+	Profile             string       `jsoon:"profile"`
+	Picture             string       `json:"picture"`
+	Website             string       `json:"website"`
+	Gender              string       `json:"gender"`
+	Birthdate           string       `json:"birthdate"`
+	ZoneInfo            string       `json:"zoneinfo"`
+	Locale              string       `json:"locale"`
+	UpdatedAt           int64        `json:"updated_at"`
+	Email               string       `json:"email"`
+	EmailAlts           []string     `json:"alt_emails"`
+	EmailVerified       bool         `json:"email_verified"`
+	PhoneNumber         string       `json:"phone_number"`
+	PhoneNumberVerified bool         `json:"phone_number_verified"`
+	Address             ClamsAddress `json:"address"`
+	Groups              []string     `json:"groups"`
+}
+
+type ClamsAddress struct {
+	StreetAddress string `json:"street_address"`
+	Locality      string `json:"locality"`
+	Region        string `json:"region"`
+	PostalCode    string `json:"postal_code"`
+	Country       string `json:"country"`
+}
+
+type Options struct {
+	Host          string
+	Port          int
+	LogFilePath   string
+	TemplatesPath string
+	ClientID      string
+	ClientSecret  string
+	Issuer        string
+	PublicURL     string
+	Scopes        string
+	CookieName    string
+	Filters       []string
+	GroupsFilter  []string
+}
diff --git a/nix/templates/goapp/frontend/src/util.go b/nix/templates/goapp/frontend/src/util.go
new file mode 100644
index 0000000..89d28ba
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/util.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+	"net/url"
+	"path"
+	"strings"
+)
+
+func isStringInSlice(s string, slice []string) bool {
+	for _, x := range slice {
+		if s == x {
+			return true
+		}
+	}
+
+	return false
+}
+
+func filterText(input string, filters []string) (output string) {
+	if len(filters) == 0 {
+		return input
+	}
+
+	for _, filter := range filters {
+		input = strings.Replace(input, filter, strings.Repeat("*", len(filter)), -1)
+	}
+
+	return input
+}
+
+func filterSliceOfText(input []string, filters []string) (output []string) {
+	for _, item := range input {
+		output = append(output, filterText(item, filters))
+	}
+
+	return output
+}
+
+func getURLs(rootURL string) (publicURL *url.URL, redirectURL *url.URL, err error) {
+	if publicURL, err = url.Parse(rootURL); err != nil {
+		return nil, nil, err
+	}
+
+	if publicURL.Scheme != "http" && publicURL.Scheme != "https" {
+		return nil, nil, fmt.Errorf("scheme must be http or https but it is '%s'", publicURL.Scheme)
+	}
+
+	if !strings.HasSuffix(publicURL.Path, "/") {
+		publicURL.Path += "/"
+	}
+
+	redirectURL = &url.URL{}
+	*redirectURL = *publicURL
+	redirectURL.Path = path.Join(redirectURL.Path, "/oauth2/callback")
+
+	return publicURL, redirectURL, nil
+}
diff --git a/nix/templates/goapp/frontend/templates/footer.html b/nix/templates/goapp/frontend/templates/footer.html
new file mode 100644
index 0000000..1899096
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/footer.html
@@ -0,0 +1,8 @@
+{{ define "footer" }}
+{{ if .asd }}
+<br><br><hr><br>
+{{ . }}
+{{ end }}
+</div>
+{{ end }}
+
diff --git a/nix/templates/goapp/frontend/templates/head.html b/nix/templates/goapp/frontend/templates/head.html
new file mode 100644
index 0000000..efc7ce3
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/head.html
@@ -0,0 +1,153 @@
+{{ define "head" }}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>goapp</title>
+
+  <style>
+* { word-wrap:break-word; font-family: monospace; margin: 0; padding: 0; }
+
+/* light/darktheme specific foo */
+@media (prefers-color-scheme: light) {
+  html { background: #fafafa; color: #040404; }
+  a:hover { color: #fafafa; background: #040404 }
+  a:not([href*="webring.xxiivv.com"]):hover, nav a:active { color: #fafafa; background: #040404 }
+  a { color: #040404; background: #fafafa; text-decoration: none;}
+  nav a:hover, a:active { color: #fafafa; background: #040404 }
+  nav { margin: 1ex 0; background: #eeeeee; }
+  nav a { display:block; background: #eeeeee; }
+  h1 { margin: 3ex 0 1ex 0; width: 100%; background-color: #eeeeee}
+  h2 { margin: 2ex 0 1ex 0; width: 100%; background-color: #eeeeee}
+  h3 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; background-color: #eeeeee}
+  h4 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #fafafa*/}
+  h5 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #fafafa*/}
+  .code { border-left: 1px solid #040404; margin-left: 2ex; padding-left: 1ex; }
+  .codeline:hover { background: #eeeeee; color: #040404; }
+  .trhover:hover { background: #c0c0c0; color: #040404; }
+
+  /* add an outline while hovering, the !important makes hovering on checked elements still visible */
+  .check-with-label:checked + .label-for-check { background-color: #040404; color: #eeeeee !important; }
+  .check-with-label:hover + .label-for-check { outline: 1px solid #040404; color: #040404; }
+
+  input, textarea {
+    outline: 1px solid #000000;
+    border: none;
+    background-color: #ffffff;
+    color: #000000;
+  }
+  button {
+    background-color: #ffffff; color: #000000; border: 1px solid #000000; border-style: solid;
+    margin-top: 1ex;
+  };
+}
+@media (prefers-color-scheme: dark) {
+  html { background: #040404; color: #c0c0c0; }
+  a:hover { color: #040404; background: #c0c0c0 }
+  body nav a:not([href*="webring.xxiivv.com"]):hover, nav a:active { color: #c0c0c0; background: #040404 }
+  a { color: #c0c0c0; background: #040404; text-decoration: none; }
+  nav a:hover, a:active { color: #040404; background: #c0c0c0 }
+  nav { margin: 1ex 0; background: #c0c0c0; }
+  nav a { display:block; background: #c0c0c0; }
+  h1 { margin: 3ex 0 1ex 0; width: 100%; background-color: #c0c0c0}
+  h2 { margin: 2ex 0 1ex 0; width: 100%; background-color: #c0c0c0}
+  h3 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; background-color: #c0c0c0}
+  h4 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #c0c0c0*/}
+  h5 { margin: 1ex 0 1ex 0; width: 100%; font-size: 1em; /*background-color: #c0c0c0*/}
+  .code { border-left: 1px solid #c0c0c0; margin-left: 2ex;  padding-left: 1ex; }
+  .codeline:hover { background: #c0c0c0; color: #040404; }
+  .webring { -webkit-filter: invert(100%); filter: invert(100%); }
+  .trhover:hover { background: #c0c0c0; color: #040404; }
+
+  /* add an outline while hovering, the !important makes hovering on checked elements still visible */
+  .check-with-label:checked + .label-for-check { background-color: #c0c0c0; color: #040404 !important; }
+  .check-with-label:hover + .label-for-check { outline: 1px solid #c0c0c0; color: #c0c0c0; }
+
+  input, textarea {
+    outline: 1px solid #ffffff;
+    border: none;
+    background-color: #000000;
+    color: #ffffff;
+  }
+
+  button {
+    background-color: #000000; color: #ffffff;
+    border: 1px solid #ffffff;
+    border-style: solid;
+    margin-top: 1ex;
+  };
+}
+
+/* settings for mobile devices*/
+@media only screen and (max-width: 768px) {
+  body { margin: 1ex; width: calc(100% - 2ex) !important; }
+  img { max-width: 100% !important; max-height: 500px; }
+}
+
+/* only display the hover dropdown on non-mobile devices */
+@media only screen and (min-width: 768px) {
+  nav ul li:hover a + ul { display: inherit; white-space: nowrap; }
+}
+
+img { max-width: 100ex; max-height: 500px; }
+
+body { margin-left: auto; margin-right: auto; margin-top: 1ex; margin-bottom: 1ex; width: 100ex; }
+
+.webring { align: right; }
+a .webring { float: right; }
+
+/* display local links using [] and external links using {} */
+body a:not(h1 a, h2 a, h3 a,h4 a):not([href*="webring.xxiivv.com"]):not([class*="local"]):before { content: "["; }
+body a:not(h1 a, h2 a, h3 a,h4 a):not([href*="webring.xxiivv.com"]):not([class*="local"]):after { content: "]"; }
+a[href*="//"]:not([href*="r2wa.rs"]):not([class*="icon"]):before { content: '{'; }
+a[href*="//"]:not([href*="r2wa.rs"]):not([class*="icon"]):after { content: '}'; }
+
+/* table { width: 100ex; } */
+input, textarea { width: 100%; }
+textarea { padding: 0.5ex; }
+
+ul { list-style-type: none; }
+
+/* navigation bar magic */
+nav * { color: #040404; }
+nav ul { list-style: none; position: relative; display: inline-block; }
+nav ul li { display:inline-block; }
+nav ul ul { display: none; position: absolute; outline: 1px solid #040404; background-color: #ff0; }
+nav ul ul li { width: 100%; padding-right: 1ex; float:none; display:list-item; position: relative; }
+nav + ul li { display: inline-block;}
+
+/* nav bar spacing char */
+nav ul li > a::after { content: " /"; }
+nav ul li > a:only-child::after { content: ""; }
+nav ul li:last-of-type a::after { content: ""; }
+
+h1 a, h2 a, h3 a { padding-right: 1ex; }
+
+pre { white-space: pre-wrap; hyphens: auto; }
+pre.code { white-space: pre-wrap; hyphens: none; }
+
+/* display the list of folders in the current one as a vertical list, if the
+ * .vert class is present */
+nav + ul.vert li { display: block; }
+
+.w-100 { width: 100%; }
+
+.check-with-label { display: none; } /* checkbox with a label */
+
+/* In tables, make the first column fit the content and the reset be relaxed */
+body table tbody { width: 100%; word-wrap: break-word; }
+/* body table tbody tr>td { padding: 0.5ex 0 0.5ex !important; } */
+body table tbody tr td:nth-child(1) { width: auto; white-space: nowrap; padding-right: 1ex; }
+body table tbody tr td:not(:nth-child(1)) { width: 100%; max-width: 100%; word-wrap: anywhere; }
+
+tr { text-wrap: wrap;}
+
+input { padding-left: 0.5ex; }
+input:focus { outline-offset: 0px; }
+textarea:focus { outline-offset: 0px; }
+
+  </style>
+</head>
+{{ end }}
diff --git a/nix/templates/goapp/frontend/templates/index.html b/nix/templates/goapp/frontend/templates/index.html
new file mode 100644
index 0000000..1d21f3d
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/index.html
@@ -0,0 +1,81 @@
+{{ define "index" }}
+
+{{ template "head" . }}
+{{ template "nav" . }}
+
+<h1>goapp</h1>
+
+{{- if .LoggedIn }}
+<p id="welcome">Logged in as {{ or .Claims.UserInfo.PreferredUsername .Claims.IDToken.Subject "unknown" }}!</p>
+<p><a href="/logout" id="log-out">Log out</a></p>
+<p>Access Token Hash: <span id="claim-at_hash">{{ .Claims.IDToken.AccessTokenHash }}</span></p>
+<p>Code Hash: <span id="claim-c_hash">{{ .Claims.IDToken.CodeHash }}</span></p>
+<p>Authentication Context Class Reference: <span id="claim-acr">{{ .Claims.IDToken.AuthenticationContextClassReference }}</span></p>
+<p>Authentication Methods Reference: <span id="claim-amr">{{ stringsJoin .Claims.IDToken.AuthenticationMethodsReference ", " }}</span></p>
+<p>Audience: <span id="claim-aud">{{ stringsJoin .Claims.IDToken.Audience ", " }}</span></p>
+<p>Expires: <span id="claim-exp">{{ .Claims.IDToken.Expires }}</span></p>
+<p>Issue Time: <span id="claim-iat">{{ .Claims.IDToken.IssueTime }}</span></p>
+<p>Requested At: <span id="claim-rat">{{ .Claims.IDToken.RequestedAt }}</span></p>
+<p>Authorize Time: <span id="claim-auth_at">{{ .Claims.IDToken.AuthorizeTime }}</span></p>
+<p>Not Before: <span id="claim-nbf">{{ .Claims.IDToken.NotBefore }}</span></p>
+<p>Issuer: <span id="claim-iss">{{ .Claims.IDToken.Issuer }}</span></p>
+<p>JWT ID: <span id="claim-jti">{{ .Claims.IDToken.JWTIdentifier }}</span></p>
+<p>Subject: <span id="claim-sub">{{ .Claims.IDToken.Subject }}</span></p>
+<p>Nonce: <span id="claim-nonce">{{ .Claims.IDToken.Nonce }}</span></p>
+<p>Name: <span id="claim-name">{{ .Claims.UserInfo.Name }}</span></p>
+<p>Name (ID Token): <span id="claim-id-token-name">{{ .Claims.IDToken.Name }}</span></p>
+<p>Given Name: <span id="claim-given_name">{{ .Claims.UserInfo.GivenName }}</span></p>
+<p>Given Name (ID Token): <span id="claim-id-token-given_name">{{ .Claims.IDToken.GivenName }}</span></p>
+<p>Family Name: <span id="claim-family_name">{{ .Claims.UserInfo.FamilyName }}</span></p>
+<p>Family Name (ID Token): <span id="claim-id-token-family_name">{{ .Claims.IDToken.FamilyName }}</span></p>
+<p>Middle Name: <span id="claim-middle_name">{{ .Claims.UserInfo.MiddleName }}</span></p>
+<p>Middle Name (ID Token): <span id="claim-id-token-middle_name">{{ .Claims.IDToken.MiddleName }}</span></p>
+<p>Nickname: <span id="claim-nickname">{{ .Claims.UserInfo.Nickname }}</span></p>
+<p>Nickname (ID Token): <span id="claim-id-token-nickname">{{ .Claims.IDToken.Nickname }}</span></p>
+<p>Preferred Username: <span id="claim-preferred_username">{{ .Claims.UserInfo.PreferredUsername }}</span></p>
+<p>Preferred Username (ID Token): <span id="claim-id-token-preferred_username">{{ .Claims.IDToken.PreferredUsername }}</span></p>
+<p>Profile: <span id="claim-profile">{{ .Claims.UserInfo.Profile }}</span></p>
+<p>Profile (ID Token): <span id="claim-id-token-profile">{{ .Claims.IDToken.Profile }}</span></p>
+<p>Website: <span id="claim-website">{{ .Claims.UserInfo.Website }}</span></p>
+<p>Website (ID Token): <span id="claim-id-token-website">{{ .Claims.IDToken.Website }}</span></p>
+<p>Gender: <span id="claim-gender">{{ .Claims.UserInfo.Gender }}</span></p>
+<p>Gender (ID Token): <span id="claim-id-token-gender">{{ .Claims.IDToken.Gender }}</span></p>
+<p>Birthdate: <span id="claim-birthdate">{{ .Claims.UserInfo.Birthdate }}</span></p>
+<p>Birthdate (ID Token): <span id="claim-id-token-birthdate">{{ .Claims.IDToken.Birthdate }}</span></p>
+<p>ZoneInfo: <span id="claim-zoneinfo">{{ .Claims.UserInfo.ZoneInfo }}</span></p>
+<p>ZoneInfo (ID Token): <span id="claim-id-token-zoneinfo">{{ .Claims.IDToken.ZoneInfo }}</span></p>
+<p>Locale: <span id="claim-locale">{{ .Claims.UserInfo.Locale }}</span></p>
+<p>Locale (ID Token): <span id="claim-id-token-locale">{{ .Claims.IDToken.Locale }}</span></p>
+<p>Updated At: <span id="claim-updated_at">{{ .Claims.UserInfo.UpdatedAt }}</span></p>
+<p>Updated At (ID Token): <span id="claim-id-token-updated_at">{{ .Claims.IDToken.UpdatedAt }}</span></p>
+<p>Email: <span id="claim-email">{{ .Claims.UserInfo.Email }}</span></p>
+<p>Email (ID Token): <span id="claim-id-token-email">{{ .Claims.IDToken.Email }}</span></p>
+<p>Email Alts: <span id="claim-alt_emails">{{ .Claims.UserInfo.EmailAlts }}</span></p>
+<p>Email Alts (ID Token): <span id="claim-id-token-alt_emails">{{ .Claims.IDToken.EmailAlts }}</span></p>
+<p>Email Verified: <span id="claim-email_verified">{{ .Claims.UserInfo.EmailVerified }}</span></p>
+<p>Email Verified (ID Token): <span id="claim-id-token-email_verified">{{ .Claims.IDToken.EmailVerified }}</span></p>
+<p>Phone Number: <span id="claim-phone_number">{{ .Claims.UserInfo.PhoneNumber }}</span></p>
+<p>Phone Number (ID Token): <span id="claim-id-token-phone_number">{{ .Claims.IDToken.PhoneNumber }}</span></p>
+<p>Phone Number Verified: <span id="claim-phone_number_verified">{{ .Claims.UserInfo.PhoneNumberVerified }}</span></p>
+<p>Phone Number Verified (ID Token): <span id="claim-id-token-phone_number_verified">{{ .Claims.IDToken.PhoneNumberVerified }}</span></p>
+<p>Groups: <span id="claim-groups">{{ stringsJoin .Groups ", " }}</span></p>
+<p>Groups (ID Token): <span id="claim-id-token-groups">{{ stringsJoin .Groups ", " }}</span></p>
+<p>Raw: <span id="raw">{{ .RawToken }}</span></p>
+<p>Authorize Code URL: <span id="auth-code-url">{{ .AuthorizeCodeURL }}</span></p>
+{{- else }}
+<p>Not logged yet...</p> <a id="login-link" href="/login">Log in</a>
+{{- end }}
+
+<!--
+<body style="height: 100vh; position: relative;">
+  <div style="position: absolute; width: 50%; transform: scale(2);
+   transform-origin: 0 0;">
+    <form action="/submit" method="POST" id="sumbit">
+      <input type="text" id="target" name="target" required size="20" />
+    </form>
+  </div>
+</body>
+-->
+
+{{ end }}
+
diff --git a/nix/templates/goapp/frontend/templates/login.html b/nix/templates/goapp/frontend/templates/login.html
new file mode 100644
index 0000000..6e54781
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/login.html
@@ -0,0 +1,41 @@
+{{ define "login" }}
+
+{{ template "head" . }}
+<body>
+  {{ template "nav" . }}
+
+  <span id="login"></span>
+  <h1><a href="#login">Login</a></h1>
+
+  {{ if .err }}{{ .err }}{{ end }}
+  {{ if .logged_in }}
+  Already logged in! <a href="/">Return home</a>
+  {{ else }}
+  <form method="POST" action="/login">
+
+    <table>
+      <tr>
+        <td><label for="username">Name:</label></td>
+        <td><input class="border" type="text" id="username" name="username" autofocus></td>
+      </tr>
+      <tr>
+        <td><label for="password">Password:</label></td>
+        <td><input class="border" type="password" id="password" name="password"></td>
+      </tr>
+      <tr>
+        <td></td>
+        <td><input class="border" type="submit" value="Login"></td>
+      </tr>
+      <tr>
+        <td></td>
+        <td>{{ .res }}</td>
+      </tr>
+    </table>
+  </form>
+  <!-- Not registered yet? <a href="/register">Register Now!</a> -->
+  {{ end }}
+
+  
+</body>
+{{ template "footer" . }}
+{{end}}
diff --git a/nix/templates/goapp/frontend/templates/nav.html b/nix/templates/goapp/frontend/templates/nav.html
new file mode 100644
index 0000000..bf9820f
--- /dev/null
+++ b/nix/templates/goapp/frontend/templates/nav.html
@@ -0,0 +1,41 @@
+{{ define "nav" }}
+  <header>
+    <p style="margin: 1ex 0; display: block; width: 100%; background-color: #ffaa00; color: white;">
+      v0.0.1 EARLY BETA - Data can be deleted at random!
+    </p>
+  </header>
+  <nav>
+
+    <ul>
+      {{ range .Breadcrumbs }}
+        <li>
+          <a class="local" href="{{ .Main.Target }}">{{ .Main.Name }}</a>
+          {{ if .Options }}
+          <ul>
+          {{ range $opt := .Options }}
+            <li><a class="local" href="{{ $opt.Target }}">{{ $opt.Name }}</a></li>
+          {{ end }}
+          </ul>
+          {{ end }}
+        </li>
+      {{ end }}
+    </ul>
+
+  
+    <ul style="float: right">
+        <li>
+            <a href="https://github.com/HanEmile/hefe/tree/main/nix/templates/goapp">src</a>
+        </li>
+    </ul>
+  </nav>
+  <ul>
+    {{ if .NextLinks }}
+    {{ range .NextLinks }}
+      <li><a class="local" href="{{ .Target }}">{{ .Name }}</a></li>
+    {{ end }}
+    {{ end }}
+  </ul>
+	<br>
+{{ end }}
+
+