DEVELOPMENT ENVIRONMENT

~liljamo/tixe

ff544f59bbafbaa4830d3c07dac049e8db6f4310 — Jonni Liljamo 1 year, 29 days ago b20e471
feat: some reworks and the first real interaction

* that interaction being the ability to change your display name
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
}