implement OIDC login flow
This commit is contained in:
		
							parent
							
								
									133d8bcfe9
								
							
						
					
					
						commit
						58e6f35585
					
				
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -32,3 +32,6 @@ test.db.old
 | 
				
			|||||||
.gitignore
 | 
					.gitignore
 | 
				
			||||||
.nvim/session
 | 
					.nvim/session
 | 
				
			||||||
*templ.txt
 | 
					*templ.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# env files
 | 
				
			||||||
 | 
					.env*
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
	"git.32bit.cafe/32bitcafe/guestbook/internal/models"
 | 
						"git.32bit.cafe/32bitcafe/guestbook/internal/models"
 | 
				
			||||||
	"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
 | 
						"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
 | 
				
			||||||
	"git.32bit.cafe/32bitcafe/guestbook/ui/views"
 | 
						"git.32bit.cafe/32bitcafe/guestbook/ui/views"
 | 
				
			||||||
 | 
						"github.com/coreos/go-oidc/v3/oidc"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (app *application) getUserRegister(w http.ResponseWriter, r *http.Request) {
 | 
					func (app *application) getUserRegister(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
@ -92,6 +93,88 @@ func (app *application) postUserLogin(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	http.Redirect(w, r, "/", http.StatusSeeOther)
 | 
						http.Redirect(w, r, "/", http.StatusSeeOther)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) userLoginOIDC(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						state, err := randString(16)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						nonce, err := randString(16)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setCallbackCookie(w, r, "state", state)
 | 
				
			||||||
 | 
						setCallbackCookie(w, r, "nonce", nonce)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						http.Redirect(w, r, app.oauth.config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (app *application) userLoginOIDCCallback(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						state, err := r.Cookie("state")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.clientError(w, http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if r.URL.Query().Get("state") != state.Value {
 | 
				
			||||||
 | 
							app.clientError(w, http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oauth2Token, err := app.oauth.config.Exchange(r.Context(), r.URL.Query().Get("code"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.logger.Error("Failed to exchange token")
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rawIDToken, ok := oauth2Token.Extra("id_token").(string)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							app.serverError(w, r, errors.New("No id_token field in oauth2 token"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						idToken, err := app.oauth.verifier.Verify(r.Context(), rawIDToken)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.logger.Error("Failed to verify ID token")
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						nonce, err := r.Cookie("nonce")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.logger.Error("nonce not found")
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if idToken.Nonce != nonce.Value {
 | 
				
			||||||
 | 
							app.serverError(w, r, errors.New("nonce did not match"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oauth2Token.AccessToken = "*REDACTED*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var t models.UserIdToken
 | 
				
			||||||
 | 
						if err := idToken.Claims(&t); err != nil {
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = app.sessionManager.RenewToken(r.Context())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							app.serverError(w, r, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						id, err := app.users.AuthenticateByOIDC(t.Email, t.Subject)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							id, err = app.users.InsertWithoutPassword(app.createShortId(), t.Username, t.Email, t.Subject, DefaultUserSettings())
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								app.serverError(w, r, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						app.sessionManager.Put(r.Context(), "authenticatedUserId", id)
 | 
				
			||||||
 | 
						http.Redirect(w, r, "/", http.StatusSeeOther)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (app *application) postUserLogout(w http.ResponseWriter, r *http.Request) {
 | 
					func (app *application) postUserLogout(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	err := app.sessionManager.RenewToken(r.Context())
 | 
						err := app.sessionManager.RenewToken(r.Context())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,11 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"math"
 | 
						"math"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
@ -140,3 +143,22 @@ func matchOrigin(origin string, u *url.URL) bool {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return true
 | 
						return true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func randString(nByte int) (string, error) {
 | 
				
			||||||
 | 
						b := make([]byte, nByte)
 | 
				
			||||||
 | 
						if _, err := io.ReadFull(rand.Reader, b); err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return base64.RawURLEncoding.EncodeToString(b), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
 | 
				
			||||||
 | 
						c := &http.Cookie{
 | 
				
			||||||
 | 
							Name:     name,
 | 
				
			||||||
 | 
							Value:    value,
 | 
				
			||||||
 | 
							MaxAge:   int(time.Hour.Seconds()),
 | 
				
			||||||
 | 
							Secure:   r.TLS != nil,
 | 
				
			||||||
 | 
							HttpOnly: true,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						http.SetCookie(w, c)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,12 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"flag"
 | 
						"flag"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"log/slog"
 | 
						"log/slog"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
@ -14,10 +17,21 @@ import (
 | 
				
			|||||||
	"git.32bit.cafe/32bitcafe/guestbook/internal/models"
 | 
						"git.32bit.cafe/32bitcafe/guestbook/internal/models"
 | 
				
			||||||
	"github.com/alexedwards/scs/sqlite3store"
 | 
						"github.com/alexedwards/scs/sqlite3store"
 | 
				
			||||||
	"github.com/alexedwards/scs/v2"
 | 
						"github.com/alexedwards/scs/v2"
 | 
				
			||||||
 | 
						"github.com/coreos/go-oidc/v3/oidc"
 | 
				
			||||||
	"github.com/gorilla/schema"
 | 
						"github.com/gorilla/schema"
 | 
				
			||||||
 | 
						"github.com/joho/godotenv"
 | 
				
			||||||
	_ "github.com/mattn/go-sqlite3"
 | 
						_ "github.com/mattn/go-sqlite3"
 | 
				
			||||||
 | 
						"golang.org/x/oauth2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type applicationOauthConfig struct {
 | 
				
			||||||
 | 
						ctx        context.Context
 | 
				
			||||||
 | 
						config     oauth2.Config
 | 
				
			||||||
 | 
						provider   *oidc.Provider
 | 
				
			||||||
 | 
						oidcConfig *oidc.Config
 | 
				
			||||||
 | 
						verifier   *oidc.IDTokenVerifier
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type application struct {
 | 
					type application struct {
 | 
				
			||||||
	sequence          uint16
 | 
						sequence          uint16
 | 
				
			||||||
	logger            *slog.Logger
 | 
						logger            *slog.Logger
 | 
				
			||||||
@ -26,6 +40,7 @@ type application struct {
 | 
				
			|||||||
	guestbookComments models.GuestbookCommentModelInterface
 | 
						guestbookComments models.GuestbookCommentModelInterface
 | 
				
			||||||
	sessionManager    *scs.SessionManager
 | 
						sessionManager    *scs.SessionManager
 | 
				
			||||||
	formDecoder       *schema.Decoder
 | 
						formDecoder       *schema.Decoder
 | 
				
			||||||
 | 
						oauth             applicationOauthConfig
 | 
				
			||||||
	debug             bool
 | 
						debug             bool
 | 
				
			||||||
	timezones         []string
 | 
						timezones         []string
 | 
				
			||||||
	rootUrl           string
 | 
						rootUrl           string
 | 
				
			||||||
@ -35,10 +50,11 @@ func main() {
 | 
				
			|||||||
	addr := flag.String("addr", ":3000", "HTTP network address")
 | 
						addr := flag.String("addr", ":3000", "HTTP network address")
 | 
				
			||||||
	dsn := flag.String("dsn", "guestbook.db", "data source name")
 | 
						dsn := flag.String("dsn", "guestbook.db", "data source name")
 | 
				
			||||||
	debug := flag.Bool("debug", false, "enable debug mode")
 | 
						debug := flag.Bool("debug", false, "enable debug mode")
 | 
				
			||||||
	root := flag.String("root", "localhost:3000", "root URL of application")
 | 
						root := flag.String("root", "https://localhost:3000", "root URL of application")
 | 
				
			||||||
	flag.Parse()
 | 
						flag.Parse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
 | 
						logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
 | 
				
			||||||
 | 
						godotenv.Load(".env.dev")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	db, err := openDB(*dsn)
 | 
						db, err := openDB(*dsn)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -67,6 +83,13 @@ func main() {
 | 
				
			|||||||
		rootUrl:           *root,
 | 
							rootUrl:           *root,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o, err := setupOauth(app.rootUrl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Error(err.Error())
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						app.oauth = o
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = app.users.InitializeSettingsMap()
 | 
						err = app.users.InitializeSettingsMap()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		logger.Error(err.Error())
 | 
							logger.Error(err.Error())
 | 
				
			||||||
@ -114,6 +137,38 @@ func openDB(dsn string) (*sql.DB, error) {
 | 
				
			|||||||
	return db, nil
 | 
						return db, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setupOauth(rootUrl string) (applicationOauthConfig, error) {
 | 
				
			||||||
 | 
						var c applicationOauthConfig
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							oauth2Provider = os.Getenv("OAUTH2_PROVIDER")
 | 
				
			||||||
 | 
							clientID       = os.Getenv("OAUTH2_CLIENT_ID")
 | 
				
			||||||
 | 
							clientSecret   = os.Getenv("OAUTH2_CLIENT_SECRET")
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if oauth2Provider == "" || clientID == "" || clientSecret == "" {
 | 
				
			||||||
 | 
							return applicationOauthConfig{}, errors.New("OAUTH2_PROVIDER, OAUTH2_CLIENT_ID, and OAUTH2_CLIENT_SECRET must be specified as environment variables.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.ctx = context.Background()
 | 
				
			||||||
 | 
						provider, err := oidc.NewProvider(c.ctx, oauth2Provider)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return applicationOauthConfig{}, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.provider = provider
 | 
				
			||||||
 | 
						c.oidcConfig = &oidc.Config{
 | 
				
			||||||
 | 
							ClientID: clientID,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.verifier = provider.Verifier(c.oidcConfig)
 | 
				
			||||||
 | 
						c.config = oauth2.Config{
 | 
				
			||||||
 | 
							ClientID:     clientID,
 | 
				
			||||||
 | 
							ClientSecret: clientSecret,
 | 
				
			||||||
 | 
							Endpoint:     provider.Endpoint(),
 | 
				
			||||||
 | 
							RedirectURL:  fmt.Sprintf("%s/users/login/oidc/callback", rootUrl),
 | 
				
			||||||
 | 
							Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c, nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getAvailableTimezones() []string {
 | 
					func getAvailableTimezones() []string {
 | 
				
			||||||
	var zones []string
 | 
						var zones []string
 | 
				
			||||||
	var zoneDirs = []string{
 | 
						var zoneDirs = []string{
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,8 @@ func (app *application) routes() http.Handler {
 | 
				
			|||||||
	mux.Handle("POST /users/register", dynamic.ThenFunc(app.postUserRegister))
 | 
						mux.Handle("POST /users/register", dynamic.ThenFunc(app.postUserRegister))
 | 
				
			||||||
	mux.Handle("GET /users/login", dynamic.ThenFunc(app.getUserLogin))
 | 
						mux.Handle("GET /users/login", dynamic.ThenFunc(app.getUserLogin))
 | 
				
			||||||
	mux.Handle("POST /users/login", dynamic.ThenFunc(app.postUserLogin))
 | 
						mux.Handle("POST /users/login", dynamic.ThenFunc(app.postUserLogin))
 | 
				
			||||||
 | 
						mux.Handle("/users/login/oidc", dynamic.ThenFunc(app.userLoginOIDC))
 | 
				
			||||||
 | 
						mux.Handle("/users/login/oidc/callback", dynamic.ThenFunc(app.userLoginOIDCCallback))
 | 
				
			||||||
	mux.Handle("GET /help", dynamic.ThenFunc(app.notImplemented))
 | 
						mux.Handle("GET /help", dynamic.ThenFunc(app.notImplemented))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	protected := dynamic.Append(app.requireAuthentication)
 | 
						protected := dynamic.Append(app.requireAuthentication)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							@ -6,9 +6,14 @@ require (
 | 
				
			|||||||
	github.com/a-h/templ v0.3.833
 | 
						github.com/a-h/templ v0.3.833
 | 
				
			||||||
	github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c
 | 
						github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c
 | 
				
			||||||
	github.com/alexedwards/scs/v2 v2.8.0
 | 
						github.com/alexedwards/scs/v2 v2.8.0
 | 
				
			||||||
 | 
						github.com/coreos/go-oidc/v3 v3.14.1
 | 
				
			||||||
	github.com/gorilla/schema v1.4.1
 | 
						github.com/gorilla/schema v1.4.1
 | 
				
			||||||
 | 
						github.com/joho/godotenv v1.5.1
 | 
				
			||||||
	github.com/justinas/alice v1.2.0
 | 
						github.com/justinas/alice v1.2.0
 | 
				
			||||||
	github.com/justinas/nosurf v1.1.1
 | 
						github.com/justinas/nosurf v1.1.1
 | 
				
			||||||
	github.com/mattn/go-sqlite3 v1.14.24
 | 
						github.com/mattn/go-sqlite3 v1.14.24
 | 
				
			||||||
	golang.org/x/crypto v0.36.0
 | 
						golang.org/x/crypto v0.36.0
 | 
				
			||||||
 | 
						golang.org/x/oauth2 v0.30.0
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require github.com/go-jose/go-jose/v4 v4.0.5 // indirect
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								go.sum
									
									
									
									
									
								
							@ -1,24 +1,35 @@
 | 
				
			|||||||
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
 | 
					github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
 | 
				
			||||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
 | 
					github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
 | 
				
			||||||
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 h1:+DCxWg/ojncqS+TGAuRUoV7OfG/S4doh0pcpAwEcow0=
 | 
					 | 
				
			||||||
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
 | 
					 | 
				
			||||||
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c h1:0gBCIsmH3+aaWK55APhhY7/Z+uv5IdbMqekI97V9shU=
 | 
					github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c h1:0gBCIsmH3+aaWK55APhhY7/Z+uv5IdbMqekI97V9shU=
 | 
				
			||||||
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
 | 
					github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
 | 
				
			||||||
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
 | 
					github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
 | 
				
			||||||
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
 | 
					github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
 | 
				
			||||||
 | 
					github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
 | 
				
			||||||
 | 
					github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
 | 
				
			||||||
 | 
					github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 | 
				
			||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
					github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
				
			||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
					github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
				
			||||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
 | 
					github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
 | 
				
			||||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
 | 
					github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
 | 
				
			||||||
 | 
					github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
				
			||||||
 | 
					github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 | 
				
			||||||
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
 | 
					github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
 | 
				
			||||||
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
 | 
					github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
 | 
				
			||||||
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
 | 
					github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
 | 
				
			||||||
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
 | 
					github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
 | 
					 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 | 
					github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 | 
				
			||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
 | 
					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/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 | 
				
			||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
 | 
					github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
				
			||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
				
			||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 | 
					golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 | 
				
			||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 | 
					golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,15 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *UserModel) InsertWithoutPassword(shortId uint64, username string, email string, password string, settings models.UserSettings) (int64, error) {
 | 
				
			||||||
 | 
						switch email {
 | 
				
			||||||
 | 
						case "dupe@example.com":
 | 
				
			||||||
 | 
							return -1, models.ErrDuplicateEmail
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return 2, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *UserModel) Get(shortId uint64) (models.User, error) {
 | 
					func (m *UserModel) Get(shortId uint64) (models.User, error) {
 | 
				
			||||||
	switch shortId {
 | 
						switch shortId {
 | 
				
			||||||
	case 1:
 | 
						case 1:
 | 
				
			||||||
@ -67,6 +76,13 @@ func (m *UserModel) Authenticate(email, password string) (int64, error) {
 | 
				
			|||||||
	return 0, models.ErrInvalidCredentials
 | 
						return 0, models.ErrInvalidCredentials
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *UserModel) AuthenticateByOIDC(email, subject string) (int64, error) {
 | 
				
			||||||
 | 
						if email == "test@example.com" {
 | 
				
			||||||
 | 
							return 1, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0, models.ErrInvalidCredentials
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *UserModel) Exists(id int64) (bool, error) {
 | 
					func (m *UserModel) Exists(id int64) (bool, error) {
 | 
				
			||||||
	switch id {
 | 
						switch id {
 | 
				
			||||||
	case 1:
 | 
						case 1:
 | 
				
			||||||
 | 
				
			|||||||
@ -35,13 +35,23 @@ type UserModel struct {
 | 
				
			|||||||
	Settings map[string]Setting
 | 
						Settings map[string]Setting
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UserIdToken struct {
 | 
				
			||||||
 | 
						Subject       string   `json:"sub"`
 | 
				
			||||||
 | 
						Email         string   `json:"email"`
 | 
				
			||||||
 | 
						EmailVerified bool     `json:"email_verified"`
 | 
				
			||||||
 | 
						Username      string   `json:"preferred_username"`
 | 
				
			||||||
 | 
						Groups        []string `json:"groups"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserModelInterface interface {
 | 
					type UserModelInterface interface {
 | 
				
			||||||
	InitializeSettingsMap() error
 | 
						InitializeSettingsMap() error
 | 
				
			||||||
	Insert(shortId uint64, username string, email string, password string, settings UserSettings) error
 | 
						Insert(shortId uint64, username string, email string, password string, settings UserSettings) error
 | 
				
			||||||
 | 
						InsertWithoutPassword(shortId uint64, username string, email string, subject string, settings UserSettings) (int64, error)
 | 
				
			||||||
	Get(shortId uint64) (User, error)
 | 
						Get(shortId uint64) (User, error)
 | 
				
			||||||
	GetById(id int64) (User, error)
 | 
						GetById(id int64) (User, error)
 | 
				
			||||||
	GetAll() ([]User, error)
 | 
						GetAll() ([]User, error)
 | 
				
			||||||
	Authenticate(email, password string) (int64, error)
 | 
						Authenticate(email, password string) (int64, error)
 | 
				
			||||||
 | 
						AuthenticateByOIDC(email, subject string) (int64, error)
 | 
				
			||||||
	Exists(id int64) (bool, error)
 | 
						Exists(id int64) (bool, error)
 | 
				
			||||||
	UpdateUserSettings(userId int64, settings UserSettings) error
 | 
						UpdateUserSettings(userId int64, settings UserSettings) error
 | 
				
			||||||
	UpdateSetting(userId int64, setting Setting, value string) error
 | 
						UpdateSetting(userId int64, setting Setting, value string) error
 | 
				
			||||||
@ -126,6 +136,49 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *UserModel) InsertWithoutPassword(shortId uint64, username string, email string, subject string, settings UserSettings) (int64, error) {
 | 
				
			||||||
 | 
						stmt := `INSERT INTO users (ShortId, Username, Email, IsBanned, OIDCSubject, Created)
 | 
				
			||||||
 | 
					    VALUES (?, ?, ?, FALSE, ?, ?)`
 | 
				
			||||||
 | 
						tx, err := m.DB.Begin()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						result, err := tx.Exec(stmt, shortId, username, email, subject, time.Now().UTC())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
								return -1, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if sqliteError, ok := err.(sqlite3.Error); ok {
 | 
				
			||||||
 | 
								if sqliteError.ExtendedCode == 2067 && strings.Contains(sqliteError.Error(), "Email") {
 | 
				
			||||||
 | 
									return -1, ErrDuplicateEmail
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						id, err := result.LastInsertId()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
								return -1, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						stmt = `INSERT INTO user_settings (UserId, SettingId, AllowedSettingValueId, UnconstrainedValue) 
 | 
				
			||||||
 | 
							VALUES (?, ?, ?, ?)`
 | 
				
			||||||
 | 
						_, err = tx.Exec(stmt, id, m.Settings[SettingUserTimezone].id, nil, settings.LocalTimezone.String())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
								return -1, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = tx.Commit()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return id, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *UserModel) Get(shortId uint64) (User, error) {
 | 
					func (m *UserModel) Get(shortId uint64) (User, error) {
 | 
				
			||||||
	stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND Deleted IS NULL`
 | 
						stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND Deleted IS NULL`
 | 
				
			||||||
	tx, err := m.DB.Begin()
 | 
						tx, err := m.DB.Begin()
 | 
				
			||||||
@ -239,6 +292,51 @@ func (m *UserModel) Authenticate(email, password string) (int64, error) {
 | 
				
			|||||||
	return id, nil
 | 
						return id, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *UserModel) AuthenticateByOIDC(email string, subject string) (int64, error) {
 | 
				
			||||||
 | 
						var id int64
 | 
				
			||||||
 | 
						var s sql.NullString
 | 
				
			||||||
 | 
						tx, err := m.DB.Begin()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						stmt := `SELECT Id, OIDCSubject FROM users WHERE Email = ?`
 | 
				
			||||||
 | 
						err = tx.QueryRow(stmt, email, subject).Scan(&id, &s)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, sql.ErrNoRows) {
 | 
				
			||||||
 | 
								if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
									return -1, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return -1, ErrNoRecord
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
									return -1, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return -1, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !s.Valid {
 | 
				
			||||||
 | 
							stmt = `UPDATE users SET OIDCSubject = ? WHERE Id = ?`
 | 
				
			||||||
 | 
							_, err = tx.Exec(stmt, subject, id)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
									return -1, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return -1, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if subject != s.String {
 | 
				
			||||||
 | 
							if rollbackErr := tx.Rollback(); rollbackErr != nil {
 | 
				
			||||||
 | 
								return -1, ErrInvalidCredentials
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = tx.Commit()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return -1, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return id, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *UserModel) Exists(id int64) (bool, error) {
 | 
					func (m *UserModel) Exists(id int64) (bool, error) {
 | 
				
			||||||
	var exists bool
 | 
						var exists bool
 | 
				
			||||||
	stmt := `SELECT EXISTS(SELECT true FROM users WHERE Id = ? AND DELETED IS NULL)`
 | 
						stmt := `SELECT EXISTS(SELECT true FROM users WHERE Id = ? AND DELETED IS NULL)`
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								migrations/000006_change_password_field.down.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								migrations/000006_change_password_field.down.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					ALTER TABLE users RENAME COLUMN HashedPassword TO HashedPasswordOld;
 | 
				
			||||||
 | 
					ALTER TABLE users ADD COLUMN HashedPassword char(60) NOT NULL DEFAULT '0000';
 | 
				
			||||||
 | 
					UPDATE users SET HashedPassword=HashedPasswordOld;
 | 
				
			||||||
 | 
					ALTER TABLE users DROP COLUMN HashedPasswordOld;
 | 
				
			||||||
							
								
								
									
										4
									
								
								migrations/000006_change_password_field.up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								migrations/000006_change_password_field.up.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					ALTER TABLE users RENAME COLUMN HashedPassword TO HashedPasswordOld;
 | 
				
			||||||
 | 
					ALTER TABLE users ADD COLUMN HashedPassword char(60) NULL;
 | 
				
			||||||
 | 
					UPDATE users SET HashedPassword=HashedPasswordOld;
 | 
				
			||||||
 | 
					ALTER TABLE users DROP COLUMN HashedPasswordOld;
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/000007_add_oidc_subject.down.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/000007_add_oidc_subject.down.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE users DROP COLUMN OIDCSubject;
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/000007_add_oidc_subject.up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								migrations/000007_add_oidc_subject.up.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE users ADD COLUMN OIDCSubject varchar(255);
 | 
				
			||||||
@ -12,6 +12,7 @@ type CommonData struct {
 | 
				
			|||||||
	CSRFToken       string
 | 
						CSRFToken       string
 | 
				
			||||||
	CurrentUser     *models.User
 | 
						CurrentUser     *models.User
 | 
				
			||||||
	IsHtmx          bool
 | 
						IsHtmx          bool
 | 
				
			||||||
 | 
						RootUrl         string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func shortIdToSlug(shortId uint64) string {
 | 
					func shortIdToSlug(shortId uint64) string {
 | 
				
			||||||
 | 
				
			|||||||
@ -102,7 +102,7 @@ func topNav(data CommonData) templ.Component {
 | 
				
			|||||||
			var templ_7745c5c3_Var3 string
 | 
								var templ_7745c5c3_Var3 string
 | 
				
			||||||
			templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username)
 | 
								templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 44, Col: 40}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 45, Col: 40}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -121,7 +121,7 @@ func topNav(data CommonData) templ.Component {
 | 
				
			|||||||
			var templ_7745c5c3_Var4 string
 | 
								var templ_7745c5c3_Var4 string
 | 
				
			||||||
			templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders)
 | 
								templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 52, Col: 62}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 53, Col: 62}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -202,7 +202,7 @@ func base(title string, data CommonData) templ.Component {
 | 
				
			|||||||
		var templ_7745c5c3_Var7 string
 | 
							var templ_7745c5c3_Var7 string
 | 
				
			||||||
		templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
 | 
							templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
 | 
				
			||||||
		if templ_7745c5c3_Err != nil {
 | 
							if templ_7745c5c3_Err != nil {
 | 
				
			||||||
			return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 71, Col: 17}
 | 
								return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 72, Col: 17}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
 | 
							_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
 | 
				
			||||||
		if templ_7745c5c3_Err != nil {
 | 
							if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -232,7 +232,7 @@ func base(title string, data CommonData) templ.Component {
 | 
				
			|||||||
			var templ_7745c5c3_Var8 string
 | 
								var templ_7745c5c3_Var8 string
 | 
				
			||||||
			templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash)
 | 
								templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 83, Col: 36}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/common.templ`, Line: 84, Col: 36}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,8 @@ templ UserLogin(title string, data CommonData, form forms.UserLoginForm) {
 | 
				
			|||||||
				<input type="password" name="password"/>
 | 
									<input type="password" name="password"/>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<input type="submit" value="login"/>
 | 
									<input type="submit" value="Login"/>
 | 
				
			||||||
 | 
									<a href="/users/login/oidc">Login with SSO</a>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</form>
 | 
							</form>
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -143,7 +143,7 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
 | 
				
			|||||||
					return templ_7745c5c3_Err
 | 
										return templ_7745c5c3_Err
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"password\" name=\"password\"></div><div><input type=\"submit\" value=\"login\"></div></form>")
 | 
								templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<input type=\"password\" name=\"password\"></div><div><input type=\"submit\" value=\"Login\"> <a href=\"/users/login/oidc\">Login with SSO</a></div></form>")
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ_7745c5c3_Err
 | 
									return templ_7745c5c3_Err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@ -197,7 +197,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
 | 
				
			|||||||
			var templ_7745c5c3_Var10 string
 | 
								var templ_7745c5c3_Var10 string
 | 
				
			||||||
			templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
 | 
								templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 43, Col: 64}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 44, Col: 64}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -220,7 +220,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
 | 
				
			|||||||
				var templ_7745c5c3_Var11 string
 | 
									var templ_7745c5c3_Var11 string
 | 
				
			||||||
				templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 | 
									templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 | 
				
			||||||
				if templ_7745c5c3_Err != nil {
 | 
									if templ_7745c5c3_Err != nil {
 | 
				
			||||||
					return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 48, Col: 33}
 | 
										return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 49, Col: 33}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
 | 
									_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
 | 
				
			||||||
				if templ_7745c5c3_Err != nil {
 | 
									if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -238,7 +238,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
 | 
				
			|||||||
			var templ_7745c5c3_Var12 string
 | 
								var templ_7745c5c3_Var12 string
 | 
				
			||||||
			templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
 | 
								templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 50, Col: 70}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 51, Col: 70}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -261,7 +261,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
 | 
				
			|||||||
				var templ_7745c5c3_Var13 string
 | 
									var templ_7745c5c3_Var13 string
 | 
				
			||||||
				templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 | 
									templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 | 
				
			||||||
				if templ_7745c5c3_Err != nil {
 | 
									if templ_7745c5c3_Err != nil {
 | 
				
			||||||
					return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 56, Col: 33}
 | 
										return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 57, Col: 33}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
 | 
									_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
 | 
				
			||||||
				if templ_7745c5c3_Err != nil {
 | 
									if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -279,7 +279,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
 | 
				
			|||||||
			var templ_7745c5c3_Var14 string
 | 
								var templ_7745c5c3_Var14 string
 | 
				
			||||||
			templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
 | 
								templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 58, Col: 65}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 59, Col: 65}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -302,7 +302,7 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
 | 
				
			|||||||
				var templ_7745c5c3_Var15 string
 | 
									var templ_7745c5c3_Var15 string
 | 
				
			||||||
				templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 | 
									templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(error)
 | 
				
			||||||
				if templ_7745c5c3_Err != nil {
 | 
									if templ_7745c5c3_Err != nil {
 | 
				
			||||||
					return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 64, Col: 33}
 | 
										return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 65, Col: 33}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
 | 
									_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
 | 
				
			||||||
				if templ_7745c5c3_Err != nil {
 | 
									if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -367,7 +367,7 @@ func UserProfile(title string, data CommonData, user models.User) templ.Componen
 | 
				
			|||||||
			var templ_7745c5c3_Var18 string
 | 
								var templ_7745c5c3_Var18 string
 | 
				
			||||||
			templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
 | 
								templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 77, Col: 21}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 78, Col: 21}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -380,7 +380,7 @@ func UserProfile(title string, data CommonData, user models.User) templ.Componen
 | 
				
			|||||||
			var templ_7745c5c3_Var19 string
 | 
								var templ_7745c5c3_Var19 string
 | 
				
			||||||
			templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
 | 
								templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 78, Col: 17}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 79, Col: 17}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -441,7 +441,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
 | 
				
			|||||||
			var templ_7745c5c3_Var22 string
 | 
								var templ_7745c5c3_Var22 string
 | 
				
			||||||
			templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
 | 
								templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
				return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 88, Col: 65}
 | 
									return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 89, Col: 65}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
 | 
								_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
 | 
				
			||||||
			if templ_7745c5c3_Err != nil {
 | 
								if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -460,7 +460,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
 | 
				
			|||||||
					var templ_7745c5c3_Var23 string
 | 
										var templ_7745c5c3_Var23 string
 | 
				
			||||||
					templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
										templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 93, Col: 25}
 | 
											return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 94, Col: 25}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
 | 
										_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -473,7 +473,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
 | 
				
			|||||||
					var templ_7745c5c3_Var24 string
 | 
										var templ_7745c5c3_Var24 string
 | 
				
			||||||
					templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
										templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 93, Col: 48}
 | 
											return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 94, Col: 48}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
 | 
										_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -491,7 +491,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
 | 
				
			|||||||
					var templ_7745c5c3_Var25 string
 | 
										var templ_7745c5c3_Var25 string
 | 
				
			||||||
					templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
										templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 25}
 | 
											return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 96, Col: 25}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
 | 
										_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
@ -504,7 +504,7 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
 | 
				
			|||||||
					var templ_7745c5c3_Var26 string
 | 
										var templ_7745c5c3_Var26 string
 | 
				
			||||||
					templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
										templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
						return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 95, Col: 32}
 | 
											return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/views/users.templ`, Line: 96, Col: 32}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
 | 
										_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
 | 
				
			||||||
					if templ_7745c5c3_Err != nil {
 | 
										if templ_7745c5c3_Err != nil {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user