package pkg import ( "context" "net" "net/http" "time" "golang.org/x/text/language" ) const ( ContextUserKey = "numerus-user" ContextCookieKey = "numerus-cookie" ContextConnKey = "numerus-database" sessionCookie = "numerus-session" defaultRole = "guest" ) type LoginPage struct { LoginError bool Email string Password string } type AppUser struct { Email string LoggedIn bool Role string Language language.Tag } func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := getUser(r) if user.LoggedIn { http.Redirect(w, r, "/", http.StatusSeeOther) return } r.ParseForm() page := LoginPage{ Email: r.FormValue("email"), Password: r.FormValue("password"), } if r.Method == "POST" { conn := getConn(r) cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r)) if cookie != "" { setSessionCookie(w, cookie) http.Redirect(w, r, "/", http.StatusSeeOther) return } w.WriteHeader(http.StatusUnauthorized) page.LoginError = true } else { w.WriteHeader(http.StatusOK) } mustRenderWebTemplate(w, r, "login.gohtml", page) }) } func LogoutHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn := getConn(r) conn.MustExec(r.Context(), "select logout()") http.SetCookie(w, createSessionCookie("", -24*time.Hour)) }) } func remoteAddr(r *http.Request) string { address := r.Header.Get("X-Forwarded-For") if address == "" { address, _, _ = net.SplitHostPort(r.RemoteAddr) } return address } func setSessionCookie(w http.ResponseWriter, cookie string) { http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour)) } func createSessionCookie(value string, duration time.Duration) *http.Cookie { return &http.Cookie{ Name: sessionCookie, Value: value, Path: "/", Expires: time.Now().Add(duration), HttpOnly: true, SameSite: http.SameSiteLaxMode, } } func CheckLogin(db *Db, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ctx = r.Context() if cookie, err := r.Cookie(sessionCookie); err == nil { ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value) } conn := db.MustAcquire(ctx) defer conn.Release() ctx = context.WithValue(ctx, ContextConnKey, conn) user := &AppUser{ Email: "", LoggedIn: false, Role: defaultRole, } row := conn.QueryRow(ctx, "select coalesce(email, ''), role, lang_tag from user_profile") var langTag string if err := row.Scan(&user.Email, &user.Role, &langTag); err != nil { panic(err) } user.LoggedIn = user.Email != "" user.Language, _ = language.Parse(langTag) ctx = context.WithValue(ctx, ContextUserKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } func getUser(r *http.Request) *AppUser { return r.Context().Value(ContextUserKey).(*AppUser) } func getConn(r *http.Request) *Conn { return r.Context().Value(ContextConnKey).(*Conn) } func Authenticated(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := getUser(r) if user.LoggedIn { next.ServeHTTP(w, r) } else { http.Redirect(w, r, "/login", http.StatusSeeOther) } }) }