package login import ( "net/http" . "git.gorlug.de/code/ersteller" "git.gorlug.de/code/ersteller/authentication" "git.gorlug.de/code/ersteller/starter/ent" "git.gorlug.de/code/ersteller/starter/ent/user" "github.com/gorilla/sessions" "golang.org/x/crypto/bcrypt" hx "maragu.dev/gomponents-htmx" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) const LoginPath = "/login" const LoginPathDe = "/anmelden" const LocalLoginPath = "/login/local" const LocalLoginPathDe = "/anmelden/lokal" var loginTexts *LoginTexts type LoginTexts struct { PageTitle I18nText PageDescription I18nText HeroTitle I18nText HeroDescription I18nText GoogleLoginBtn I18nText EmailLabel I18nText PasswordLabel I18nText LoginBtn I18nText OrSeparator I18nText InvalidCreds I18nText SessionSaveError I18nText } type Page struct { createPage CreateHtmxPageFunc ViewRoute HtmxRoute db *ent.Client sessionStore *sessions.CookieStore localLoginRoute HtmxRoute } func NewPage(createPage CreateHtmxPageFunc, server HtmxServer, path *ActivePath, db *ent.Client, sessionStore *sessions.CookieStore) *Page { if loginTexts == nil { createLoginTexts() } page := &Page{ createPage: createPage, db: db, sessionStore: sessionStore, } page.ViewRoute = NewHtmxGetRoute(page.View, LanguagePaths{ En: LoginPath, De: LoginPathDe, }).SetActivePath(path) page.ViewRoute.Add(server) // Add POST route for local login page.localLoginRoute = NewHtmxPostRoute(page.HandleLocalLogin, LanguagePaths{ En: LocalLoginPath, De: LocalLoginPathDe, }) page.localLoginRoute.Add(server) return page } func createLoginTexts() { loginTexts = &LoginTexts{ PageTitle: NewI18nText(map[Language]string{ En: "Login", De: "Anmelden", }), PageDescription: NewI18nText(map[Language]string{ En: "Sign in to your account", De: "Melden Sie sich bei Ihrem Konto an", }), HeroTitle: NewI18nText(map[Language]string{ En: "Sign In", De: "Anmelden", }), HeroDescription: NewI18nText(map[Language]string{ En: "Please sign in to access your account.", De: "Bitte melden Sie sich an, um auf Ihr Konto zuzugreifen.", }), GoogleLoginBtn: NewI18nText(map[Language]string{ En: "Sign in with Google", De: "Mit Google anmelden", }), EmailLabel: NewI18nText(map[Language]string{ En: "Email", De: "E-Mail", }), PasswordLabel: NewI18nText(map[Language]string{ En: "Password", De: "Passwort", }), LoginBtn: NewI18nText(map[Language]string{ En: "Sign in", De: "Anmelden", }), OrSeparator: NewI18nText(map[Language]string{ En: "OR", De: "ODER", }), InvalidCreds: NewI18nText(map[Language]string{ En: "Invalid email or password", De: "Ungültige E-Mail oder Passwort", }), SessionSaveError: NewI18nText(map[Language]string{ En: "Failed to save session", De: "Sitzung konnte nicht gespeichert werden", }), } } func (p *Page) getMetaData() PageWebsiteMetaData { return PageWebsiteMetaData{ Title: loginTexts.PageTitle, Lang: En, Description: loginTexts.PageDescription, HideNavigation: true, } } func (p *Page) View(c HtmxContext) { content := p.LoginContent(c.GetLanguage(), "") p.createPage(c, p.getMetaData(), content) } func (p *Page) LoginContent(language Language, errorMsg string) Group { nodes := []Node{ Div(Class("hero-section login-section"), H1(Class("hero-title"), Text(loginTexts.HeroTitle.FromLang(language))), P(Class("hero-description"), Text(loginTexts.HeroDescription.FromLang(language))), ), } if errorMsg != "" { nodes = append(nodes, Div(Class("error-message"), Text(errorMsg))) } nodes = append(nodes, Form( p.localLoginRoute.GetHtmx(language), hx.Target("body"), Class("login-form"), Div(Class("form-group"), Label(For("email"), Text(loginTexts.EmailLabel.FromLang(language))), Input(Type("email"), ID("email"), Name("email"), Required(), Class("form-control")), ), Div(Class("form-group"), Label(For("password"), Text(loginTexts.PasswordLabel.FromLang(language))), Input(Type("password"), ID("password"), Name("password"), Required(), Class("form-control")), ), Button( Type("submit"), Class("btn btn-primary"), Text(loginTexts.LoginBtn.FromLang(language)), ), ), Div(Class("separator"), Text(loginTexts.OrSeparator.FromLang(language))), Div(Class("login-buttons"), A( Href("/login/google"), Button( Class("btn btn-primary google-login-btn"), Type("button"), Text(loginTexts.GoogleLoginBtn.FromLang(language)), ), ), ), ) return nodes } func (p *Page) HandleLocalLogin(c HtmxContext) { req := c.GetRequest() if err := req.ParseForm(); err != nil { content := p.LoginContent(c.GetLanguage(), loginTexts.InvalidCreds.FromLang(c.GetLanguage())) p.createPage(c, p.getMetaData(), content) return } email := req.FormValue("email") password := req.FormValue("password") // Find user by email ctx := req.Context() foundUser, err := p.db.User.Query().Where(user.EmailEQ(email)).Only(ctx) if err != nil { Error("could not find user ", email, "with error:", err) content := p.LoginContent(c.GetLanguage(), loginTexts.InvalidCreds.FromLang(c.GetLanguage())) p.createPage(c, p.getMetaData(), content) return } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(foundUser.PasswordHash), []byte(password)); err != nil { Error("could not verify password for ", email, "with error:", err) content := p.LoginContent(c.GetLanguage(), loginTexts.InvalidCreds.FromLang(c.GetLanguage())) p.createPage(c, p.getMetaData(), content) return } // Set session err = authentication.SaveEmailAndUserIdToSessionStore(c.GetRequest(), c.GetResponseWriter(), p.sessionStore, foundUser.Email, foundUser.ID) if err != nil { Error("could not save user id for ", email, "to session store:", err) content := p.LoginContent(c.GetLanguage(), loginTexts.SessionSaveError.FromLang(c.GetLanguage())) p.createPage(c, p.getMetaData(), content) return } // Redirect to home page http.Redirect(c.GetResponseWriter(), req, "/", http.StatusSeeOther) }