Skip to content

Commit

Permalink
add login and admin page
Browse files Browse the repository at this point in the history
  • Loading branch information
martinjirku committed May 6, 2024
1 parent 48ec0b1 commit e1d0e70
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 28 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ 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.4 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/justinas/nosurf v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
Expand Down
40 changes: 40 additions & 0 deletions handlers/Admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package handlers

import (
"html/template"
"io/fs"
"log/slog"
"net/http"

"github.com/justinas/nosurf"
"jirku.sk/mcmamina/pkg/middleware"
)

type AdminHandlers struct {
CssPathGetter CSSPathGetter
Recaptcha RecaptchaValidator
Log *slog.Logger
loginTmpl *template.Template
}

func (h *AdminHandlers) InitTmpl(tmpl *template.Template, file fs.FS) *AdminHandlers {
var err error
h.loginTmpl, err = getTmpl(tmpl, "admin.tmpl", file)
if err != nil {
h.Log.Error("cloning template: %w", err)
}
return h
}

func (h *AdminHandlers) DashboardGet(w http.ResponseWriter, r *http.Request) {
model := createModel("Prihlásenie", "/login", "activities", h.CssPathGetter)
model["csrfTokenField"] = nosurf.FormFieldName
model["csrfToken"] = nosurf.Token(r)
model["user"] = middleware.GetUser(r)
model["recaptchaKey"] = h.Recaptcha.Key()

if err := h.loginTmpl.ExecuteTemplate(w, "page", model); err != nil {
h.Log.Error("page executing context", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
}
}
27 changes: 8 additions & 19 deletions handlers/Login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package handlers

import (
"encoding/json"
"fmt"
"html/template"
"io"
"io/fs"
"log/slog"
"net/http"

"github.com/gorilla/sessions"
"github.com/justinas/nosurf"
"golang.org/x/oauth2"

"jirku.sk/mcmamina/pkg/middleware"
"jirku.sk/mcmamina/pkg/models"
)

type RecaptchaValidator interface {
Expand Down Expand Up @@ -89,7 +92,7 @@ func (l *LoginPage) loginAction(w http.ResponseWriter, r *http.Request) {
}
}

func GoogleCallbackHandler(config oauth2.Config) http.HandlerFunc {
func GoogleCallbackHandler(config oauth2.Config, storeService sessions.Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
code := r.FormValue("code")
Expand All @@ -112,26 +115,12 @@ func GoogleCallbackHandler(config oauth2.Config) http.HandlerFunc {
http.Error(w, "Failed to read response: "+err.Error(), http.StatusInternalServerError)
return
}

var user struct {
Name string
ID string
Email string
VerifiedEmail string
Picture string
}
var user models.UserLogin
if err := json.Unmarshal(userInfoBytes, &user); err != nil {
http.Error(w, "Failed to parse user info: "+err.Error(), http.StatusInternalServerError)
return
}

// Render a response containing user info
fmt.Fprintf(w, `<html><head><title>Google User Info</title></head>
<body><h1>Welcome %s!</h1>
<p><strong>ID:</strong> %s</p><p><strong>Email:</strong> %s</p><p><strong>Email Verified:</strong> %v</p>
<p><strong>Profile Picture:</strong> <img src="%s" alt="Profile Picture"></p>
</body></html>`,
user.Name, user.ID, user.Email, user.VerifiedEmail, user.Picture)

middleware.StoreUser(w, r, &user, storeService)
http.Redirect(w, r, "/admin", http.StatusSeeOther)
}
}
32 changes: 25 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"embed"
"encoding/gob"
"flag"
"fmt"
"html/template"
Expand All @@ -20,11 +21,13 @@ import (

"github.com/42atomys/sprout"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/joho/godotenv"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"jirku.sk/mcmamina/handlers"
"jirku.sk/mcmamina/pkg/middleware"
"jirku.sk/mcmamina/pkg/models"
"jirku.sk/mcmamina/pkg/services"
)

Expand All @@ -36,6 +39,7 @@ const (
GOOGLE_AUTH_REDIRECT_PATH = "GOOGLE_AUTH_REDIRECT_PATH"
GOOGLE_AUTH_CLIENT_ID = "GOOGLE_AUTH_CLIENT_ID"
GOOGLE_AUTH_CLIENT_SECRET = "GOOGLE_AUTH_CLIENT_SECRET"
SESSION_KEY = "SESSION_KEY"
)

//go:embed dist dist/.vite templates/**/*.tmpl
Expand Down Expand Up @@ -76,8 +80,9 @@ func setupWebserver(log *slog.Logger) {
workingFolder = os.DirFS(config.publicPath)
}

cssService, sponsorService, recaptchaService, calendarService := prepareServices(log, workingFolder)
prepareMiddleware(router, log)
gob.Register(models.UserLogin{})
cssService, sponsorService, recaptchaService, calendarService, storeService := prepareServices(log, workingFolder)
prepareMiddleware(router, log, storeService)

router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -113,7 +118,19 @@ func setupWebserver(log *slog.Logger) {
Endpoint: google.Endpoint,
}
router.HandleFunc("/prihlasenie", handlers.Login(log, cssService, recaptchaService, tmpl, googleOAuth2Config, distFS)).Methods("GET", "POST")
router.HandleFunc("/auth/google/callback", handlers.GoogleCallbackHandler(googleOAuth2Config))
router.HandleFunc("/auth/google/callback", handlers.GoogleCallbackHandler(googleOAuth2Config, storeService))

// ADMINISTRATION
adminHandlers := handlers.AdminHandlers{
CssPathGetter: cssService,
Recaptcha: recaptchaService,
Log: log,
}
adminHandlers.InitTmpl(tmpl, distFS)
adminRoute := router.PathPrefix("/admin").Subrouter()
adminRoute.Use(middleware.AuthorizeMiddleware)
adminRoute.HandleFunc("", adminHandlers.DashboardGet)

handleFiles(router, http.FS(workingFolder))

sigs := make(chan os.Signal, 1)
Expand Down Expand Up @@ -161,20 +178,21 @@ func parseConfig(log *slog.Logger) configuration {
return config
}

func prepareServices(log *slog.Logger, fs fs.FS) (*services.CSS, *services.SponsorService, *services.RecaptchaService, *services.CalendarService) {
func prepareServices(log *slog.Logger, fs fs.FS) (*services.CSS, *services.SponsorService, *services.RecaptchaService, *services.CalendarService, sessions.Store) {
cssService := services.NewCSS(fs, serviceLog(log, "css"))
sponsorService := services.NewSponsorService()
recaptchaService := services.NewRecaptchaService(os.Getenv(GOOGLE_API_KEY), os.Getenv(GOOGLE_CAPTCHA_SITE))
calendarService := services.NewCalendarService(os.Getenv(GOOGLE_API_KEY), os.Getenv(GOOGLE_CALENDAR_ID))
return cssService, sponsorService, recaptchaService, calendarService
storeService := sessions.NewCookieStore([]byte(os.Getenv(SESSION_KEY)))
return cssService, sponsorService, recaptchaService, calendarService, storeService
}

func prepareMiddleware(router *mux.Router, log *slog.Logger) {
func prepareMiddleware(router *mux.Router, log *slog.Logger, storeService sessions.Store) {
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.Use(middleware.AuthMiddleware(storeService))
}

func setupPanorama(panoramaPath string, router *mux.Router, log *slog.Logger) {
Expand Down
68 changes: 68 additions & 0 deletions pkg/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package middleware

import (
"context"
"net/http"

"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"jirku.sk/mcmamina/pkg/models"
)

const SessionName = "session"

type UserCookie int

const UserCookieKey UserCookie = 0

func AuthMiddleware(store sessions.Store) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, err := store.Get(r, SessionName)
if err != nil || session.Values["user"] != nil {
if user, ok := session.Values["user"].(models.UserLogin); ok {
ctx = context.WithValue(ctx, UserCookieKey, user)
}
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

func AuthorizeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUser(r)
if user == nil {
// TODO: handle redirect back to original page
http.Redirect(w, r, "/prihlasenie", http.StatusFound)
} else {
next.ServeHTTP(w, r)
}
})
}

func StoreUser(w http.ResponseWriter, r *http.Request, user *models.UserLogin, store sessions.Store) error {
session := sessions.NewSession(store, SessionName)
if user == nil {
session.Values["user"] = nil
} else {
session.Values["user"] = &user
}

session.Options.MaxAge = 60 * 60 * 24 * 10
session.Options.HttpOnly = true
session.Options.Path = "/"
session.Save(r, w)
return nil
}

func GetUser(r *http.Request) *models.UserLogin {
result := r.Context().Value(UserCookieKey)
if result == nil {
return nil
} else if result, ok := result.(models.UserLogin); ok {
return &result
}
return nil
}
11 changes: 11 additions & 0 deletions pkg/models/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package models

type UserLogin struct {
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Picture string `json:"picture"`
EmailVerified bool `json:"email_verified"`
}
13 changes: 13 additions & 0 deletions templates/pages/admin.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{- define "content" -}}
{{- template "layout.start" .}}

{{- template "com_cards.fullwidth.start" dict "m" "mb-0 mt-12"}}
{{- template "com_cards.cardcontent.start" "flex flex-col md:flex-row gap-3 md:gap-4 lg:gap-10 justify-center"}}

<h1 class="text-3xl">Dobrý deň, {{.user.Name}}!</h1>

{{- template "com_cards.cardcontent.end" -}}
{{- template "com_cards.fullwidth.end" -}}

{{- template "layout.end" .}}
{{- end -}}
6 changes: 4 additions & 2 deletions templates/pages/login.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
{{- template "com_cards.fullwidth.start" dict "m" "mb-0 mt-12"}}
{{- template "com_cards.cardcontent.start" "flex flex-col md:flex-row gap-3 md:gap-4 lg:gap-10 justify-center"}}

<div>Prihlásenie</div>
<div class="w-full flex flex-col">
<h1 class="text-4xl pb-4">Prihlásenie</h1>
<form action="/prihlasenie" method="post" id="login-form">
{{ if .errorMsg }}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
Expand All @@ -13,7 +14,7 @@
</div>
{{ end }}
<input type="hidden" name="{{.csrfTokenField}}" value={{.csrfToken}} />
<input type="text" name="username" placeholder="Prihlásenie" value="{{.username}}" />
<input type="text" name="username" placeholder="Prihlásenie" value="{{.username}}" class="w-64" />
<button
type="submit"
class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded{{ if .recaptchaKey}} g-recaptcha{{ end }}"
Expand All @@ -24,6 +25,7 @@
{{ end }}
>Prihlásiť</button>
</form>
</div>
<script>
function onSubmit(token) {
console.log("submitting form");
Expand Down

0 comments on commit e1d0e70

Please sign in to comment.