#73 begin implement keycloak
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s

This commit is contained in:
Tim
2024-08-25 21:52:35 +02:00
parent 886214aad0
commit f826718c03
13 changed files with 223 additions and 79 deletions

2
go.mod
View File

@@ -3,7 +3,9 @@ module me-fit
go 1.22.5 go 1.22.5
require ( require (
github.com/NYTimes/gziphandler v1.1.1
github.com/a-h/templ v0.2.771 github.com/a-h/templ v0.2.771
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-migrate/migrate/v4 v4.17.1 github.com/golang-migrate/migrate/v4 v4.17.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22

7
go.sum
View File

@@ -1,11 +1,16 @@
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/a-h/templ v0.2.771 h1:4KH5ykNigYGGpCe0fRJ7/hzwz72k3qFqIiiLLJskbSo= github.com/a-h/templ v0.2.771 h1:4KH5ykNigYGGpCe0fRJ7/hzwz72k3qFqIiiLLJskbSo=
github.com/a-h/templ v0.2.771/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= github.com/a-h/templ v0.2.771/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -37,6 +42,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=

View File

@@ -16,10 +16,10 @@ func getHandler(db *sql.DB) http.Handler {
// Serve static files (CSS, JS and images) // Serve static files (CSS, JS and images)
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
router.HandleFunc("/app", service.App) router.HandleFunc("/app", service.WorkoutIndex)
router.HandleFunc("POST /api/workout", service.NewWorkout(db)) router.HandleFunc("POST /api/workout", service.NewWorkout(db))
router.HandleFunc("GET /api/workout", service.GetWorkouts(db)) // router.HandleFunc("GET /api/workout", service.GetWorkouts(db))
router.HandleFunc("DELETE /api/workout", service.DeleteWorkout(db)) // router.HandleFunc("DELETE /api/workout", service.DeleteWorkout(db))
return middleware.Logging(middleware.EnableCors(router)) return middleware.Logging(middleware.Gzip(middleware.EnableCors(router)))
} }

View File

@@ -20,6 +20,8 @@ func main() {
log.Fatal("Error loading .env file") log.Fatal("Error loading .env file")
} }
utils.InitializeAuth()
db, err := sql.Open("sqlite3", "./data.db") db, err := sql.Open("sqlite3", "./data.db")
if err != nil { if err != nil {
log.Fatal("Could not open Database data.db: ", err) log.Fatal("Could not open Database data.db: ", err)

11
middleware/gzip.go Normal file
View File

@@ -0,0 +1,11 @@
package middleware
import (
"net/http"
"github.com/NYTimes/gziphandler"
)
func Gzip(next http.Handler) http.Handler {
return gziphandler.GzipHandler(next)
}

25
package-lock.json generated
View File

@@ -8,6 +8,9 @@
"name": "me-fit", "name": "me-fit",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": {
"keycloak-js": "^25.0.4"
},
"devDependencies": { "devDependencies": {
"daisyui": "4.12.10", "daisyui": "4.12.10",
"htmx.org": "2.0.2", "htmx.org": "2.0.2",
@@ -628,6 +631,28 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-sha256": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz",
"integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q=="
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"engines": {
"node": ">=18"
}
},
"node_modules/keycloak-js": {
"version": "25.0.4",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.4.tgz",
"integrity": "sha512-LW7dVgqcBxMnnJTdmh7Zgd0NpStJnX2sCMrJGqcGtm4zmk4Rwlqk2o2uOvY7PaRHHYePXfbIwrqVhlN3GAnRCg==",
"dependencies": {
"js-sha256": "^0.11.0",
"jwt-decode": "^4.0.0"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",

View File

@@ -4,16 +4,17 @@
"description": "Your (almost) independent tech stack to host on a VPC.", "description": "Your (almost) independent tech stack to host on a VPC.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --minify", "copy": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && cp -f node_modules/keycloak-js/dist/keycloak.min.js static/js/keycloak.min.js",
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --watch", "build": "npm run copy && tailwindcss build -o static/css/tailwind.css --minify",
"test": "" "watch": "tailwindcss build -o static/css/tailwind.css --watch"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"htmx.org": "2.0.2", "htmx.org": "2.0.2",
"daisyui": "4.12.10",
"tailwindcss": "3.4.10", "tailwindcss": "3.4.10",
"daisyui": "4.12.10" "keycloak-js": "^25.0.4"
} }
} }

View File

