From 318360a60aa52cf91ac80d547285f4d14c2c4517 Mon Sep 17 00:00:00 2001 From: Bobby <30593201+luciferreeves@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:44:37 +0530 Subject: imap client, flash messages, imap login verification --- controllers/auth.go | 9 +++++ go.mod | 2 + go.sum | 9 +++++ session/functions.go | 33 ++++++---------- session/kv.go | 32 +++++++++++++++ session/session.go | 5 +++ types/email.go | 7 ++++ utils/email/imap.go | 39 +++++++++++++++++++ utils/shortcuts/flash.go | 31 +++++++++++++++ utils/shortcuts/helpers.go | 95 +++++++++++++++++++++++++++++++++++++-------- utils/shortcuts/redirect.go | 7 ++++ utils/shortcuts/render.go | 24 ++++-------- 12 files changed, 238 insertions(+), 55 deletions(-) create mode 100644 session/kv.go create mode 100644 types/email.go create mode 100644 utils/email/imap.go create mode 100644 utils/shortcuts/flash.go diff --git a/controllers/auth.go b/controllers/auth.go index 3351058..f945d8d 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -6,6 +6,7 @@ import ( "lain/session" "lain/types" "lain/utils/crypto" + "lain/utils/email" "lain/utils/meta" "lain/utils/shortcuts" @@ -26,6 +27,14 @@ func Login(context *fiber.Ctx) error { return BadRequest(context, err) } + imapClient, err := email.ConnectIMAP(formData.Email, formData.Password) + if err != nil { + return shortcuts.RedirectWithFlash(context, "auth.login", fiber.Map{ + "Error": "Invalid email or password.", + }) + } + imapClient.Close() + encryptedPassword, err := crypto.Encrypt(formData.Password) if err != nil { return InternalServerError(context, err) diff --git a/go.mod b/go.mod index fc976a5..6b693e1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module lain go 1.25.5 require ( + github.com/emersion/go-imap v1.2.1 github.com/flosch/pongo2/v6 v6.0.0 github.com/gofiber/fiber/v2 v2.52.10 github.com/gofiber/storage/postgres/v3 v3.3.1 @@ -16,6 +17,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect diff --git a/go.sum b/go.sum index fd12304..f3f7658 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,12 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= +github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= +github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= @@ -170,8 +176,11 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/session/functions.go b/session/functions.go index 589091f..776fddd 100644 --- a/session/functions.go +++ b/session/functions.go @@ -2,35 +2,26 @@ package session import "github.com/gofiber/fiber/v2" -func CreateSession(context *fiber.Ctx, email string) error { - sess, err := Store.Get(context) - if err != nil { - return err - } +const emailKey = "email" - sess.Set("email", email) - return sess.Save() +func CreateSession(ctx *fiber.Ctx, email string) error { + return Set(ctx, emailKey, email) } -func DestroySession(context *fiber.Ctx) error { - sess, err := Store.Get(context) - if err != nil { - return err - } - - return sess.Destroy() +func DestroySession(ctx *fiber.Ctx) error { + return Delete(ctx, emailKey) } -func GetSessionEmail(context *fiber.Ctx) (string, error) { - sess, err := Store.Get(context) - if err != nil { +func GetSessionEmail(ctx *fiber.Ctx) (string, error) { + value, err := Get(ctx, emailKey) + if err != nil || value == nil { return "", err } - email := sess.Get("email") - if emailStr, ok := email.(string); ok { - return emailStr, nil + email, ok := value.(string) + if !ok { + return "", nil } - return "", nil + return email, nil } diff --git a/session/kv.go b/session/kv.go new file mode 100644 index 0000000..eecd4c5 --- /dev/null +++ b/session/kv.go @@ -0,0 +1,32 @@ +package session + +import "github.com/gofiber/fiber/v2" + +func Set(ctx *fiber.Ctx, key string, value any) error { + sess, err := Store.Get(ctx) + if err != nil { + return err + } + + sess.Set(key, value) + return sess.Save() +} + +func Get(ctx *fiber.Ctx, key string) (any, error) { + sess, err := Store.Get(ctx) + if err != nil { + return nil, err + } + + return sess.Get(key), nil +} + +func Delete(ctx *fiber.Ctx, key string) error { + sess, err := Store.Get(ctx) + if err != nil { + return err + } + + sess.Delete(key) + return sess.Save() +} diff --git a/session/session.go b/session/session.go index c24817a..c90c810 100644 --- a/session/session.go +++ b/session/session.go @@ -1,11 +1,13 @@ package session import ( + "encoding/gob" "fmt" "lain/config" "log" "time" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" "github.com/gofiber/storage/postgres/v3" ) @@ -13,6 +15,9 @@ import ( var Store *session.Store func init() { + gob.Register(fiber.Map{}) + log.Println("gob: registered fiber.Map for session storage") + storage := postgres.New(postgres.Config{ Host: config.Database.Host, Port: config.Database.Port, diff --git a/types/email.go b/types/email.go new file mode 100644 index 0000000..aaf3376 --- /dev/null +++ b/types/email.go @@ -0,0 +1,7 @@ +package types + +import "github.com/emersion/go-imap/client" + +type EmailClient struct { + *client.Client +} diff --git a/utils/email/imap.go b/utils/email/imap.go new file mode 100644 index 0000000..69181b3 --- /dev/null +++ b/utils/email/imap.go @@ -0,0 +1,39 @@ +package email + +import ( + "crypto/tls" + "fmt" + "lain/config" + "lain/types" + + "github.com/emersion/go-imap/client" +) + +func ConnectIMAP(email, password string) (*types.EmailClient, error) { + address := fmt.Sprintf("%s:%d", config.MailServer.IMAPHost, config.MailServer.IMAPPort) + + var c *client.Client + var err error + + if config.MailServer.IMAPTLS { + c, err = client.DialTLS(address, &tls.Config{ + ServerName: config.MailServer.IMAPHost, + }) + } else { + c, err = client.Dial(address) + } + + if err != nil { + return nil, fmt.Errorf("failed to connect to IMAP server: %w", err) + } + + if err := c.Login(email, password); err != nil { + return nil, fmt.Errorf("invalid credentials: %w", err) + } + + return &types.EmailClient{Client: c}, nil +} + +func DisconnectIMAP(c *types.EmailClient) error { + return c.Logout() +} diff --git a/utils/shortcuts/flash.go b/utils/shortcuts/flash.go new file mode 100644 index 0000000..da1507a --- /dev/null +++ b/utils/shortcuts/flash.go @@ -0,0 +1,31 @@ +package shortcuts + +import ( + "lain/session" + + "github.com/gofiber/fiber/v2" +) + +const flashKey = "__flash__" + +func Flash(ctx *fiber.Ctx, data any) error { + normalized, err := normalizeBind(data) + if err != nil { + return err + } + + return session.Set(ctx, flashKey, normalized) +} + +func ConsumeFlash(ctx *fiber.Ctx) (any, error) { + value, err := session.Get(ctx, flashKey) + if err != nil || value == nil { + return nil, err + } + + if err := session.Delete(ctx, flashKey); err != nil { + return nil, err + } + + return value, nil +} diff --git a/utils/shortcuts/helpers.go b/utils/shortcuts/helpers.go index 8503a2d..5ffd43e 100644 --- a/utils/shortcuts/helpers.go +++ b/utils/shortcuts/helpers.go @@ -2,8 +2,11 @@ package shortcuts import ( "fmt" + "maps" "reflect" "strings" + + "github.com/gofiber/fiber/v2" ) func structValue(data any) (reflect.Value, error) { @@ -13,10 +16,7 @@ func structValue(data any) (reflect.Value, error) { } if v.Kind() != reflect.Struct { - return reflect.Value{}, fmt.Errorf( - "Render: unsupported bind type %T; must be struct or *struct", - data, - ) + return reflect.Value{}, fmt.Errorf("unsupported bind type %T; must be struct or *struct", data) } return v, nil @@ -27,24 +27,85 @@ func mapStruct(v reflect.Value) map[string]any { result := make(map[string]any, v.NumField()) for i := 0; i < v.NumField(); i++ { - fieldType := t.Field(i) - if !fieldType.IsExported() { + field := t.Field(i) + if !field.IsExported() { continue } - key := fieldType.Name - if tag := fieldType.Tag.Get("json"); tag != "" && tag != "-" { - if idx := strings.IndexByte(tag, ','); idx >= 0 { - if idx > 0 { - key = tag[:idx] - } - } else { - key = tag - } + result[getFieldKey(field)] = v.Field(i).Interface() + } + + return result +} + +func getFieldKey(field reflect.StructField) string { + key := field.Name + tag := field.Tag.Get("json") + + if tag == "" || tag == "-" { + return key + } + + if idx := strings.IndexByte(tag, ','); idx >= 0 { + if idx > 0 { + return tag[:idx] } + return key + } + + return tag +} - result[key] = v.Field(i).Interface() +func normalizeBind(data any) (fiber.Map, error) { + if data == nil { + return nil, nil } - return result + switch v := data.(type) { + case fiber.Map: + return v, nil + case map[string]any: + return fiber.Map(v), nil + default: + return structToMap(v) + } +} + +func structToMap(data any) (fiber.Map, error) { + v, err := structValue(data) + if err != nil { + return nil, err + } + return fiber.Map(mapStruct(v)), nil +} + +func mergeFlash(ctx *fiber.Ctx, bind fiber.Map) error { + flash, err := ConsumeFlash(ctx) + if err != nil || flash == nil { + return err + } + + flashMap, err := normalizeBind(flash) + if err != nil { + return err + } + + maps.Copy(bind, flashMap) + return nil +} + +func mergeUserValues(ctx *fiber.Ctx, bind fiber.Map) { + ctx.Context().VisitUserValues(func(key []byte, value any) { + bind[string(key)] = value + }) +} + +func mergeData(bind fiber.Map, data any) error { + dataMap, err := normalizeBind(data) + if err != nil { + return err + } + + maps.Copy(bind, dataMap) + return nil } diff --git a/utils/shortcuts/redirect.go b/utils/shortcuts/redirect.go index 77e1959..1add12f 100644 --- a/utils/shortcuts/redirect.go +++ b/utils/shortcuts/redirect.go @@ -27,3 +27,10 @@ func RedirectTo(route string) fiber.Handler { return Redirect(ctx, route) } } + +func RedirectWithFlash(context *fiber.Ctx, routeName string, data fiber.Map) error { + if err := Flash(context, data); err != nil { + return err + } + return Redirect(context, routeName) +} diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go index 1efeb61..7862b91 100644 --- a/utils/shortcuts/render.go +++ b/utils/shortcuts/render.go @@ -1,31 +1,21 @@ package shortcuts import ( - "maps" - "github.com/gofiber/fiber/v2" ) func Render(ctx *fiber.Ctx, template string, data any) error { bind := make(fiber.Map) - ctx.Context().VisitUserValues(func(key []byte, value any) { - bind[string(key)] = value - }) + if err := mergeFlash(ctx, bind); err != nil { + return err + } + + mergeUserValues(ctx, bind) if data != nil { - switch v := data.(type) { - case map[string]any: - maps.Copy(bind, v) - case fiber.Map: - maps.Copy(bind, v) - default: - rv, err := structValue(data) - if err != nil { - return err - } - - maps.Copy(bind, mapStruct(rv)) + if err := mergeData(bind, data); err != nil { + return err } } -- cgit v1.2.3