summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-12-22 14:44:37 +0530
committerBobby <[email protected]>2025-12-22 14:44:37 +0530
commit318360a60aa52cf91ac80d547285f4d14c2c4517 (patch)
treeb5261424716c4ecbe4085dfe914e99223a1a610d
parent32ee8047eba06c9f1c7575b66fc0f9195657ac04 (diff)
downloadlain-318360a60aa52cf91ac80d547285f4d14c2c4517.tar.xz
lain-318360a60aa52cf91ac80d547285f4d14c2c4517.zip
imap client, flash messages, imap login verification
-rw-r--r--controllers/auth.go9
-rw-r--r--go.mod2
-rw-r--r--go.sum9
-rw-r--r--session/functions.go33
-rw-r--r--session/kv.go32
-rw-r--r--session/session.go5
-rw-r--r--types/email.go7
-rw-r--r--utils/email/imap.go39
-rw-r--r--utils/shortcuts/flash.go31
-rw-r--r--utils/shortcuts/helpers.go95
-rw-r--r--utils/shortcuts/redirect.go7
-rw-r--r--utils/shortcuts/render.go24
12 files changed, 238 insertions, 55 deletions
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
}
}