Compare commits

..

3 Commits
dev ... sso

Author SHA1 Message Date
db1d4e1ad2 env file config 2025-06-29 20:16:37 -07:00
c56a445c6a add app configuration 2025-06-29 18:12:32 -07:00
58e6f35585 implement OIDC login flow 2025-06-29 17:19:35 -07:00
17 changed files with 476 additions and 87 deletions

3
.gitignore vendored
View File

@ -32,3 +32,6 @@ test.db.old
.gitignore
.nvim/session
*templ.txt
# env files
.env*

View File

@ -9,15 +9,22 @@ import (
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"git.32bit.cafe/32bitcafe/guestbook/internal/validator"
"git.32bit.cafe/32bitcafe/guestbook/ui/views"
"github.com/coreos/go-oidc/v3/oidc"
)
func (app *application) getUserRegister(w http.ResponseWriter, r *http.Request) {
if !app.config.localAuthEnabled {
http.Redirect(w, r, "/users/login/oidc", http.StatusFound)
}
form := forms.UserRegistrationForm{}
data := app.newCommonData(r)
views.UserRegistration("User Registration", data, form).Render(r.Context(), w)
}
func (app *application) getUserLogin(w http.ResponseWriter, r *http.Request) {
if !app.config.localAuthEnabled {
http.Redirect(w, r, "/users/login/oidc", http.StatusFound)
}
views.UserLogin("Login", app.newCommonData(r), forms.UserLoginForm{}).Render(r.Context(), w)
}
@ -92,6 +99,91 @@ func (app *application) postUserLogin(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (app *application) userLoginOIDC(w http.ResponseWriter, r *http.Request) {
if !app.config.oauthEnabled {
http.Redirect(w, r, "/users/login", http.StatusFound)
}
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.config.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.config.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.config.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) {
err := app.sessionManager.RenewToken(r.Context())
if err != nil {

View File

@ -1,8 +1,11 @@
package main
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/url"
@ -104,13 +107,15 @@ func (app *application) getCurrentUser(r *http.Request) *models.User {
func (app *application) newCommonData(r *http.Request) views.CommonData {
return views.CommonData{
CurrentYear: time.Now().Year(),
Flash: app.sessionManager.PopString(r.Context(), "flash"),
IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r),
CurrentUser: app.getCurrentUser(r),
IsHtmx: r.Header.Get("Hx-Request") == "true",
RootUrl: app.rootUrl,
CurrentYear: time.Now().Year(),
Flash: app.sessionManager.PopString(r.Context(), "flash"),
IsAuthenticated: app.isAuthenticated(r),
CSRFToken: nosurf.Token(r),
CurrentUser: app.getCurrentUser(r),
IsHtmx: r.Header.Get("Hx-Request") == "true",
RootUrl: app.config.rootUrl,
LocalAuthEnabled: app.config.localAuthEnabled,
OIDCEnabled: app.config.oauthEnabled,
}
}
@ -140,3 +145,22 @@ func matchOrigin(origin string, u *url.URL) bool {
}
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)
}

View File

@ -1,12 +1,17 @@
package main
import (
"context"
"crypto/tls"
"database/sql"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"unicode"
@ -14,10 +19,28 @@ import (
"git.32bit.cafe/32bitcafe/guestbook/internal/models"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/schema"
"github.com/joho/godotenv"
_ "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 applicationConfig struct {
oauthEnabled bool
localAuthEnabled bool
oauth applicationOauthConfig
rootUrl string
}
type application struct {
sequence uint16
logger *slog.Logger
@ -26,19 +49,29 @@ type application struct {
guestbookComments models.GuestbookCommentModelInterface
sessionManager *scs.SessionManager
formDecoder *schema.Decoder
config applicationConfig
debug bool
timezones []string
rootUrl string
}
func main() {
addr := flag.String("addr", ":3000", "HTTP network address")
dsn := flag.String("dsn", "guestbook.db", "data source name")
debug := flag.Bool("debug", false, "enable debug mode")
root := flag.String("root", "localhost:3000", "root URL of application")
env := flag.String("env", ".env", ".env file path")
flag.Parse()
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
err := godotenv.Load(*env)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
cfg, err := setupConfig(*addr)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
db, err := openDB(*dsn)
if err != nil {
@ -62,9 +95,9 @@ func main() {
users: &models.UserModel{DB: db, Settings: make(map[string]models.Setting)},
guestbookComments: &models.GuestbookCommentModel{DB: db},
formDecoder: formDecoder,
config: cfg,
debug: *debug,
timezones: getAvailableTimezones(),
rootUrl: *root,
}
err = app.users.InitializeSettingsMap()
@ -114,6 +147,75 @@ func openDB(dsn string) (*sql.DB, error) {
return db, nil
}
func setupConfig(addr string) (applicationConfig, error) {
var c applicationConfig
var (
rootUrl = os.Getenv("ROOT_URL")
oidcEnabled = os.Getenv("ENABLE_OIDC")
localLoginEnabled = os.Getenv("ENABLE_LOCAL_LOGIN")
oauth2Provider = os.Getenv("OAUTH2_PROVIDER")
clientID = os.Getenv("OAUTH2_CLIENT_ID")
clientSecret = os.Getenv("OAUTH2_CLIENT_SECRET")
)
if rootUrl != "" {
c.rootUrl = rootUrl
} else {
u, err := url.Parse(fmt.Sprintf("https://localhost%s", addr))
if err != nil {
return c, err
}
c.rootUrl = u.String()
}
oauthEnabled, err := strconv.ParseBool(oidcEnabled)
if err != nil {
c.oauthEnabled = false
}
c.oauthEnabled = oauthEnabled
localAuthEnabled, err := strconv.ParseBool(localLoginEnabled)
if err != nil {
c.localAuthEnabled = true
}
c.localAuthEnabled = localAuthEnabled
if !c.oauthEnabled && !c.localAuthEnabled {
return c, errors.New("Either ENABLE_OIDC or ENABLE_LOCAL_LOGIN must be set to true")
}
// if OIDC is disabled, no more configuration needs to be read
if !oauthEnabled {
return c, nil
}
var o applicationOauthConfig
if oauth2Provider == "" || clientID == "" || clientSecret == "" {
return c, errors.New("OAUTH2_PROVIDER, OAUTH2_CLIENT_ID, and OAUTH2_CLIENT_SECRET must be specified as environment variables.")
}
o.ctx = context.Background()
provider, err := oidc.NewProvider(o.ctx, oauth2Provider)
if err != nil {
return c, err
}
o.provider = provider
o.oidcConfig = &oidc.Config{
ClientID: clientID,
}
o.verifier = provider.Verifier(o.oidcConfig)
o.config = oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: provider.Endpoint(),
RedirectURL: fmt.Sprintf("%s/users/login/oidc/callback", c.rootUrl),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
c.oauth = o
return c, nil
}
func getAvailableTimezones() []string {
var zones []string
var zoneDirs = []string{

View File

@ -27,6 +27,8 @@ func (app *application) routes() http.Handler {
mux.Handle("POST /users/register", dynamic.ThenFunc(app.postUserRegister))
mux.Handle("GET /users/login", dynamic.ThenFunc(app.getUserLogin))
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))
protected := dynamic.Append(app.requireAuthentication)

5
go.mod
View File

@ -6,9 +6,14 @@ require (
github.com/a-h/templ v0.3.833
github.com/alexedwards/scs/sqlite3store v0.0.0-20250212122300-421ef1d8611c
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/joho/godotenv v1.5.1
github.com/justinas/alice v1.2.0
github.com/justinas/nosurf v1.1.1
github.com/mattn/go-sqlite3 v1.14.24
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
View File

@ -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/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/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
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/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
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/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
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.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/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=

View File

@ -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) {
switch shortId {
case 1:
@ -67,6 +76,13 @@ func (m *UserModel) Authenticate(email, password string) (int64, error) {
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) {
switch id {
case 1:

View File

@ -35,13 +35,23 @@ type UserModel struct {
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 {
InitializeSettingsMap() 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)
GetById(id int64) (User, error)
GetAll() ([]User, error)
Authenticate(email, password string) (int64, error)
AuthenticateByOIDC(email, subject string) (int64, error)
Exists(id int64) (bool, error)
UpdateUserSettings(userId int64, settings UserSettings) 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
}
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) {
stmt := `SELECT Id, ShortId, Username, Email, Created FROM users WHERE ShortId = ? AND Deleted IS NULL`
tx, err := m.DB.Begin()
@ -239,6 +292,51 @@ func (m *UserModel) Authenticate(email, password string) (int64, error) {
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) {
var exists bool
stmt := `SELECT EXISTS(SELECT true FROM users WHERE Id = ? AND DELETED IS NULL)`

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

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

View File

@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN OIDCSubject;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN OIDCSubject varchar(255);

View File

@ -6,12 +6,15 @@ import "fmt"
import "strings"
type CommonData struct {
CurrentYear int
Flash string
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
IsHtmx bool
CurrentYear int
Flash string
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
IsHtmx bool
RootUrl string
LocalAuthEnabled bool
OIDCEnabled bool
}
func shortIdToSlug(shortId uint64) string {
@ -51,7 +54,9 @@ templ topNav(data CommonData) {
<a href="/users/settings">Settings</a> |
<a href="#" hx-post="/users/logout" hx-headers={ hxHeaders }>Logout</a>
} else {
<a href="/users/register">Create an Account</a> |
if data.LocalAuthEnabled {
<a href="/users/register">Create an Account</a> |
}
<a href="/users/login">Login</a>
}
</div>

View File

@ -14,13 +14,15 @@ import "fmt"
import "strings"
type CommonData struct {
CurrentYear int
Flash string
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
IsHtmx bool
RootUrl string
CurrentYear int
Flash string
IsAuthenticated bool
CSRFToken string
CurrentUser *models.User
IsHtmx bool
RootUrl string
LocalAuthEnabled bool
OIDCEnabled bool
}
func shortIdToSlug(shortId uint64) string {
@ -102,7 +104,7 @@ func topNav(data CommonData) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentUser.Username)
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: 47, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -121,7 +123,7 @@ func topNav(data CommonData) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(hxHeaders)
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: 55, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@ -132,12 +134,18 @@ func topNav(data CommonData) templ.Component {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"/users/register\">Create an Account</a> | <a href=\"/users/login\">Login</a>")
if data.LocalAuthEnabled {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"/users/register\">Create an Account</a> | ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <a href=\"/users/login\">Login</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></nav>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -166,7 +174,7 @@ func commonFooter() templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<footer><p>A <a href=\"https://32bit.cafe\">32bit.cafe</a> Project</p></footer>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<footer><p>A <a href=\"https://32bit.cafe\">32bit.cafe</a> Project</p></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -195,20 +203,20 @@ func base(title string, data CommonData) templ.Component {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<!doctype html><html lang=\"en\"><head><title>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<!doctype html><html lang=\"en\"><head><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
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: 76, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/classless.min.css\" rel=\"stylesheet\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " - webweav.ing</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/classless.min.css\" rel=\"stylesheet\"><link href=\"/static/css/style.css\" rel=\"stylesheet\"><script src=\"/static/js/htmx.min.js\"></script></head><body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -220,25 +228,25 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<main>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Flash != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"flash\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"flash\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.Flash)
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: 88, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -247,7 +255,7 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</main>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -255,7 +263,7 @@ func base(title string, data CommonData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -30,7 +30,10 @@ templ UserLogin(title string, data CommonData, form forms.UserLoginForm) {
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="login"/>
<input type="submit" value="Login"/>
if data.OIDCEnabled {
<a href="/users/login/oidc">Login with SSO</a>
}
</div>
</form>
}

View File

@ -143,7 +143,17 @@ func UserLogin(title string, data CommonData, form forms.UserLoginForm) templ.Co
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\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.OIDCEnabled {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<a href=\"/users/login/oidc\">Login with SSO</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -190,130 +200,130 @@ func UserRegistration(title string, data CommonData, form forms.UserRegistration
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h1>User Registration</h1><form action=\"/users/register\" method=\"post\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<h1>User Registration</h1><form action=\"/users/register\" method=\"post\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
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: 46, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"><div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists := form.FieldErrors["name"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<label for=\"username\">Username: </label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<label for=\"username\">Username: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<label class=\"error\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(error)
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: 51, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<input type=\"text\" name=\"username\" id=\"username\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<input type=\"text\" name=\"username\" id=\"username\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(form.Name)
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: 53, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" required></div><div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["email"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<label for=\"email\">Email: </label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<label for=\"email\">Email: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<label class=\"error\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(error)
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: 59, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<input type=\"text\" name=\"email\" id=\"email\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<input type=\"text\" name=\"email\" id=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(form.Email)
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: 61, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" required></div><div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" required></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
error, exists = form.FieldErrors["password"]
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<label for=\"password\">Password: </label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<label for=\"password\">Password: </label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if exists {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<label class=\"error\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<label class=\"error\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(error)
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: 67, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<input type=\"password\" name=\"password\" id=\"password\"></div><div><input type=\"submit\" value=\"Register\"></div></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<input type=\"password\" name=\"password\" id=\"password\"></div><div><input type=\"submit\" value=\"Register\"></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -360,33 +370,33 @@ func UserProfile(title string, data CommonData, user models.User) templ.Componen
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<h1>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
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: 80, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</h1><p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</h1><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
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: 81, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -434,89 +444,89 @@ func UserSettingsView(data CommonData, timezones []string) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div><h1>User Settings</h1><form hx-put=\"/users/settings\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div><h1>User Settings</h1><form hx-put=\"/users/settings\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.CSRFToken)
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: 91, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"> <label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"> <label>Local Timezone</label> <select name=\"timezones\" id=\"timezone-select\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, tz := range timezones {
if tz == user.Settings.LocalTimezone.String() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<option value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
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: 96, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" selected=\"true\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" selected=\"true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
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: 96, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</option>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<option value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
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: 98, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tz)
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: 98, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</option>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</select> <input type=\"submit\" value=\"Submit\"></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</select> <input type=\"submit\" value=\"Submit\"></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}