Skip to content

Commit

Permalink
login page
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Jirku <martin@jirku.sk>
  • Loading branch information
martinjirku committed Apr 11, 2024
1 parent b47590f commit f48c533
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 29 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
GOOGLE_API_KEY=""
GOOGLE_CAPTCHA_SITE=""
GOOGLE_CALENDAR_ID="" # mcmamina
GOOGLE_SMTP_PWD=""
GOOGLE_SMTP_MAIL=""
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/justinas/nosurf v1.1.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
go.opencensus.io v0.24.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
4 changes: 4 additions & 0 deletions handlers/Index.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ type EventsGetter interface {
type CSSPathGetter interface {
GetCssPath() (string, error)
}
type RecaptchaValidator interface {
ValidateCaptcha(r *http.Request) error
Key() string
}
type IndexHandler struct {
Log *slog.Logger
EventsGetter EventsGetter
Expand Down
65 changes: 54 additions & 11 deletions handlers/Login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,66 @@ import (
"log/slog"
"net/http"

"github.com/justinas/nosurf"
"jirku.sk/mcmamina/template/components"
"jirku.sk/mcmamina/template/pages"
)

func Login(Log *slog.Logger, cssPathGetter CSSPathGetter) func(w http.ResponseWriter, r *http.Request) {
func Login(Log *slog.Logger,
cssPathGetter CSSPathGetter,
recaptcha RecaptchaValidator,
) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Log.Info("request", slog.String("method", r.Method), slog.String("path", r.URL.Path))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
cssPath, _ := cssPathGetter.GetCssPath()
dto := pages.NewLoginPageDto("", "", "")
// TODO: handle GET
// TODO: get CSRF token
components.Page(components.NewPage(
"Login",
"Prihlásenie",
cssPath,
pages.LoginPage(dto),
)).Render(r.Context(), w)
page := LoginPage{
cssPathGetter: cssPathGetter,
recaptcha: recaptcha,
log: Log,
}

switch r.Method {
case http.MethodGet:
page.loginGet(w, r)
case http.MethodPost:
page.loginAction(w, r)
}
}
}

type LoginPage struct {
cssPathGetter CSSPathGetter
recaptcha RecaptchaValidator
log *slog.Logger
}

func (l *LoginPage) loginGet(w http.ResponseWriter, r *http.Request) {
cssPath, _ := l.cssPathGetter.GetCssPath()
dto := pages.NewLoginPageDto(nosurf.Token(r), "", l.recaptcha.Key())
components.Page(components.NewPage(
"Login",
"Prihlásenie",
cssPath,
pages.LoginPage(dto),
)).Render(r.Context(), w)
}
func (l *LoginPage) loginAction(w http.ResponseWriter, r *http.Request) {
cssPath, _ := l.cssPathGetter.GetCssPath()
r.ParseForm()
username := r.Form.Get("username")

err := l.recaptcha.ValidateCaptcha(r)
dto := pages.NewLoginPageDto(nosurf.Token(r), username, l.recaptcha.Key())
if err != nil {
l.log.Info("recaptcha validation failed", slog.Any("error", err))
dto.SetErrorMsg("Chyba pri overovaní reCAPTCHA")
} else {
dto.SetErrorMsg("Nesprávne prihlasovacie údaje")
}
components.Page(components.NewPage(
"Login",
"Prihlásenie",
cssPath,
pages.LoginPage(dto),
)).Render(r.Context(), w)
}
19 changes: 12 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ import (

// .env keys
const (
GOOGLE_API_KEY = "GOOGLE_API_KEY"
GOOGLE_CALENDAR_ID = "GOOGLE_CALENDAR_ID"
GOOGLE_API_KEY = "GOOGLE_API_KEY"
GOOGLE_CALENDAR_ID = "GOOGLE_CALENDAR_ID"
GOOGLE_CAPTCHA_SITE = "GOOGLE_CAPTCHA_SITE"
)

//go:embed dist dist/.vite
Expand Down Expand Up @@ -82,11 +83,15 @@ func setupWebserver(log *slog.Logger, calendarService *services.CalendarService)

cssService := services.NewCSS(workingFolder, serviceLog(log, "css"))
sponsorService := services.NewSponsorService()
recaptchaService := services.NewRecaptchaService(os.Getenv(GOOGLE_API_KEY), os.Getenv(GOOGLE_CAPTCHA_SITE))

// Logger middleware
router.Use(middleware.Recover(middlwareLog(log, "recover")))
router.Use(middleware.RequestID(middlwareLog(log, "requestID")))
router.Use(middleware.Logger(middlwareLog(log, "logger")))
router.Use(middleware.Recover(middlewareLog(log, "recover")))
router.Use(middleware.RequestID(middlewareLog(log, "requestID")))
router.Use(middleware.Logger(middlewareLog(log, "logger")))

router.Use(middleware.Csrf)
// router.Use(middleware.AuthMiddleware(store))

router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -116,7 +121,7 @@ func setupWebserver(log *slog.Logger, calendarService *services.CalendarService)
router.HandleFunc("/", handlers.NewIndexHandler(log, calendarService, cssService).ServeHTTP)
router.HandleFunc("/o-nas", handlers.AboutUs(log, cssService))
// MCMAMINA -->> GENERATED CODE
router.HandleFunc("/prihlasenie", handlers.Login(log, cssService))
router.HandleFunc("/prihlasenie", handlers.Login(log, cssService, recaptchaService))
router.HandleFunc("/aktivity/burzy", handlers.Marketplace(log, cssService))
router.HandleFunc("/aktivity/podporne-skupiny", handlers.SupportGroups(log, cssService))
router.HandleFunc("/aktivity/predporodny-kurz", handlers.BabyDeliveryCourse(log, cssService))
Expand Down Expand Up @@ -200,7 +205,7 @@ func validatePort(port int) bool {
return port > 0 && port <= 65535
}

func middlwareLog(log *slog.Logger, name string) *slog.Logger {
func middlewareLog(log *slog.Logger, name string) *slog.Logger {
return log.With(slog.String("type", "middleware"), slog.String("name", name))
}

Expand Down
18 changes: 18 additions & 0 deletions pkg/middleware/csrf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package middleware

import (
"log"
"net/http"

"github.com/justinas/nosurf"
)

func Csrf(h http.Handler) http.Handler {
surfing := nosurf.New(h)
surfing.SetBaseCookie(http.Cookie{Path: "/"})
surfing.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Failed to validate CSRF token:", nosurf.Reason(r))
w.WriteHeader(http.StatusBadRequest)
}))
return surfing
}
55 changes: 55 additions & 0 deletions services/recaptcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package services

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)

