#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
require (
github.com/NYTimes/gziphandler v1.1.1
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/joho/godotenv v1.5.1
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/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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/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/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
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/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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)
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("GET /api/workout", service.GetWorkouts(db))
router.HandleFunc("DELETE /api/workout", service.DeleteWorkout(db))
// router.HandleFunc("GET /api/workout", service.GetWorkouts(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")
}
utils.InitializeAuth()
db, err := sql.Open("sqlite3", "./data.db")
if err != nil {
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",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"keycloak-js": "^25.0.4"
},
"devDependencies": {
"daisyui": "4.12.10",
"htmx.org": "2.0.2",
@@ -628,6 +631,28 @@
"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": {
"version": "2.1.0",
"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.",
"main": "index.js",
"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",
"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",
"test": ""
"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",
"build": "npm run copy && tailwindcss build -o static/css/tailwind.css --minify",
"watch": "tailwindcss build -o static/css/tailwind.css --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"htmx.org": "2.0.2",
"daisyui": "4.12.10",
"tailwindcss": "3.4.10",
"daisyui": "4.12.10"
"keycloak-js": "^25.0.4"
}
}

View File

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

View File

@@ -10,6 +10,8 @@ templ Layout(comp templ.Component) {
<link rel="stylesheet" href="static/css/tailwind.css"/>
<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 src="static/js/keycloak.min.js"></script>
<script src="static/js/layout.js"></script>
</head>
<body>
<div class="h-screen flex flex-col">

View File

@@ -1,9 +1,65 @@
package utils
// import (
// "context"
// "log"
// )
import (
"encoding/json"
"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) {
// if app == nil {