Implement OIDC single sign-on #27
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -31,4 +31,7 @@ tls/ | |||||||
| test.db.old | 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