diff --git a/go.mod b/go.mod index 922014c..3649357 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ go 1.22.5 require ( github.com/a-h/templ v0.2.771 github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/google/uuid v1.4.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.22 github.com/prometheus/client_golang v1.20.2 + golang.org/x/crypto v0.20.0 ) require ( diff --git a/go.sum b/go.sum index 859afb9..a22ef81 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMn 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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -41,6 +43,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT 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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/handler.go b/handler.go index 9b8de5d..7098725 100644 --- a/handler.go +++ b/handler.go @@ -11,15 +11,21 @@ import ( func getHandler(db *sql.DB) http.Handler { var router = http.NewServeMux() - router.HandleFunc("/", service.HandleIndexAnd404) + router.HandleFunc("/", service.HandleIndexAnd404(db)) // 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("POST /api/workout", service.NewWorkout(db)) - router.HandleFunc("GET /api/workout", service.GetWorkouts(db)) - router.HandleFunc("DELETE /api/workout", service.DeleteWorkout(db)) + router.HandleFunc("/app", service.HandleWorkoutPage(db)) + router.HandleFunc("POST /api/workout", service.HandleNewWorkout(db)) + router.HandleFunc("GET /api/workout", service.HandleGetWorkouts(db)) + router.HandleFunc("DELETE /api/workout/{id}", service.HandleDeleteWorkout(db)) + + router.HandleFunc("/auth/signin", service.HandleSignInPage(db)) + router.HandleFunc("/auth/signup", service.HandleSignUpPage(db)) + router.HandleFunc("/api/auth/signup", service.HandleSignUp(db)) + router.HandleFunc("/api/auth/signin", service.HandleSignIn(db)) + router.HandleFunc("/api/auth/signout", service.HandleSignOutComp(db)) return middleware.Logging(middleware.EnableCors(router)) } diff --git a/migrations/001_initial_schema.up.sql b/migrations/001_initial_schema.up.sql index 9e3e5c9..df7bae9 100644 --- a/migrations/001_initial_schema.up.sql +++ b/migrations/001_initial_schema.up.sql @@ -6,3 +6,4 @@ CREATE TABLE workout ( sets INTEGER NOT NULL, reps INTEGER NOT NULL ); + diff --git a/migrations/002_user_and_session.up.sql b/migrations/002_user_and_session.up.sql new file mode 100644 index 0000000..98aa0cf --- /dev/null +++ b/migrations/002_user_and_session.up.sql @@ -0,0 +1,21 @@ + +CREATE TABLE user ( + user_uuid TEXT NOT NULL UNIQUE PRIMARY KEY, + + email TEXT NOT NULL UNIQUE, + email_verified BOOLEAN NOT NULL, + + is_admin BOOLEAN NOT NULL, + + password BLOB NOT NULL, + salt BLOB NOT NULL, + + created_at DATETIME NOT NULL +) WITHOUT ROWID; + +CREATE TABLE session ( + session_id TEXT NOT NULL UNIQUE PRIMARY KEY, + user_uuid TEXT NOT NULL, + + created_at DATETIME NOT NULL +) WITHOUT ROWID; diff --git a/service/auth.go b/service/auth.go new file mode 100644 index 0000000..d14efec --- /dev/null +++ b/service/auth.go @@ -0,0 +1,273 @@ +package service + +import ( + "bytes" + "crypto/rand" + "database/sql" + "encoding/base64" + "log" + "net/http" + "net/mail" + "strings" + "time" + + "me-fit/template" + "me-fit/template/auth" + + "github.com/a-h/templ" + "github.com/google/uuid" + "golang.org/x/crypto/argon2" +) + +type User struct { + user_uuid uuid.UUID + email string +} + +func HandleSignInPage(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) + signIn := auth.SignInOrUp(true) + template.Layout(signIn, user_comp).Render(r.Context(), w) + } +} + +func HandleSignUpPage(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) + signIn := auth.SignInOrUp(false) + template.Layout(signIn, user_comp).Render(r.Context(), w) + } +} + +func UserInfoComp(user *User) templ.Component { + + if user != nil { + return auth.UserComp(user.email) + } else { + return auth.UserComp("") + } +} + +func HandleSignUp(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var email = r.FormValue("email") + var password = r.FormValue("password") + + _, err := mail.ParseAddress(email) + if err != nil { + http.Error(w, "Invalid email", http.StatusBadRequest) + return + } + + if len(password) < 8 || + !strings.ContainsAny(password, "0123456789") || + !strings.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") || + !strings.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") || + !strings.ContainsAny(password, "!@#$%^&*()_+-=[]{}\\|;:'\",.<>/?") { + http.Error(w, "Password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character", http.StatusBadRequest) + return + } + + user_uuid, err := uuid.NewRandom() + if err != nil { + log.Printf("Could not generate UUID: %v", err) + auth.Error("Internal Server Error").Render(r.Context(), w) + return + } + + salt := make([]byte, 16) + rand.Read(salt) + + hash := getHashPassword(password, salt) + + _, err = db.Exec("INSERT INTO user (user_uuid, email, email_verified, is_admin, password, salt, created_at) VALUES (?, ?, FALSE, FALSE, ?, ?, datetime())", user_uuid, email, hash, salt) + if err != nil { + if strings.Contains(err.Error(), "email") { + auth.Error("Bad Request").Render(r.Context(), w) + return + } + + auth.Error("Internal Server Error").Render(r.Context(), w) + log.Printf("Could not insert user: %v", err) + return + } + + result := tryCreateSessionAndSetCookie(w, db, user_uuid) + if !result { + return + } + + w.Header().Add("HX-Redirect", "/") + w.WriteHeader(http.StatusOK) + } +} + +func HandleSignIn(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var email = r.FormValue("email") + var password = r.FormValue("password") + + var result bool = true + start := time.Now() + + var user_uuid uuid.UUID + var saved_hash []byte + var salt []byte + err := db.QueryRow("SELECT user_uuid, password, salt FROM user WHERE email = ?", email).Scan(&user_uuid, &saved_hash, &salt) + if err != nil { + result = false + } + + if result { + new_hash := getHashPassword(password, salt) + + if !bytes.Equal(new_hash, saved_hash) { + result = false + } + } + + if result { + result := tryCreateSessionAndSetCookie(w, db, user_uuid) + if !result { + return + } + } + + duration := time.Since(start) + time_to_wait := 100 - duration.Milliseconds() + // It is important to sleep for a while to prevent timing attacks + // If the email is correct, the server will calculate the hash, which will take some time + // This way an attacker could guess emails when comparing the response time + // Because of that, we cant use WriteHeader in the middle of the function. We have to wait until the end + time.Sleep(time.Duration(time_to_wait) * time.Millisecond) + + if result { + w.Header().Add("HX-Redirect", "/") + w.WriteHeader(http.StatusOK) + } else { + auth.Error("Invalid email or password").Render(r.Context(), w) + } + } +} + +func HandleSignOutComp(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := getSessionID(r) + + _, err := db.Exec("DELETE FROM session WHERE session_id = ?", id) + if err != nil { + log.Printf("Could not delete session: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + c := http.Cookie{ + Name: "id", + Value: "", + MaxAge: -1, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Path: "/", + } + + http.SetCookie(w, &c) + auth.UserComp("").Render(r.Context(), w) + } +} + +// var ( +// metricsAuthSignUp = promauto.NewCounterVec( +// prometheus.CounterOpts{ +// Name: "mefit_api_auth_signup_total", +// Help: "The total number of auth signup api requests processed", +// }, +// []string{"result"}, +// ) +// +// metricsError = promauto.NewCounterVec( +// prometheus.CounterOpts{ +// Name: "mefit_api_error_total", +// Help: "The total number of errors", +// }, +// []string{"result"}, +// ) +// +// // metricsAuthSignIn = promauto.NewCounterVec( +// // prometheus.CounterOpts{ +// // Name: "mefit_api_auth_signin_total", +// // }, +// // []string{"result"}, +// // ) +// ) + +func tryCreateSessionAndSetCookie(w http.ResponseWriter, db *sql.DB, user_uuid uuid.UUID) bool { + var session_id_bytes []byte = make([]byte, 32) + _, err := rand.Reader.Read(session_id_bytes) + if err != nil { + log.Printf("Could not generate session ID: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return false + } + session_id := base64.StdEncoding.EncodeToString(session_id_bytes) + + _, err = db.Exec("INSERT INTO session (session_id, user_uuid, created_at) VALUES (?, ?, datetime())", session_id, user_uuid) + if err != nil { + log.Printf("Could not insert session: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return false + } + + cookie := http.Cookie{ + Name: "id", + Value: session_id, + MaxAge: 60 * 60 * 8, // 8 hours + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Path: "/", + } + http.SetCookie(w, &cookie) + + return true +} + +func getSessionID(r *http.Request) string { + for _, c := range r.Cookies() { + if c.Name == "id" { + return c.Value + } + } + return "" +} + +func verifySessionAndReturnUser(db *sql.DB, r *http.Request) *User { + session_id := getSessionID(r) + if session_id == "" { + return nil + } + + var user User + var created_at time.Time + + err := db.QueryRow(` + SELECT u.user_uuid, u.email, s.created_at + FROM session s + INNER JOIN user u ON s.user_uuid = u.user_uuid + WHERE session_id = ?`, session_id).Scan(&user.user_uuid, &user.email, &created_at) + if err != nil { + log.Printf("Could not verify session: %v", err) + return nil + } + + if created_at.Add(time.Duration(8 * time.Hour)).Before(time.Now()) { + return nil + } + + return &user +} + +func getHashPassword(password string, salt []byte) []byte { + return argon2.IDKey([]byte(password), salt, 1, 64*1024, 1, 16) +} diff --git a/service/static_ui.go b/service/static_ui.go index 2822b68..d86efe8 100644 --- a/service/static_ui.go +++ b/service/static_ui.go @@ -1,20 +1,25 @@ package service import ( - "me-fit/templates" + "database/sql" + "me-fit/template" "net/http" "github.com/a-h/templ" ) -func HandleIndexAnd404(w http.ResponseWriter, r *http.Request) { - var comp templ.Component = nil - if r.URL.Path != "/" { - comp = templates.Layout(templates.NotFound()) - w.WriteHeader(http.StatusNotFound) - } else { - comp = templates.Layout(templates.Index()) - } +func HandleIndexAnd404(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var comp templ.Component = nil + user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) - comp.Render(r.Context(), w) + if r.URL.Path != "/" { + comp = template.Layout(template.NotFound(), user_comp) + w.WriteHeader(http.StatusNotFound) + } else { + comp = template.Layout(template.Index(), user_comp) + } + + comp.Render(r.Context(), w) + } } diff --git a/service/workout.go b/service/workout.go index 41b2dcf..c0a6a78 100644 --- a/service/workout.go +++ b/service/workout.go @@ -1,8 +1,8 @@ package service import ( - "me-fit/templates" - "me-fit/utils" + "me-fit/template" + "me-fit/template/workout" "database/sql" "net/http" @@ -23,16 +23,26 @@ var ( ) ) -func App(w http.ResponseWriter, r *http.Request) { - comp := templates.App() - layout := templates.Layout(comp) - layout.Render(r.Context(), w) +func HandleWorkoutPage(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + currentDate := time.Now().Format("2006-01-02") + inner := workout.WorkoutComp(currentDate) + user_comp := UserInfoComp(verifySessionAndReturnUser(db, r)) + layout := template.Layout(inner, user_comp) + layout.Render(r.Context(), w) + } } -func NewWorkout(db *sql.DB) http.HandlerFunc { +func HandleNewWorkout(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { metrics.WithLabelValues("new").Inc() + user := verifySessionAndReturnUser(db, r) + if user == nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + var dateStr = r.FormValue("date") var typeStr = r.FormValue("type") var setsStr = r.FormValue("sets") @@ -61,78 +71,84 @@ func NewWorkout(db *sql.DB) http.HandlerFunc { return } - //TODO: Ensure auth - // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) - - _, err = db.Exec("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?)", "", date, typeStr, sets, reps) + _, err = db.Exec("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?)", user.user_uuid, date, typeStr, sets, reps) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + + wo := workout.Workout{ + Date: r.FormValue("date"), + Type: r.FormValue("type"), + Sets: r.FormValue("sets"), + Reps: r.FormValue("reps"), + } + + workout.WorkoutItemComp(wo, true).Render(r.Context(), w) } } -func GetWorkouts(db *sql.DB) http.HandlerFunc { +func HandleGetWorkouts(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 = "" + user := verifySessionAndReturnUser(db, r) + if user == nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } - 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 = ?", user.user_uuid) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - var workouts = make([]map[string]interface{}, 0) + var workouts = make([]workout.Workout, 0) for rows.Next() { - var id int - var date string - var workoutType string - var sets int - var reps int + var workout workout.Workout - err = rows.Scan(&id, &date, &workoutType, &sets, &reps) + err = rows.Scan(&workout.Id, &workout.Date, &workout.Type, &workout.Sets, &workout.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) + workout.WorkoutListComp(workouts).Render(r.Context(), w) } } -func DeleteWorkout(db *sql.DB) http.HandlerFunc { +func HandleDeleteWorkout(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 = "" + user := verifySessionAndReturnUser(db, r) - rowId := r.FormValue("id") + rowId := r.PathValue("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) + res, err := db.Exec("DELETE FROM workout WHERE user_id = ? AND rowid = ?", user.user_uuid, rowId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + + rows, err := res.RowsAffected() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if rows == 0 { + http.Error(w, "Not found", http.StatusNotFound) + return + } } } diff --git a/tailwind.config.js b/tailwind.config.js index cafdb9b..5ae080b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./templates/**/*.templ"], + content: ["./template/**/*.templ"], theme: { extend: {}, }, diff --git a/template/auth/sign_in_or_up.templ b/template/auth/sign_in_or_up.templ new file mode 100644 index 0000000..41ba568 --- /dev/null +++ b/template/auth/sign_in_or_up.templ @@ -0,0 +1,72 @@ +package auth + +templ SignInOrUp(isSignIn bool) { +
+

+ if isSignIn { + Sign In + } else { + Sign Up + } +

+ + +
+ if isSignIn { + Don't have an account? Sign Up + + } else { + Already have an account? Sign In + + } +
+ @Error("") +
+} + +templ Error(message string) { +

+ { message } +

+} diff --git a/template/auth/user.templ b/template/auth/user.templ new file mode 100644 index 0000000..4b639be --- /dev/null +++ b/template/auth/user.templ @@ -0,0 +1,13 @@ +package auth + +templ UserComp(user string) { +
+ if user != "" { + Sign Out +

{ user }

+ } else { + Sign Up + Sign In + } +
+} diff --git a/templates/index.templ b/template/index.templ similarity index 95% rename from templates/index.templ rename to template/index.templ index 352c04c..51fd811 100644 --- a/templates/index.templ +++ b/template/index.templ @@ -1,4 +1,4 @@ -package templates +package template templ Index() {
diff --git a/template/layout.templ b/template/layout.templ new file mode 100644 index 0000000..b81cf36 --- /dev/null +++ b/template/layout.templ @@ -0,0 +1,32 @@ +package template + +templ Layout(slot templ.Component, user templ.Component) { + + + + + ME-FIT + + + + + + + +
+
+ + ME-FIT logo + ME-FIT + + @user +
+
+ if slot != nil { + @slot + } +
+
+ + +} diff --git a/templates/not_found.templ b/template/not_found.templ similarity index 95% rename from templates/not_found.templ rename to template/not_found.templ index 9ace173..89bb7d5 100644 --- a/templates/not_found.templ +++ b/template/not_found.templ @@ -1,4 +1,4 @@ -package templates +package template templ NotFound() {
diff --git a/template/workout/workout.templ b/template/workout/workout.templ new file mode 100644 index 0000000..5f4ca57 --- /dev/null +++ b/template/workout/workout.templ @@ -0,0 +1,89 @@ +package workout + +templ WorkoutComp(currentDate string) { +
+
+

Track your workout

+ + + + + +
+
+
+} + +type Workout struct { + Id string + Date string + Type string + Sets string + Reps string +} + +templ WorkoutListComp(workouts []Workout) { +
+

Workout history

+ + + + + + + + + + + + + for _,w := range workouts { + @WorkoutItemComp(w, false) + } + +
DateTypeSetsReps
+
+} + +templ WorkoutItemComp(w Workout, includePlaceholder bool) { + if includePlaceholder { + + } + + { w.Date } + { w.Type } + { w.Sets } + { w.Reps } + +
+ +
+ + +} diff --git a/templates/app.templ b/templates/app.templ deleted file mode 100644 index 2eed8e7..0000000 --- a/templates/app.templ +++ /dev/null @@ -1,65 +0,0 @@ -package templates - -templ App() { -
-
-

Track your workout

- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-} diff --git a/templates/header.templ b/templates/header.templ deleted file mode 100644 index 66393fd..0000000 --- a/templates/header.templ +++ /dev/null @@ -1,12 +0,0 @@ -package templates - -templ header() { - -} diff --git a/templates/layout.templ b/templates/layout.templ deleted file mode 100644 index 0a71e1d..0000000 --- a/templates/layout.templ +++ /dev/null @@ -1,25 +0,0 @@ -package templates - -templ Layout(comp templ.Component) { - - - - - ME-FIT - - - - - - -
- @header() -
- if comp != nil { - @comp - } -
-
- - -} diff --git a/utils/auth.go b/utils/auth.go deleted file mode 100644 index 108440c..0000000 --- a/utils/auth.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -// import ( -// "context" -// "log" -// ) - -// func VerifyToken(token string) (*auth.Token, error) { -// if app == nil { -// setup() -// } -// -// client, err := app.Auth(context.Background()) -// if err != nil { -// log.Fatalf("error getting Auth client: %v\n", err) -// } -// return client.VerifyIDToken(context.Background(), token) -// } -// -// func setup() { -// opt := option.WithCredentialsFile("./secrets/firebase.json") -// -// firebaseApp, err := firebase.NewApp(context.Background(), nil, opt) -// -// if err != nil { -// log.Fatalf("error initializing app: %v", err) -// } -// -// app = firebaseApp -// }