diff --git a/go.mod b/go.mod index 922014c..5e03580 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 859afb9..9895196 100644 --- a/go.sum +++ b/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= diff --git a/handler.go b/handler.go index 9b8de5d..158d9e0 100644 --- a/handler.go +++ b/handler.go @@ -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))) } diff --git a/main.go b/main.go index eb9fd34..1c2758c 100644 --- a/main.go +++ b/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) diff --git a/middleware/gzip.go b/middleware/gzip.go new file mode 100644 index 0000000..32454a3 --- /dev/null +++ b/middleware/gzip.go @@ -0,0 +1,11 @@ +package middleware + +import ( + "net/http" + + "github.com/NYTimes/gziphandler" +) + +func Gzip(next http.Handler) http.Handler { + return gziphandler.GzipHandler(next) +} diff --git a/package-lock.json b/package-lock.json index 7f1d164..5925f7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0597fdb..6877b8c 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/service/workout.go b/service/workout.go index 41b2dcf..297aa3d 100644 --- a/service/workout.go +++ b/service/workout.go @@ -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 +// } +// } +// } diff --git a/static/js/keycloak.min.js b/static/js/keycloak.min.js new file mode 100644 index 0000000..d63ebf2 --- /dev/null +++ b/static/js/keycloak.min.js @@ -0,0 +1,11 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("crypto"),require("buffer")):"function"==typeof define&&define.amd?define("keycloak",["crypto","buffer"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Keycloak=t(e.require$$0,e.require$$1)}(this,(function(e,t){"use strict";var r="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function n(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var o={exports:{}};!function(e,t){e.exports=function(){function e(e){var t=typeof e;return null!==e&&("object"===t||"function"===t)}function t(e){return"function"==typeof e}function n(e){K=e}function o(e){J=e}function i(){return function(){return process.nextTick(l)}}function s(){return void 0!==V?function(){V(l)}:u()}function a(){var e=0,t=new F(l),r=document.createTextNode("");return t.observe(r,{characterData:!0}),function(){r.data=e=++e%2}}function c(){var e=new MessageChannel;return e.port1.onmessage=l,function(){return e.port2.postMessage(0)}}function u(){var e=setTimeout;return function(){return e(l,1)}}function l(){for(var e=0;e>>6,a[u++]=128|63&s):s<55296||s>=57344?(a[u++]=224|s>>>12,a[u++]=128|s>>>6&63,a[u++]=128|63&s):(s=65536+((1023&s)<<10|1023&e.charCodeAt(++n)),a[u++]=240|s>>>18,a[u++]=128|s>>>12&63,a[u++]=128|s>>>6&63,a[u++]=128|63&s);e=a}else{if("object"!==i)throw new Error(o);if(null===e)throw new Error(o);if(l&&e.constructor===ArrayBuffer)e=new Uint8Array(e);else if(!(Array.isArray(e)||l&&ArrayBuffer.isView(e)))throw new Error(o)}e.length>64&&(e=new _(t,!0).update(e).array());var d=[],f=[];for(n=0;n<64;++n){var h=e[n]||0;d[n]=92^h,f[n]=54^h}_.call(this,t,r),this.update(f),this.oKeyPad=d,this.inner=!0,this.sharedMemory=r}_.prototype.update=function(e){if(!this.finalized){var t,r=typeof e;if("string"!==r){if("object"!==r)throw new Error(o);if(null===e)throw new Error(o);if(l&&e.constructor===ArrayBuffer)e=new Uint8Array(e);else if(!(Array.isArray(e)||l&&ArrayBuffer.isView(e)))throw new Error(o);t=!0}for(var n,i,s=0,a=e.length,c=this.blocks;s>>2]|=e[s]<>>2]|=n<>>2]|=(192|n>>>6)<>>2]|=(128|63&n)<=57344?(c[i>>>2]|=(224|n>>>12)<>>2]|=(128|n>>>6&63)<>>2]|=(128|63&n)<>>2]|=(240|n>>>18)<>>2]|=(128|n>>>12&63)<>>2]|=(128|n>>>6&63)<>>2]|=(128|63&n)<=64?(this.block=c[16],this.start=i-64,this.hash(),this.hashed=!0):this.start=i}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296|0,this.bytes=this.bytes%4294967296),this}},_.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var e=this.blocks,t=this.lastByteIndex;e[16]=this.block,e[t>>>2]|=f[3&t],this.block=e[16],t>=56&&(this.hashed||this.hash(),e[0]=this.block,e[16]=e[1]=e[2]=e[3]=e[4]=e[5]=e[6]=e[7]=e[8]=e[9]=e[10]=e[11]=e[12]=e[13]=e[14]=e[15]=0),e[14]=this.hBytes<<3|this.bytes>>>29,e[15]=this.bytes<<3,this.hash()}},_.prototype.hash=function(){var e,t,r,n,o,i,s,a,c,u=this.h0,l=this.h1,d=this.h2,f=this.h3,h=this.h4,v=this.h5,m=this.h6,g=this.h7,w=this.blocks;for(e=16;e<64;++e)t=((o=w[e-15])>>>7|o<<25)^(o>>>18|o<<14)^o>>>3,r=((o=w[e-2])>>>17|o<<15)^(o>>>19|o<<13)^o>>>10,w[e]=w[e-16]+t+w[e-7]+r|0;for(c=l&d,e=0;e<64;e+=4)this.first?(this.is224?(i=300032,g=(o=w[0]-1413257819)-150054599|0,f=o+24177077|0):(i=704751109,g=(o=w[0]-210244248)-1521486534|0,f=o+143694565|0),this.first=!1):(t=(u>>>2|u<<30)^(u>>>13|u<<19)^(u>>>22|u<<10),n=(i=u&l)^u&d^c,g=f+(o=g+(r=(h>>>6|h<<26)^(h>>>11|h<<21)^(h>>>25|h<<7))+(h&v^~h&m)+p[e]+w[e])|0,f=o+(t+n)|0),t=(f>>>2|f<<30)^(f>>>13|f<<19)^(f>>>22|f<<10),n=(s=f&u)^f&l^i,m=d+(o=m+(r=(g>>>6|g<<26)^(g>>>11|g<<21)^(g>>>25|g<<7))+(g&h^~g&v)+p[e+1]+w[e+1])|0,t=((d=o+(t+n)|0)>>>2|d<<30)^(d>>>13|d<<19)^(d>>>22|d<<10),n=(a=d&f)^d&u^s,v=l+(o=v+(r=(m>>>6|m<<26)^(m>>>11|m<<21)^(m>>>25|m<<7))+(m&g^~m&h)+p[e+2]+w[e+2])|0,t=((l=o+(t+n)|0)>>>2|l<<30)^(l>>>13|l<<19)^(l>>>22|l<<10),n=(c=l&d)^l&f^a,h=u+(o=h+(r=(v>>>6|v<<26)^(v>>>11|v<<21)^(v>>>25|v<<7))+(v&m^~v&g)+p[e+3]+w[e+3])|0,u=o+(t+n)|0,this.chromeBugWorkAround=!0;this.h0=this.h0+u|0,this.h1=this.h1+l|0,this.h2=this.h2+d|0,this.h3=this.h3+f|0,this.h4=this.h4+h|0,this.h5=this.h5+v|0,this.h6=this.h6+m|0,this.h7=this.h7+g|0},_.prototype.hex=function(){this.finalize();var e=this.h0,t=this.h1,r=this.h2,n=this.h3,o=this.h4,i=this.h5,s=this.h6,a=this.h7,c=d[e>>>28&15]+d[e>>>24&15]+d[e>>>20&15]+d[e>>>16&15]+d[e>>>12&15]+d[e>>>8&15]+d[e>>>4&15]+d[15&e]+d[t>>>28&15]+d[t>>>24&15]+d[t>>>20&15]+d[t>>>16&15]+d[t>>>12&15]+d[t>>>8&15]+d[t>>>4&15]+d[15&t]+d[r>>>28&15]+d[r>>>24&15]+d[r>>>20&15]+d[r>>>16&15]+d[r>>>12&15]+d[r>>>8&15]+d[r>>>4&15]+d[15&r]+d[n>>>28&15]+d[n>>>24&15]+d[n>>>20&15]+d[n>>>16&15]+d[n>>>12&15]+d[n>>>8&15]+d[n>>>4&15]+d[15&n]+d[o>>>28&15]+d[o>>>24&15]+d[o>>>20&15]+d[o>>>16&15]+d[o>>>12&15]+d[o>>>8&15]+d[o>>>4&15]+d[15&o]+d[i>>>28&15]+d[i>>>24&15]+d[i>>>20&15]+d[i>>>16&15]+d[i>>>12&15]+d[i>>>8&15]+d[i>>>4&15]+d[15&i]+d[s>>>28&15]+d[s>>>24&15]+d[s>>>20&15]+d[s>>>16&15]+d[s>>>12&15]+d[s>>>8&15]+d[s>>>4&15]+d[15&s];return this.is224||(c+=d[a>>>28&15]+d[a>>>24&15]+d[a>>>20&15]+d[a>>>16&15]+d[a>>>12&15]+d[a>>>8&15]+d[a>>>4&15]+d[15&a]),c},_.prototype.toString=_.prototype.hex,_.prototype.digest=function(){this.finalize();var e=this.h0,t=this.h1,r=this.h2,n=this.h3,o=this.h4,i=this.h5,s=this.h6,a=this.h7,c=[e>>>24&255,e>>>16&255,e>>>8&255,255&e,t>>>24&255,t>>>16&255,t>>>8&255,255&t,r>>>24&255,r>>>16&255,r>>>8&255,255&r,n>>>24&255,n>>>16&255,n>>>8&255,255&n,o>>>24&255,o>>>16&255,o>>>8&255,255&o,i>>>24&255,i>>>16&255,i>>>8&255,255&i,s>>>24&255,s>>>16&255,s>>>8&255,255&s];return this.is224||c.push(a>>>24&255,a>>>16&255,a>>>8&255,255&a),c},_.prototype.array=_.prototype.digest,_.prototype.arrayBuffer=function(){this.finalize();var e=new ArrayBuffer(this.is224?28:32),t=new DataView(e);return t.setUint32(0,this.h0),t.setUint32(4,this.h1),t.setUint32(8,this.h2),t.setUint32(12,this.h3),t.setUint32(16,this.h4),t.setUint32(20,this.h5),t.setUint32(24,this.h6),this.is224||t.setUint32(28,this.h7),e},S.prototype=new _,S.prototype.finalize=function(){if(_.prototype.finalize.call(this),this.inner){this.inner=!1;var e=this.array();_.call(this,this.is224,this.sharedMemory),this.update(this.oKeyPad),this.update(e),_.prototype.finalize.call(this)}};var A=w();A.sha256=A,A.sha224=w(!0),A.sha256.hmac=b(),A.sha224.hmac=b(!0),u?n.exports=A:(s.sha256=A.sha256,s.sha224=A.sha224)}()}(s);var a=n(s.exports);class c extends Error{}function u(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");switch(t.length%4){case 0:break;case 2:t+="==";break;case 3:t+="=";break;default:throw new Error("base64 string is not of the correct length")}try{return function(e){return decodeURIComponent(atob(e).replace(/(.)/g,((e,t)=>{let r=t.charCodeAt(0).toString(16).toUpperCase();return r.length<2&&(r="0"+r),"%"+r})))}(t)}catch(e){return atob(t)}}function l(e,t){if("string"!=typeof e)throw new c("Invalid token specified: must be a string");t||(t={});const r=!0===t.header?0:1,n=e.split(".")[r];if("string"!=typeof n)throw new c(`Invalid token specified: missing part #${r+1}`);let o;try{o=u(n)}catch(e){throw new c(`Invalid token specified: invalid base64 for part #${r+1} (${e.message})`)}try{return JSON.parse(o)}catch(e){throw new c(`Invalid token specified: invalid json for part #${r+1} (${e.message})`)}}if(c.prototype.name="InvalidTokenError",void 0===i.Promise)throw Error("Keycloak requires an environment that supports Promises. Make sure that you include the appropriate polyfill.");return function e(t){if(!(this instanceof e))throw new Error("The 'Keycloak' constructor must be invoked with 'new'.");for(var r,n,o=this,s=[],c={enable:!0,callbackList:[],interval:5},u=document.getElementsByTagName("script"),d=0;d=0;--r){var n=t[r];"error"==e.data?n.setError():n.setSuccess("unchanged"==e.data)}}}),!1),e.promise}function T(){c.enable&&o.token&&setTimeout((function(){E().then((function(e){e&&T()}))}),1e3*c.interval)}function E(){var e=A();if(c.iframe&&c.iframeOrigin){var t=o.clientId+" "+(o.sessionId?o.sessionId:"");c.callbackList.push(e);var r=c.iframeOrigin;1==c.callbackList.length&&c.iframe.contentWindow.postMessage(t,r)}else e.setSuccess();return e.promise}function I(){var e=A();if(c.enable||o.silentCheckSsoRedirectUri){var t=document.createElement("iframe");t.setAttribute("src",o.endpoints.thirdPartyCookiesIframe()),t.setAttribute("sandbox","allow-storage-access-by-user-activation allow-scripts allow-same-origin"),t.setAttribute("title","keycloak-3p-check-iframe"),t.style.display="none",document.body.appendChild(t);var r=function(n){t.contentWindow===n.source&&("supported"!==n.data&&"unsupported"!==n.data||("unsupported"===n.data&&(p("[KEYCLOAK] Your browser is blocking access to 3rd-party cookies, this means:\n\n - It is not possible to retrieve tokens without redirecting to the Keycloak server (a.k.a. no support for silent authentication).\n - It is not possible to automatically detect changes to the session status (such as the user logging out in another tab).\n\nFor more information see: https://www.keycloak.org/docs/latest/securing_apps/#_modern_browsers"),c.enable=!1,o.silentCheckSsoFallback&&(o.silentCheckSsoRedirectUri=!1)),document.body.removeChild(t),window.removeEventListener("message",r),e.setSuccess()))};window.addEventListener("message",r,!1)}else e.setSuccess();return function(e,t,r){var n=null,o=new i.Promise((function(e,o){n=setTimeout((function(){o({error:r})}),t)}));return i.Promise.race([e,o]).finally((function(){clearTimeout(n)}))}(e.promise,o.messageReceiveTimeout,"Timeout when waiting for 3rd party check iframe message.")}function O(e){if(!e||"default"==e)return{login:function(e){return window.location.assign(o.createLoginUrl(e)),A().promise},logout:async function(e){if("GET"===(e?.logoutMethod??o.logoutMethod))return void window.location.replace(o.createLogoutUrl(e));const t=o.createLogoutUrl(e),n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({id_token_hint:o.idToken,client_id:o.clientId,post_logout_redirect_uri:r.redirectUri(e,!1)})});if(n.redirected)window.location.href=n.url;else{if(!n.ok)throw new Error("Logout failed, request returned an error code.");window.location.reload()}},register:function(e){return window.location.assign(o.createRegisterUrl(e)),A().promise},accountManagement:function(){var e=o.createAccountUrl();if(void 0===e)throw"Not supported by the OIDC server";return window.location.href=e,A().promise},redirectUri:function(e,t){return e&&e.redirectUri?e.redirectUri:o.redirectUri?o.redirectUri:location.href}};if("cordova"==e){c.enable=!1;var t=function(e,t,r){return window.cordova&&window.cordova.InAppBrowser?window.cordova.InAppBrowser.open(e,t,r):window.open(e,t,r)},n=function(e){var t=function(e){return e&&e.cordovaOptions?Object.keys(e.cordovaOptions).reduce((function(t,r){return t[r]=e.cordovaOptions[r],t}),{}):{}}(e);return t.location="no",e&&"none"==e.prompt&&(t.hidden="yes"),function(e){return Object.keys(e).reduce((function(t,r){return t.push(r+"="+e[r]),t}),[]).join(",")}(t)},i=function(){return o.redirectUri||"http://localhost"};return{login:function(e){var r=A(),s=n(e),a=o.createLoginUrl(e),c=t(a,"_blank",s),u=!1,l=!1,d=function(){l=!0,c.close()};return c.addEventListener("loadstart",(function(e){0==e.url.indexOf(i())&&(w(_(e.url),r),d(),u=!0)})),c.addEventListener("loaderror",(function(e){u||(0==e.url.indexOf(i())?(w(_(e.url),r),d(),u=!0):(r.setError(),d()))})),c.addEventListener("exit",(function(e){l||r.setError({reason:"closed_by_user"})})),r.promise},logout:function(e){var r,n=A(),s=o.createLogoutUrl(e),a=t(s,"_blank","location=no,hidden=yes,clearcache=yes");return a.addEventListener("loadstart",(function(e){0==e.url.indexOf(i())&&a.close()})),a.addEventListener("loaderror",(function(e){0==e.url.indexOf(i())||(r=!0),a.close()})),a.addEventListener("exit",(function(e){r?n.setError():(o.clearToken(),n.setSuccess())})),n.promise},register:function(e){var r=A(),s=o.createRegisterUrl(),a=n(e),c=t(s,"_blank",a);return c.addEventListener("loadstart",(function(e){0==e.url.indexOf(i())&&(c.close(),w(_(e.url),r))})),r.promise},accountManagement:function(){var e=o.createAccountUrl();if(void 0===e)throw"Not supported by the OIDC server";var r=t(e,"_blank","location=no");r.addEventListener("loadstart",(function(e){0==e.url.indexOf(i())&&r.close()}))},redirectUri:function(e){return i()}}}if("cordova-native"==e)return c.enable=!1,{login:function(e){var t=A(),r=o.createLoginUrl(e);return universalLinks.subscribe("keycloak",(function(e){universalLinks.unsubscribe("keycloak"),window.cordova.plugins.browsertab.close(),w(_(e.url),t)})),window.cordova.plugins.browsertab.openUrl(r),t.promise},logout:function(e){var t=A(),r=o.createLogoutUrl(e);return universalLinks.subscribe("keycloak",(function(e){universalLinks.unsubscribe("keycloak"),window.cordova.plugins.browsertab.close(),o.clearToken(),t.setSuccess()})),window.cordova.plugins.browsertab.openUrl(r),t.promise},register:function(e){var t=A(),r=o.createRegisterUrl(e);return universalLinks.subscribe("keycloak",(function(e){universalLinks.unsubscribe("keycloak"),window.cordova.plugins.browsertab.close(),w(_(e.url),t)})),window.cordova.plugins.browsertab.openUrl(r),t.promise},accountManagement:function(){var e=o.createAccountUrl();if(void 0===e)throw"Not supported by the OIDC server";window.cordova.plugins.browsertab.openUrl(e)},redirectUri:function(e){return e&&e.redirectUri?e.redirectUri:o.redirectUri?o.redirectUri:"http://localhost"}};throw"invalid adapter type: "+e}o.init=function(e){if(o.didInitialize)throw new Error("A 'Keycloak' instance can only be initialized once.");o.didInitialize=!0,o.authenticated=!1,n=function(){try{return new C}catch(e){}return new R}();if(r=e&&["default","cordova","cordova-native"].indexOf(e.adapter)>-1?O(e.adapter):e&&"object"==typeof e.adapter?e.adapter:window.Cordova||window.cordova?O("cordova"):O(),e){if(void 0!==e.useNonce&&(f=e.useNonce),void 0!==e.checkLoginIframe&&(c.enable=e.checkLoginIframe),e.checkLoginIframeInterval&&(c.interval=e.checkLoginIframeInterval),"login-required"===e.onLoad&&(o.loginRequired=!0),e.responseMode){if("query"!==e.responseMode&&"fragment"!==e.responseMode)throw"Invalid value for responseMode";o.responseMode=e.responseMode}if(e.flow){switch(e.flow){case"standard":o.responseType="code";break;case"implicit":o.responseType="id_token token";break;case"hybrid":o.responseType="code id_token token";break;default:throw"Invalid value for flow"}o.flow=e.flow}if(null!=e.timeSkew&&(o.timeSkew=e.timeSkew),e.redirectUri&&(o.redirectUri=e.redirectUri),e.silentCheckSsoRedirectUri&&(o.silentCheckSsoRedirectUri=e.silentCheckSsoRedirectUri),"boolean"==typeof e.silentCheckSsoFallback?o.silentCheckSsoFallback=e.silentCheckSsoFallback:o.silentCheckSsoFallback=!0,void 0!==e.pkceMethod){if("S256"!==e.pkceMethod&&!1!==e.pkceMethod)throw new TypeError(`Invalid value for pkceMethod', expected 'S256' or false but got ${e.pkceMethod}.`);o.pkceMethod=e.pkceMethod}else o.pkceMethod="S256";"boolean"==typeof e.enableLogging?o.enableLogging=e.enableLogging:o.enableLogging=!1,"POST"===e.logoutMethod?o.logoutMethod="POST":o.logoutMethod="GET","string"==typeof e.scope&&(o.scope=e.scope),"string"==typeof e.acrValues&&(o.acrValues=e.acrValues),"number"==typeof e.messageReceiveTimeout&&e.messageReceiveTimeout>0?o.messageReceiveTimeout=e.messageReceiveTimeout:o.messageReceiveTimeout=1e4}o.responseMode||(o.responseMode="fragment"),o.responseType||(o.responseType="code",o.flow="standard");var i=A(),s=A();s.promise.then((function(){o.onReady&&o.onReady(o.authenticated),i.setSuccess(o.authenticated)})).catch((function(e){i.setError(e)}));var a=function(e){var r,n=A();t?"string"==typeof t&&(r=t):r="keycloak.json";function i(e){o.endpoints=e?{authorize:function(){return e.authorization_endpoint},token:function(){return e.token_endpoint},logout:function(){if(!e.end_session_endpoint)throw"Not supported by the OIDC server";return e.end_session_endpoint},checkSessionIframe:function(){if(!e.check_session_iframe)throw"Not supported by the OIDC server";return e.check_session_iframe},register:function(){throw'Redirection to "Register user" page not supported in standard OIDC mode'},userinfo:function(){if(!e.userinfo_endpoint)throw"Not supported by the OIDC server";return e.userinfo_endpoint}}:{authorize:function(){return g()+"/protocol/openid-connect/auth"},token:function(){return g()+"/protocol/openid-connect/token"},logout:function(){return g()+"/protocol/openid-connect/logout"},checkSessionIframe:function(){var e=g()+"/protocol/openid-connect/login-status-iframe.html";return o.iframeVersion&&(e=e+"?version="+o.iframeVersion),e},thirdPartyCookiesIframe:function(){var e=g()+"/protocol/openid-connect/3p-cookies/step1.html";return o.iframeVersion&&(e=e+"?version="+o.iframeVersion),e},register:function(){return g()+"/protocol/openid-connect/registrations"},userinfo:function(){return g()+"/protocol/openid-connect/userinfo"}}}if(r){(c=new XMLHttpRequest).open("GET",r,!0),c.setRequestHeader("Accept","application/json"),c.onreadystatechange=function(){if(4==c.readyState)if(200==c.status||k(c)){var e=JSON.parse(c.responseText);o.authServerUrl=e["auth-server-url"],o.realm=e.realm,o.clientId=e.resource,i(null),n.setSuccess()}else n.setError()},c.send()}else{if(!t.clientId)throw"clientId missing";o.clientId=t.clientId;var s=t.oidcProvider;if(s){var a,c;if("string"==typeof s)a="/"==s.charAt(s.length-1)?s+".well-known/openid-configuration":s+"/.well-known/openid-configuration",(c=new XMLHttpRequest).open("GET",a,!0),c.setRequestHeader("Accept","application/json"),c.onreadystatechange=function(){4==c.readyState&&(200==c.status||k(c)?(i(JSON.parse(c.responseText)),n.setSuccess()):n.setError())},c.send();else i(s),n.setSuccess()}else{if(!t.url)for(var u=document.getElementsByTagName("script"),l=0;l=0},o.hasResourceRole=function(e,t){if(!o.resourceAccess)return!1;var r=o.resourceAccess[t||o.clientId];return!!r&&r.roles.indexOf(e)>=0},o.loadUserProfile=function(){var e=g()+"/account",t=new XMLHttpRequest;t.open("GET",e,!0),t.setRequestHeader("Accept","application/json"),t.setRequestHeader("Authorization","bearer "+o.token);var r=A();return t.onreadystatechange=function(){4==t.readyState&&(200==t.status?(o.profile=JSON.parse(t.responseText),r.setSuccess(o.profile)):r.setError())},t.send(),r.promise},o.loadUserInfo=function(){var e=o.endpoints.userinfo(),t=new XMLHttpRequest;t.open("GET",e,!0),t.setRequestHeader("Accept","application/json"),t.setRequestHeader("Authorization","bearer "+o.token);var r=A();return t.onreadystatechange=function(){4==t.readyState&&(200==t.status?(o.userInfo=JSON.parse(t.responseText),r.setSuccess(o.userInfo)):r.setError())},t.send(),r.promise},o.isTokenExpired=function(e){if(!o.tokenParsed||!o.refreshToken&&"implicit"!=o.flow)throw"Not authenticated";if(null==o.timeSkew)return h("[KEYCLOAK] Unable to determine if token is expired as timeskew is not set"),!0;var t=o.tokenParsed.exp-Math.ceil((new Date).getTime()/1e3)+o.timeSkew;if(e){if(isNaN(e))throw"Invalid minValidity";t-=e}return t<0},o.updateToken=function(e){var t=A();if(!o.refreshToken)return t.setError(),t.promise;e=e||5;var r=function(){var r=!1;if(-1==e?(r=!0,h("[KEYCLOAK] Refreshing token: forced refresh")):o.tokenParsed&&!o.isTokenExpired(e)||(r=!0,h("[KEYCLOAK] Refreshing token: token expired")),r){var n="grant_type=refresh_token&refresh_token="+o.refreshToken,i=o.endpoints.token();if(s.push(t),1==s.length){var a=new XMLHttpRequest;a.open("POST",i,!0),a.setRequestHeader("Content-type","application/x-www-form-urlencoded"),a.withCredentials=!0,n+="&client_id="+encodeURIComponent(o.clientId);var c=(new Date).getTime();a.onreadystatechange=function(){if(4==a.readyState)if(200==a.status){h("[KEYCLOAK] Token refreshed"),c=(c+(new Date).getTime())/2;var e=JSON.parse(a.responseText);y(e.access_token,e.refresh_token,e.id_token,c),o.onAuthRefreshSuccess&&o.onAuthRefreshSuccess();for(var t=s.pop();null!=t;t=s.pop())t.setSuccess(!0)}else{p("[KEYCLOAK] Failed to refresh token"),400==a.status&&o.clearToken(),o.onAuthRefreshError&&o.onAuthRefreshError();for(t=s.pop();null!=t;t=s.pop())t.setError(!0)}},a.send(n)}}else t.setSuccess(!1)};c.enable?E().then((function(){r()})).catch((function(e){t.setError(e)})):r();return t.promise},o.clearToken=function(){o.token&&(y(null,null,null),o.onAuthLogout&&o.onAuthLogout(),o.loginRequired&&o.login())};var C=function(){if(!(this instanceof C))return new C;localStorage.setItem("kc-test","test"),localStorage.removeItem("kc-test");function e(){for(var e=(new Date).getTime(),t=0;tME-FIT Sign Up - Sign In + } diff --git a/templates/layout.templ b/templates/layout.templ index 0a71e1d..10faf1c 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -10,6 +10,8 @@ templ Layout(comp templ.Component) { + +
diff --git a/utils/auth.go b/utils/auth.go index 108440c..91a6dbe 100644 --- a/utils/auth.go +++ b/utils/auth.go @@ -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 {