A api/user_settings.go => api/user_settings.go +46 -0
@@ 0,0 1,46 @@
+package api
+
+import (
+ "context"
+ "log"
+ "net/http"
+ "tixe/db"
+ "tixe/types"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+)
+
+type postDisplayName struct {
+ DisplayName string `form:"display_name"`
+}
+
+func UserUpdateDisplayName(c *gin.Context) {
+ data := &postDisplayName{}
+ if err := c.Bind(data); err != nil {
+ log.Printf("[tixe/api] ERROR: Could not bind display name update data: %v", err)
+ c.String(http.StatusBadRequest, "Could not bind display name update data")
+ return;
+ }
+
+ session := sessions.Default(c)
+ user := session.Get("user").(types.User)
+
+ _, err := db.PgPool.Exec(context.Background(),
+ "UPDATE users SET display_name = $1 WHERE id = $2", data.DisplayName, user.Id)
+ if err != nil {
+ log.Printf("[tixe/api] ERROR: Could not update display name in database: %v", err)
+ c.String(http.StatusInternalServerError, "Could not update display name in database")
+ return;
+ }
+
+ // Update session data
+ session.Set("user", types.User { Id: user.Id, DisplayName: data.DisplayName })
+ if err := session.Save(); err != nil {
+ log.Printf("[tixe/auth] ERROR: Failed to save session: %v", err)
+ c.String(http.StatusInternalServerError, "Failed to save session!")
+ return
+ }
+
+ c.Redirect(http.StatusFound, "/settings")
+}
M handlers/auth.go => handlers/auth.go +8 -6
@@ 6,6 6,7 @@ import (
"net/http"
"tixe/auth"
"tixe/db"
+ "tixe/types"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ 41,18 42,19 @@ func AuthCallback(auth *auth.Auth) gin.HandlerFunc {
}
// Try to get the relevant details of the user
- var userId string
+ var userId, userDisplayName string
// idToken.Subject should be unique and should not change.
// I think. Maybe. Possibly. Hopefully.
scanErr := db.PgPool.QueryRow(context.Background(),
- "SELECT id FROM users WHERE oidc_subject = $1", idToken.Subject).Scan(&userId)
+ "SELECT id, display_name FROM users WHERE oidc_subject = $1", idToken.Subject).Scan(&userId, &userDisplayName)
if scanErr == pgx.ErrNoRows {
log.Printf("[tixe/auth] New user detected logging in, creating entry in database")
// The user does not exist in the db, create it
userId = ulid.Make().String()
+ userDisplayName = profile["name"].(string)
_, err = db.PgPool.Exec(context.Background(),
"INSERT INTO users(id, display_name, oidc_subject) VALUES($1, $2, $3)",
- userId, profile["name"].(string), idToken.Subject)
+ userId, userDisplayName, idToken.Subject)
if err != nil {
log.Printf("[tixe/auth] ERROR: Could not create database entry for oidc user")
c.String(http.StatusInternalServerError, "Could not create database entry for oidc user")
@@ 65,12 67,12 @@ func AuthCallback(auth *auth.Auth) gin.HandlerFunc {
return
}
- // The user_id field is read in other requests to read user data from the db
- session.Set("user_id", userId)
+ // The user.Id field is read in other requests to read user data from the db
+ session.Set("user", types.User { Id: userId, DisplayName: userDisplayName })
session.Set("access_token", token.AccessToken)
session.Set("profile", profile)
if err := session.Save(); err != nil {
- log.Printf("[tixe/auth] ERROR: Failed to save session")
+ log.Printf("[tixe/auth] ERROR: Failed to save session: %v", err)
c.String(http.StatusInternalServerError, "Failed to save session!")
return
}
A handlers/settings.go => handlers/settings.go +29 -0
@@ 0,0 1,29 @@
+package handlers
+
+import (
+ "net/http"
+ "tixe/template"
+ "tixe/types"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+)
+
+type SettingsData struct {
+}
+
+func Settings(c *gin.Context) {
+ session := sessions.Default(c)
+ user := session.Get("user").(types.User)
+
+ // This now comes from the session data, but kept as reference for other things
+ //var displayName string
+ //_ = db.PgPool.QueryRow(context.Background(),
+ // "SELECT display_name FROM users WHERE id = $1", user.Id).Scan(&displayName)
+
+ settingsData := SettingsData {
+ }
+
+ html := template.TmplEngine.Render("settings.tmpl", map[string]interface{}{"user": user, "data": settingsData})
+ c.Data(http.StatusOK, "text/html", html)
+}
M static/styles.css => static/styles.css +24 -0
@@ 567,6 567,11 @@ video {
width: max-content;
}
+.w-fit {
+ width: -moz-fit-content;
+ width: fit-content;
+}
+
.min-w-max {
min-width: -moz-max-content;
min-width: max-content;
@@ 661,6 666,10 @@ video {
padding: 1rem;
}
+.p-1 {
+ padding: 0.25rem;
+}
+
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
@@ 689,6 698,16 @@ video {
color: transparent;
}
+.placeholder-slate-800::-moz-placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(30 41 59 / var(--tw-placeholder-opacity));
+}
+
+.placeholder-slate-800::placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(30 41 59 / var(--tw-placeholder-opacity));
+}
+
.opacity-0 {
opacity: 0;
}
@@ 727,6 746,11 @@ video {
text-decoration-line: underline;
}
+.focus\:outline-none:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
.group:hover .group-hover\:visible {
visibility: visible;
}
M template/templates/common/base.tmpl => template/templates/common/base.tmpl +2 -2
@@ 26,10 26,10 @@
<div class="flex flex-col min-w-max">
{{ if eq .notauthed true }}
Not logged in
- {{ else if ne .profile nil }}
+ {{ else if ne .user nil }}
<div class="group">
<div class="group-hover:animate-wiggle">
- <a class="font-bold animate-starshoot1 hover:underline" href="/settings">{{ .profile.name }}</a>
+ <a class="font-bold animate-starshoot1 hover:underline" href="/settings">{{ .user.DisplayName }}</a>
</div>
<div class="absolute group-hover:animate-starmove">
<svg class="z-50 invisible opacity-0 h-4 w-4 fill-yellow-200 stroke-yellow-400 group-hover:visible group-hover:animate-starspin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
A template/templates/settings.tmpl => template/templates/settings.tmpl +11 -0
@@ 0,0 1,11 @@
+{{ define "content" }}
+<form class="flex flex-col" action="/api/settings/user/display_name" method="POST">
+ <p class="text-sm" for="display_name">Display Name</p>
+ <div class="flex gap-2">
+ <input class="w-full p-1 border-2 rounded-md placeholder-slate-800 focus:outline-none" type="text" placeholder="{{ .user.DisplayName }}" name="display_name" id="display_name"/>
+ <button class="w-fit p-1 border-2 rounded-md drop-shadow-md bg-slate-50 hover:bg-slate-200 transition-colors" type="submit">
+ Update
+ </button>
+ </div>
+</form>
+{{ end }}
M tixe.go => tixe.go +11 -4
@@ 11,6 11,7 @@ import (
"tixe/handlers"
"tixe/middlewares"
"tixe/template"
+ "tixe/types"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
@@ 23,17 24,17 @@ func ping(c *gin.Context) {
func root(c *gin.Context) {
session := sessions.Default(c)
- profile := session.Get("profile")
+ user := session.Get("user")
- html := template.TmplEngine.Render("index.tmpl", map[string]interface{}{"title": "tixë", "profile": profile})
+ html := template.TmplEngine.Render("index.tmpl", map[string]interface{}{"title": "tixë", "user": user})
c.Data(http.StatusOK, "text/html", html)
}
func handleNoRoute(c *gin.Context) {
session := sessions.Default(c)
- profile := session.Get("profile")
+ user := session.Get("user")
- html := template.TmplEngine.Render("404.tmpl", map[string]interface{}{"profile": profile})
+ html := template.TmplEngine.Render("404.tmpl", map[string]interface{}{"user": user})
c.Data(http.StatusNotFound, "text/html", html)
}
@@ 41,7 42,10 @@ func setupRouter(auth *auth.Auth) *gin.Engine {
r := gin.Default()
r.Static("/static", "./static")
+ // Register types that will be saved in sessions
gob.Register(map[string]interface{}{})
+ gob.Register(types.User{})
+
store := cookie.NewStore([]byte(config.TixeConfig.CookieSecret))
r.Use(sessions.Sessions("auth-session", store))
@@ 60,10 64,13 @@ func setupRouter(auth *auth.Auth) *gin.Engine {
{
reqAuth.GET("/", root)
+ reqAuth.GET("/settings", handlers.Settings)
+
apiRoute := reqAuth.Group("/api")
{
apiRoute.GET("/", api.Root)
apiRoute.GET("/ping", ping)
+ apiRoute.POST("/settings/user/display_name", api.UserUpdateDisplayName)
}
}
A types/user.go => types/user.go +6 -0
@@ 0,0 1,6 @@
+package types
+
+type User struct {
+ Id string
+ DisplayName string
+}