@@ -2,7 +2,6 @@ package service
import ( import (
"me-fit/templates" "me-fit/templates"
"me-fit/utils"
"database/sql" "database/sql"
"net/http" "net/http"
@@ -23,7 +22,7 @@ var (
) )
) )
func App(w http.ResponseWriter, r *http.Request) { func WorkoutIndex(w http.ResponseWriter, r *http.Request) {
comp := templates.App() comp := templates.App()
layout := templates.Layout(comp) layout := templates.Layout(comp)
layout.Render(r.Context(), w) layout.Render(r.Context(), w)
@@ -31,7 +30,12 @@ func App(w http.ResponseWriter, r *http.Request) {
func NewWorkout(db *sql.DB) http.HandlerFunc { func NewWorkout(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("new").Inc() // metrics.WithLabelValues("new").Inc()
// if not isAuthorized(r) {
// http.Error(w, "Unauthorized", http.StatusUnauthorized)
// return
// }
var dateStr = r.FormValue("date") var dateStr = r.FormValue("date")
var typeStr = r.FormValue("type") var typeStr = r.FormValue("type")
@@ -73,66 +77,66 @@ func NewWorkout(db *sql.DB) http.HandlerFunc {
} }
} }
func GetWorkouts(db *sql.DB) http.HandlerFunc { // func GetWorkouts(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("get").Inc() // metrics.WithLabelValues("get").Inc()
//
// token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) // // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
// var userId = token.UID // // var userId = token.UID
var userId = "" // var userId = ""
//
rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", userId) // rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", userId)
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // http.Error(w, err.Error(), http.StatusInternalServerError)
return // return
} // }
//
var workouts = make([]map[string]interface{}, 0) // var workouts = make([]map[string]interface{}, 0)
for rows.Next() { // for rows.Next() {
var id int // var id int
var date string // var date string
var workoutType string // var workoutType string
var sets int // var sets int
var reps int // var reps int
//
err = rows.Scan(&id, &date, &workoutType, &sets, &reps) // err = rows.Scan(&id, &date, &workoutType, &sets, &reps)
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // http.Error(w, err.Error(), http.StatusInternalServerError)
return // return
} // }
//
workout := map[string]interface{}{ // workout := map[string]interface{}{
"id": id, // "id": id,
"date": date, // "date": date,
"type": workoutType, // "type": workoutType,
"sets": sets, // "sets": sets,
"reps": reps, // "reps": reps,
} // }
workouts = append(workouts, workout) // workouts = append(workouts, workout)
} // }
//
utils.WriteJSON(w, workouts) // utils.WriteJSON(w, workouts)
} // }
} // }
//
func DeleteWorkout(db *sql.DB) http.HandlerFunc { // func DeleteWorkout(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("delete").Inc() // metrics.WithLabelValues("delete").Inc()
//
// token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) // // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
// var userId = token.UID // // var userId = token.UID
var userId = "" // var userId = ""
//
rowId := r.FormValue("id") // rowId := r.FormValue("id")
if rowId == "" { // if rowId == "" {
http.Error(w, "Missing required fields", http.StatusBadRequest) // http.Error(w, "Missing required fields", http.StatusBadRequest)
return // return
} // }
//
_, err := db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", userId, rowId) // _, err := db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", userId, rowId)
if err != nil { // if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // http.Error(w, err.Error(), http.StatusInternalServerError)
return // return
} // }
} // }
} // }

11
static/js/keycloak.min.js vendored Normal file

File diff suppressed because one or more lines are too long

23
static/js/layout.js Normal file
View File

@@ -0,0 +1,23 @@
const keycloak = new Keycloak({
url: 'https://auth.me-fit.eu',
realm: 'me-fit',
clientId: 'me-fit'
});
async function initKeycloak() {
try {
const authenticated = await keycloak.init({
checkLoginIframe: false,
});
console.log(`User is ${authenticated ? 'authenticated' : 'not authenticated'}`);
console.log({ keycloak });
} catch (error) {
console.error('Failed to initialize adapter:', error);
}
};
initKeycloak();
async function login() {
await keycloak.login();
};

View File

@@ -7,6 +7,6 @@ templ header() {
<span>ME-FIT</span> <span>ME-FIT</span>
</a> </a>
<a href="/signup" class="btn btn-sm">Sign Up</a> <a href="/signup" class="btn btn-sm">Sign Up</a>
<a href="/signin" class="btn btn-sm">Sign In</a> <button class="btn btn-sm" onclick="login()">Sign In</button>
</div> </div>
} }

View File

@@ -10,6 +10,8 @@ templ Layout(comp templ.Component) {
<link rel="stylesheet" href="static/css/tailwind.css"/> <link rel="stylesheet" href="static/css/tailwind.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<script defer src="https://umami.me-fit.eu/script.js" data-website-id="3c8efb09-44e4-4372-8a1e-c3bc675cd89a"></script> <script defer src="https://umami.me-fit.eu/script.js" data-website-id="3c8efb09-44e4-4372-8a1e-c3bc675cd89a"></script>
<script src="static/js/keycloak.min.js"></script>
<script src="static/js/layout.js"></script>
</head> </head>
<body> <body>
<div class="h-screen flex flex-col"> <div class="h-screen flex flex-col">

View File

@@ -1,9 +1,65 @@
package utils package utils
// import ( import (
// "context" "encoding/json"
// "log" "errors"
// ) "io"
"log"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
func InitializeAuth() {
resp, err := http.Get("https://auth.me-fit.eu/realms/me-fit/protocol/openid-connect/certs")
if err != nil {
log.Fatalf("error getting certs: %v\n", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("error reading body: %v\n", err)
}
var certs map[string]interface{}
err = json.Unmarshal(body, &certs)
if err != nil {
log.Fatalf("error unmarshalling certs: %v\n", err)
}
log.Println("initialized auth", certs["keys"].([]interface{})[0].(map[string]interface{})["kid"])
}
func keyFunc() jwt.Keyfunc {
return func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil
}
}
func isAuthorized(r *http.Request) (*jwt.Token, error) {
auth := r.Header.Get("Authorization")
if auth == "" {
return nil, errors.New("no authorization header")
}
tokenStr := strings.Split(auth, " ")[1]
if tokenStr == "" {
return nil, errors.New("no authorization header")
}
token, err := jwt.Parse(tokenStr, keyFunc(), nil)
if err != nil {
return nil, errors.New("no authorization header")
}
if !token.Valid {
return nil, errors.New("no authorization header")
}
return token, nil
}
// func VerifyToken(token string) (*auth.Token, error) { // func VerifyToken(token string) (*auth.Token, error) {
// if app == nil { // if app == nil {