Files
ersteller/starter/login/login.go
T
Achim Rohn a15ca501b8 Add authentication, styling needs to be fixed
# Conflicts:
#	starter/ent/runtime.go
#	starter/go.mod
#	starter/go.sum
#	starter/routes/routing.go
2026-03-21 14:00:47 +00:00

223 lines
6.1 KiB
Go

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)
}