#73 begin implement keycloak
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 47s
This commit is contained in:
2
go.mod
2
go.mod
@@ -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
7
go.sum
@@ -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=
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
2
main.go
2
main.go
@@ -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
11
middleware/gzip.go
Normal 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
25
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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
23
static/js/layout.js
Normal 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();
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user