type RecaptchaService struct {
apiKey string
siteKey string
}

func NewRecaptchaService(apiKey, siteKey string) *RecaptchaService {
return &RecaptchaService{
apiKey: apiKey,
siteKey: siteKey,
}
}

type googleCaptchaResponse struct {
Success bool `json:"success"`
ChallengeTs string `json:"challenge_ts"`
Hostname string `json:"hostname"`
ErrorCodes []string `json:"error-codes,omitempty"`
}

func (s *RecaptchaService) ValidateCaptcha(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return fmt.Errorf("parsing form: %w", err)
}
token := r.Form.Get("g-recaptcha-response")
if token == "" {
return fmt.Errorf("missing captcha token")
}
resp, err := http.PostForm(fmt.Sprintf("https://www.google.com/recaptcha/api/siteverify?%s", s.apiKey), url.Values{
"siteKey": {s.siteKey},
"response": {token},
})
if err != nil {
return fmt.Errorf("sending captcha request: %w", err)
}
defer resp.Body.Close()
var response googleCaptchaResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return fmt.Errorf("decoding captcha response: %w", err)
}
return nil
}

func (s *RecaptchaService) Key() string {
return s.siteKey
}
2 changes: 1 addition & 1 deletion template/layout/Layout.templ
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ templ Socials() {

templ Footer() {
<footer class="footer w-full flex pt-10 px-6 pb-16 shadow-xl content-center justify-around bg-neutral-900 h-min-40 text-indigo-100 sticky">
<div class="flex flex-col flex-wrap sm:flex-row gap-10 md:gap-x-32 justify-between xl:flex-nowrap md:max-w-5xl">
<div class="flex flex-col flex-wrap sm:flex-row gap-10 md:gap-x-28 justify-between xl:flex-nowrap md:max-w-5xl">
<address class="flex-grow not-italic font-thin text-sm">
<h2 class="font-bold leading-10 underline underline-offset-4">
Materské centrum MAMINA o.z.
Expand Down
53 changes: 43 additions & 10 deletions template/pages/Login.templ
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,66 @@ package pages
import (
"jirku.sk/mcmamina/template/layout"
"jirku.sk/mcmamina/template/components"
"github.com/justinas/nosurf"
)

type LoginPageDto struct {
csrf string
username string
password string
csrfToken string
recaptchaKey string
username string
errorMsg string
}

func NewLoginPageDto(csrf string, username string, password string) LoginPageDto {
func (mv *LoginPageDto) hasRecaptcha() bool {
return mv.recaptchaKey != ""
}

func NewLoginPageDto(csrfToken, username, recaptchaKey string) LoginPageDto {
return LoginPageDto{
csrf: csrf,
csrfToken: csrfToken,
recaptchaKey: recaptchaKey,
username: username,
password: password,
}
}
func (l *LoginPageDto) SetErrorMsg(msg string) {
l.errorMsg = msg
}

templ LoginPage(dto LoginPageDto) {
@layout.Layout(templ.CSSClasses{"login w-full bg-cover bg-center text-indigo-800 font-light"}, func(link string) bool { return link == "prihlasenie"}) {
@components.FullWidthCard(components.NewFullWidthCard().Margin("mb-0")) {
@components.CardContent("") {
<div>Prihlásenie</div>
<form action="/prihlasenie" method="post">
<input type="hidden" name="csrf" value={dto.csrf} />
<form action="/prihlasenie" method="post" id="login-form">
if dto.errorMsg != "" {
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">Chyba!</strong>
<span class="block sm:inline">{ dto.errorMsg }</span>
</div>
}
<input type="hidden" name={ nosurf.FormFieldName } value={ dto.csrfToken } />
<input type="text" name="username" placeholder="Prihlásenie" value={dto.username} />
<input type="password" name="password" placeholder="Heslo" value={dto.password}/>
<button type="submit">Prihlásiť</button>
<input type="password" name="password" placeholder="Heslo" value=""/>
<button
type="submit"
class={
"mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded",
templ.KV("g-recaptcha", dto.hasRecaptcha())
}
if dto.hasRecaptcha() {
data-sitekey={ dto.recaptchaKey }
data-callback="onSubmit"
data-action="submit"
}
>Prihlásiť</button>
</form>
<script>
function onSubmit(token) {
console.log("submitting form");
document.getElementById("login-form").submit();
}
</script>
<script src="https://www.google.com/recaptcha/api.js"></script>
}
}
}
Expand Down

0 comments on commit f48c533

Please sign in to comment.