summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.go12
-rw-r--r--config/env.go (renamed from config/types.go)24
-rw-r--r--controllers/login.go17
-rw-r--r--go.mod15
-rw-r--r--go.sum118
-rw-r--r--lain/main.go6
-rw-r--r--middleware/auth.go15
-rw-r--r--middleware/middleware.go7
-rw-r--r--processors/metadata.go18
-rw-r--r--processors/processors.go8
-rw-r--r--processors/request.go12
-rw-r--r--router/auth.go13
-rw-r--r--router/router.go10
-rw-r--r--sessions/session.go40
-rw-r--r--static/css/style.css86
-rw-r--r--tags/tags.go25
-rw-r--r--tags/url.go75
-rw-r--r--templates/auth/login.django37
-rw-r--r--types/http.go28
-rw-r--r--utils/auth/auth.go17
-rw-r--r--utils/crypto/crypto.go71
-rw-r--r--utils/meta/request.go47
-rw-r--r--utils/meta/title.go13
-rw-r--r--utils/shortcuts/helpers.go50
-rw-r--r--utils/shortcuts/redirect.go23
-rw-r--r--utils/shortcuts/render.go38
-rw-r--r--utils/urls/attach.go38
-rw-r--r--utils/urls/namespace.go7
-rw-r--r--utils/urls/path.go51
-rw-r--r--utils/urls/registery.go28
30 files changed, 909 insertions, 40 deletions
diff --git a/config/config.go b/config/config.go
index a3835c5..84729c1 100644
--- a/config/config.go
+++ b/config/config.go
@@ -8,12 +8,12 @@ import (
)
var (
- Server ServerConfig
- MailServer MailServerConfig
- Database DatabaseConfig
- MinIO MinIOConfig
- AIServer AIServerConfig
- Session SessionConfig
+ Server server
+ MailServer mail
+ Database database
+ MinIO minio
+ AIServer ai
+ Session session
)
func init() {
diff --git a/config/types.go b/config/env.go
index c1934ef..2c947d6 100644
--- a/config/types.go
+++ b/config/env.go
@@ -2,15 +2,18 @@ package config
import "time"
-type ServerConfig struct {
+type server struct {
Host string `env:"SERVER_HOST" default:"localhost"`
Port int `env:"SERVER_PORT" default:"8080"`
AppSecret string `env:"APP_SECRET" default:"mysecret"`
+ AppName string `env:"APP_NAME" default:"Lain Mail"`
+ AppDescription string `env:"APP_DESCRIPTION" default:"Present day, present time!"`
+ AppEngine string `env:"APP_ENGINE" default:"Lain"`
AllowedDomains []string `env:"ALLOWED_DOMAINS" default:"localhost"`
DevMode bool `env:"DEV_MODE" default:"true"`
}
-type MailServerConfig struct {
+type mail struct {
IMAPHost string `env:"IMAP_HOST" default:""`
IMAPPort int `env:"IMAP_PORT" default:"993"`
IMAPTLS bool `env:"IMAP_TLS" default:"true"`
@@ -19,7 +22,7 @@ type MailServerConfig struct {
SMTPTLS bool `env:"SMTP_TLS" default:"true"`
}
-type DatabaseConfig struct {
+type database struct {
Host string `env:"DB_HOST" default:"localhost"`
Port int `env:"DB_PORT" default:"5432"`
Username string `env:"DB_USER" default:"postgres"`
@@ -28,7 +31,7 @@ type DatabaseConfig struct {
SSLMode string `env:"DB_SSLMODE" default:"disable"`
}
-type MinIOConfig struct {
+type minio struct {
Endpoint string `env:"MINIO_ENDPOINT" default:"localhost:9000"`
AccessKey string `env:"MINIO_ACCESS_KEY" default:""`
SecretKey string `env:"MINIO_SECRET_KEY" default:""`
@@ -36,13 +39,16 @@ type MinIOConfig struct {
UseSSL bool `env:"MINIO_USE_SSL" default:"false"`
}
-type AIServerConfig struct {
+type ai struct {
URL string `env:"AI_SERVER_URL" default:""`
AuthKey string `env:"AI_SERVER_AUTH_KEY" default:""`
}
-type SessionConfig struct {
- CookieName string `env:"SESSION_COOKIE_NAME" default:"lain_session"`
- Timeout time.Duration `env:"SESSION_TIMEOUT" default:"24h"`
- SecureCookie bool `env:"SESSION_SECURE_COOKIE" default:"false"`
+type session struct {
+ CookieDomain string `env:"SESSION_COOKIE_DOMAIN" default:"localhost"`
+ CookieName string `env:"SESSION_COOKIE_NAME" default:"lain_session"`
+ CookiePath string `env:"SESSION_COOKIE_PATH" default:"/"`
+ CookieSameSite string `env:"SESSION_COOKIE_SAME_SITE" default:"Lax"`
+ CookieSecure bool `env:"SESSION_SECURE_COOKIE" default:"false"`
+ CookieTimeout time.Duration `env:"SESSION_TIMEOUT" default:"24h"`
}
diff --git a/controllers/login.go b/controllers/login.go
new file mode 100644
index 0000000..2690538
--- /dev/null
+++ b/controllers/login.go
@@ -0,0 +1,17 @@
+package controllers
+
+import (
+ "lain/config"
+ "lain/utils/meta"
+ "lain/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func LoginPage(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "Login")
+
+ return shortcuts.Render(context, "auth/login", fiber.Map{
+ "AllowedDomains": config.Server.AllowedDomains,
+ })
+}
diff --git a/go.mod b/go.mod
index 2a83fca..fc976a5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,9 @@ module lain
go 1.25.5
require (
+ 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
github.com/gofiber/template/django/v3 v3.1.14
github.com/joho/godotenv v1.5.1
gorm.io/datatypes v1.2.7
@@ -14,18 +16,17 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
- github.com/flosch/pongo2/v6 v6.0.0 // 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
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
- github.com/jackc/pgx/v5 v5.6.0 // indirect
+ github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
- github.com/klauspost/compress v1.17.9 // indirect
+ github.com/klauspost/compress v1.18.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -34,9 +35,9 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
- golang.org/x/crypto v0.31.0 // indirect
- golang.org/x/sync v0.10.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/text v0.21.0 // indirect
+ golang.org/x/crypto v0.45.0 // indirect
+ golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
+ golang.org/x/text v0.31.0 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
)
diff --git a/go.sum b/go.sum
index 1dc056b..fd12304 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,55 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
+github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+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/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=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/gofiber/storage/postgres/v3 v3.3.1 h1:W/Z88/o63O6VIYztcMF6yLPzVV6iFv4PLRghFpK0WNE=
+github.com/gofiber/storage/postgres/v3 v3.3.1/go.mod h1:JTspuhSuWCrR5pWdULaAyez0zKSfRz/jKzd4cSWz004=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/django/v3 v3.1.14 h1:SvTvs+u5vTZuu1Y2pMUD2NhaGIjBj9FmDA3XD50QBvw=
@@ -28,8 +66,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
-github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
+github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
+github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -38,12 +76,16 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
+github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
+github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -55,33 +97,81 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
+github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
+github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
+github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
+github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ=
+github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
+github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
+github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
+github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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/lain/main.go b/lain/main.go
index e179d10..a1606b8 100644
--- a/lain/main.go
+++ b/lain/main.go
@@ -3,7 +3,10 @@ package main
import (
"fmt"
"lain/config"
+ "lain/middleware"
+ "lain/processors"
"lain/router"
+ "lain/tags"
"lain/utils/env"
"log"
@@ -20,6 +23,7 @@ func main() {
log.Println("Warning: AppSecret is set to a default value which is not secure. Please set a strong random secret in your APP_SECRET environment variable or .env file.")
}
+ tags.Initialize()
engine := django.New("./templates", ".django")
engine.Reload(config.Server.DevMode)
app := fiber.New(fiber.Config{
@@ -36,7 +40,9 @@ func main() {
}))
app.Use(cors.New())
+ processors.Initialize(app)
router.Initialize(app)
+ middleware.Initialize(app)
address := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)
log.Printf("Starting server at %s\n", address)
diff --git a/middleware/auth.go b/middleware/auth.go
new file mode 100644
index 0000000..baa67fb
--- /dev/null
+++ b/middleware/auth.go
@@ -0,0 +1,15 @@
+package middleware
+
+import (
+ "lain/utils/auth"
+ "lain/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func authentication(context *fiber.Ctx) error {
+ if !auth.IsAuthenticated(context) {
+ return shortcuts.Redirect(context, "auth.login")
+ }
+ return context.Next()
+}
diff --git a/middleware/middleware.go b/middleware/middleware.go
new file mode 100644
index 0000000..c8749bb
--- /dev/null
+++ b/middleware/middleware.go
@@ -0,0 +1,7 @@
+package middleware
+
+import "github.com/gofiber/fiber/v2"
+
+func Initialize(app *fiber.App) {
+ app.Use(authentication)
+}
diff --git a/processors/metadata.go b/processors/metadata.go
new file mode 100644
index 0000000..96fcdb2
--- /dev/null
+++ b/processors/metadata.go
@@ -0,0 +1,18 @@
+package processors
+
+import (
+ "lain/config"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+const defaultTitle = "Lain | Present day, present time!"
+
+func metadata(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", defaultTitle)
+ ctx.Locals("AppName", config.Server.AppName)
+ ctx.Locals("AppDescription", config.Server.AppDescription)
+ ctx.Locals("AppEngine", config.Server.AppEngine)
+
+ return ctx.Next()
+}
diff --git a/processors/processors.go b/processors/processors.go
new file mode 100644
index 0000000..d56cbde
--- /dev/null
+++ b/processors/processors.go
@@ -0,0 +1,8 @@
+package processors
+
+import "github.com/gofiber/fiber/v2"
+
+func Initialize(app *fiber.App) {
+ app.Use(metadata)
+ app.Use(request)
+}
diff --git a/processors/request.go b/processors/request.go
new file mode 100644
index 0000000..4372693
--- /dev/null
+++ b/processors/request.go
@@ -0,0 +1,12 @@
+package processors
+
+import (
+ "lain/utils/meta"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func request(ctx *fiber.Ctx) error {
+ ctx.Locals("Request", meta.BuildRequest(ctx))
+ return ctx.Next()
+}
diff --git a/router/auth.go b/router/auth.go
new file mode 100644
index 0000000..8c13e37
--- /dev/null
+++ b/router/auth.go
@@ -0,0 +1,13 @@
+package router
+
+import (
+ "lain/controllers"
+ "lain/types"
+ "lain/utils/urls"
+)
+
+func init() {
+ urls.SetNamespace("auth")
+
+ urls.Path(types.GET, "/login", controllers.LoginPage, "login")
+}
diff --git a/router/router.go b/router/router.go
index 11015f9..e5c9303 100644
--- a/router/router.go
+++ b/router/router.go
@@ -1,11 +1,13 @@
package router
-import "github.com/gofiber/fiber/v2"
+import (
+ "lain/utils/urls"
+
+ "github.com/gofiber/fiber/v2"
+)
func Initialize(router *fiber.App) {
router.Static("/static", "./static")
- router.Get("/", func(c *fiber.Ctx) error {
- return c.SendString("Lain Mail - Present day, present time")
- })
+ urls.Attach(router)
}
diff --git a/sessions/session.go b/sessions/session.go
new file mode 100644
index 0000000..c24817a
--- /dev/null
+++ b/sessions/session.go
@@ -0,0 +1,40 @@
+package session
+
+import (
+ "fmt"
+ "lain/config"
+ "log"
+ "time"
+
+ "github.com/gofiber/fiber/v2/middleware/session"
+ "github.com/gofiber/storage/postgres/v3"
+)
+
+var Store *session.Store
+
+func init() {
+ storage := postgres.New(postgres.Config{
+ Host: config.Database.Host,
+ Port: config.Database.Port,
+ Username: config.Database.Username,
+ Password: config.Database.Password,
+ Database: config.Database.Name,
+ Table: config.Session.CookieName,
+ Reset: false,
+ SSLMode: config.Database.SSLMode,
+ GCInterval: 10 * time.Second,
+ })
+
+ Store = session.New(session.Config{
+ Storage: storage,
+ Expiration: config.Session.CookieTimeout,
+ KeyLookup: fmt.Sprintf("cookie:%s", config.Session.CookieName),
+ CookieDomain: config.Session.CookieDomain,
+ CookiePath: config.Session.CookiePath,
+ CookieSecure: config.Session.CookieSecure,
+ CookieSameSite: config.Session.CookieSameSite,
+ CookieHTTPOnly: true,
+ })
+
+ log.Println("session storage initialized")
+}
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..683a620
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,86 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Verdana', sans-serif;
+ background: #fff5f8;
+}
+
+.login-page {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+}
+
+.login-container {
+ background: white;
+ padding: 2rem;
+ border: 2px solid #ffccee;
+ border-radius: 8px;
+ max-width: 400px;
+ width: 100%;
+}
+
+.login-container h1 {
+ color: #663366;
+ text-align: center;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ text-align: center;
+ color: #ff99cc;
+ margin-bottom: 2rem;
+ font-style: italic;
+}
+
+.error {
+ background: #ffcccc;
+ color: #cc0000;
+ padding: 0.75rem;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
+
+.field {
+ margin-bottom: 1rem;
+}
+
+.field label {
+ display: block;
+ color: #663366;
+ margin-bottom: 0.25rem;
+}
+
+.field input {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid #ffccee;
+ border-radius: 4px;
+}
+
+button {
+ width: 100%;
+ padding: 0.75rem;
+ background: #ff99cc;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+}
+
+button:hover {
+ background: #ff66aa;
+}
+
+footer {
+ margin-top: 2rem;
+ text-align: center;
+ font-size: 0.75rem;
+ color: #999;
+} \ No newline at end of file
diff --git a/tags/tags.go b/tags/tags.go
new file mode 100644
index 0000000..9a20b55
--- /dev/null
+++ b/tags/tags.go
@@ -0,0 +1,25 @@
+package tags
+
+import (
+ "log"
+
+ "github.com/flosch/pongo2/v6"
+)
+
+type templateTag struct {
+ Name string
+ Fn pongo2.TagParser
+}
+
+func Initialize() {
+
+ tags := []templateTag{
+ {"url", url},
+ }
+
+ for _, t := range tags {
+ if err := pongo2.RegisterTag(t.Name, t.Fn); err != nil {
+ log.Println("Failed to register tag:", t.Name, "Error:", err)
+ }
+ }
+}
diff --git a/tags/url.go b/tags/url.go
new file mode 100644
index 0000000..194d00b
--- /dev/null
+++ b/tags/url.go
@@ -0,0 +1,75 @@
+package tags
+
+import (
+ "fmt"
+ "lain/utils/urls"
+ "strings"
+
+ "github.com/flosch/pongo2/v6"
+)
+
+type urlNode struct {
+ routeName string
+ params map[string]pongo2.IEvaluator
+}
+
+func url(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) {
+ routeNameToken := arguments.MatchType(pongo2.TokenString)
+ if routeNameToken == nil {
+ return nil, arguments.Error("expected route name string", nil)
+ }
+ routeName := routeNameToken.Val
+
+ params := make(map[string]pongo2.IEvaluator)
+
+ for arguments.Remaining() > 0 {
+ keyToken := arguments.MatchType(pongo2.TokenIdentifier)
+ if keyToken == nil {
+ return nil, arguments.Error("expected param key identifier", nil)
+ }
+
+ if arguments.Match(pongo2.TokenSymbol, "=") == nil {
+ return nil, arguments.Error("expected '=' after param key", nil)
+ }
+
+ valueExpr, err := arguments.ParseExpression()
+ if err != nil {
+ return nil, err
+ }
+
+ params[keyToken.Val] = valueExpr
+ }
+
+ return &urlNode{
+ routeName: routeName,
+ params: params,
+ }, nil
+}
+
+func (n *urlNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) *pongo2.Error {
+ path, ok := urls.GetFullPath(n.routeName)
+ if !ok {
+ return &pongo2.Error{
+ Sender: "tag:url",
+ OrigError: fmt.Errorf("route not found: %s", n.routeName),
+ }
+ }
+
+ for key, expr := range n.params {
+ val, err := expr.Evaluate(ctx)
+ if err != nil {
+ return err
+ }
+ path = strings.ReplaceAll(path, ":"+key, fmt.Sprintf("%v", val.Interface()))
+ }
+
+ _, err := writer.WriteString(path)
+ if err != nil {
+ return &pongo2.Error{
+ Sender: "tag:url",
+ OrigError: err,
+ }
+ }
+
+ return nil
+}
diff --git a/templates/auth/login.django b/templates/auth/login.django
new file mode 100644
index 0000000..0fbdceb
--- /dev/null
+++ b/templates/auth/login.django
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>{{ Title }}</title>
+ <link rel="stylesheet" href="/static/css/style.css" />
+ </head>
+ <body class="login-page">
+ <div class="login-container">
+ <h1>{{ AppName }}</h1>
+ <p class="subtitle">{{ AppDescription }}</p>
+
+ {% if Error %}
+ <div class="error">{{ Error }}</div>
+ {% endif %}
+
+ <form method="POST" action="{% url 'auth.login' %}">
+ <div class="field">
+ <label>Email</label>
+ <input type="email" name="email" required autofocus />
+ </div>
+
+ <div class="field">
+ <label>Password</label>
+ <input type="password" name="password" required />
+ </div>
+
+ <button type="submit">Login</button>
+ </form>
+
+ <footer>
+ {{ AppName }} - Powered by {{ AppEngine }} - &copy; <a href="https://shi.foo" target="_blank" rel="noopener noreferrer">shi.foo</a> 2025
+ </footer>
+ </div>
+ </body>
+</html>
diff --git a/types/http.go b/types/http.go
new file mode 100644
index 0000000..5fac0e8
--- /dev/null
+++ b/types/http.go
@@ -0,0 +1,28 @@
+package types
+
+type HTTPMethod string
+
+const (
+ GET HTTPMethod = "GET"
+ POST HTTPMethod = "POST"
+ PUT HTTPMethod = "PUT"
+ PATCH HTTPMethod = "PATCH"
+ DELETE HTTPMethod = "DELETE"
+ OPTIONS HTTPMethod = "OPTIONS"
+ HEAD HTTPMethod = "HEAD"
+)
+
+type HTTPQueryParam struct {
+ Key string
+ Value string
+}
+
+type HTTPRequest struct {
+ Path string
+ Method string
+ Query []HTTPQueryParam
+ Params []HTTPQueryParam
+ QueryString string
+ IP string
+ URL string
+}
diff --git a/utils/auth/auth.go b/utils/auth/auth.go
new file mode 100644
index 0000000..3a45ac3
--- /dev/null
+++ b/utils/auth/auth.go
@@ -0,0 +1,17 @@
+package auth
+
+import (
+ session "lain/sessions"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func IsAuthenticated(context *fiber.Ctx) bool {
+ session, err := session.Store.Get(context)
+ if err != nil {
+ return false
+ }
+
+ email := session.Get("email")
+ return email != nil
+}
diff --git a/utils/crypto/crypto.go b/utils/crypto/crypto.go
new file mode 100644
index 0000000..54d2eec
--- /dev/null
+++ b/utils/crypto/crypto.go
@@ -0,0 +1,71 @@
+package crypto
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "lain/config"
+)
+
+func getKey() []byte {
+ hash := sha256.Sum256([]byte(config.Server.AppSecret))
+ return hash[:]
+}
+
+func Encrypt(plaintext string) (string, error) {
+ key := getKey()
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return "", err
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+
+ nonce := make([]byte, gcm.NonceSize())
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ return "", err
+ }
+
+ ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
+ return base64.URLEncoding.EncodeToString(ciphertext), nil
+}
+
+func Decrypt(ciphertext string) (string, error) {
+ key := getKey()
+
+ ciphertextBytes, err := base64.URLEncoding.DecodeString(ciphertext)
+ if err != nil {
+ return "", err
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return "", err
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+
+ nonceSize := gcm.NonceSize()
+ if len(ciphertextBytes) < nonceSize {
+ return "", fmt.Errorf("ciphertext too short")
+ }
+
+ nonce, ciphertextBytes := ciphertextBytes[:nonceSize], ciphertextBytes[nonceSize:]
+ plaintextBytes, err := gcm.Open(nil, nonce, ciphertextBytes, nil)
+ if err != nil {
+ return "", err
+ }
+
+ return string(plaintextBytes), nil
+}
diff --git a/utils/meta/request.go b/utils/meta/request.go
new file mode 100644
index 0000000..e1db77c
--- /dev/null
+++ b/utils/meta/request.go
@@ -0,0 +1,47 @@
+package meta
+
+import (
+ "lain/types"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func BuildRequest(context *fiber.Ctx) types.HTTPRequest {
+ return types.HTTPRequest{
+ Path: context.Path(),
+ Method: context.Method(),
+ Query: buildQueryParams(context),
+ Params: buildRouteParams(context),
+ QueryString: string(context.Request().URI().QueryString()),
+ IP: context.IP(),
+ URL: context.OriginalURL(),
+ }
+}
+
+func buildQueryParams(context *fiber.Ctx) []types.HTTPQueryParam {
+ params := make([]types.HTTPQueryParam, 0)
+ args := context.Request().URI().QueryArgs()
+
+ args.VisitAll(transformQueryParam(&params))
+ return params
+}
+
+func buildRouteParams(context *fiber.Ctx) []types.HTTPQueryParam {
+ params := make([]types.HTTPQueryParam, 0)
+ for key, value := range context.AllParams() {
+ params = append(params, types.HTTPQueryParam{
+ Key: key,
+ Value: value,
+ })
+ }
+ return params
+}
+
+func transformQueryParam(params *[]types.HTTPQueryParam) func(key, value []byte) {
+ return func(key, value []byte) {
+ *params = append(*params, types.HTTPQueryParam{
+ Key: string(key),
+ Value: string(value),
+ })
+ }
+}
diff --git a/utils/meta/title.go b/utils/meta/title.go
new file mode 100644
index 0000000..27ee2ed
--- /dev/null
+++ b/utils/meta/title.go
@@ -0,0 +1,13 @@
+package meta
+
+import (
+ "fmt"
+ "lain/config"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func SetPageTitle(context *fiber.Ctx, title string) {
+ title = fmt.Sprintf("%s | %s", title, config.Server.AppName)
+ context.Locals("Title", title)
+}
diff --git a/utils/shortcuts/helpers.go b/utils/shortcuts/helpers.go
new file mode 100644
index 0000000..8503a2d
--- /dev/null
+++ b/utils/shortcuts/helpers.go
@@ -0,0 +1,50 @@
+package shortcuts
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+func structValue(data any) (reflect.Value, error) {
+ v := reflect.ValueOf(data)
+ if v.Kind() == reflect.Pointer {
+ v = v.Elem()
+ }
+
+ if v.Kind() != reflect.Struct {
+ return reflect.Value{}, fmt.Errorf(
+ "Render: unsupported bind type %T; must be struct or *struct",
+ data,
+ )
+ }
+
+ return v, nil
+}
+
+func mapStruct(v reflect.Value) map[string]any {
+ t := v.Type()
+ result := make(map[string]any, v.NumField())
+
+ for i := 0; i < v.NumField(); i++ {
+ fieldType := t.Field(i)
+ if !fieldType.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[key] = v.Field(i).Interface()
+ }
+
+ return result
+}
diff --git a/utils/shortcuts/redirect.go b/utils/shortcuts/redirect.go
new file mode 100644
index 0000000..aa760dc
--- /dev/null
+++ b/utils/shortcuts/redirect.go
@@ -0,0 +1,23 @@
+package shortcuts
+
+import (
+ "lain/utils/urls"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Redirect(ctx *fiber.Ctx, routeName string) error {
+ path, ok := urls.GetFullPath(routeName)
+ if !ok {
+ return fiber.ErrNotFound
+ }
+ return ctx.Redirect(path)
+}
+
+func RedirectWithStatus(ctx *fiber.Ctx, routeName string, statusCode int) error {
+ path, ok := urls.GetFullPath(routeName)
+ if !ok {
+ return fiber.ErrNotFound
+ }
+ return ctx.Redirect(path, statusCode)
+}
diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go
new file mode 100644
index 0000000..1efeb61
--- /dev/null
+++ b/utils/shortcuts/render.go
@@ -0,0 +1,38 @@
+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 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))
+ }
+ }
+
+ return ctx.Render(template, bind)
+}
+
+func RenderWithStatus(ctx *fiber.Ctx, template string, data any, statusCode int) error {
+ ctx.Status(statusCode)
+ return Render(ctx, template, data)
+}
diff --git a/utils/urls/attach.go b/utils/urls/attach.go
new file mode 100644
index 0000000..70c6db9
--- /dev/null
+++ b/utils/urls/attach.go
@@ -0,0 +1,38 @@
+package urls
+
+import (
+ "lain/types"
+ "log"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+var methodBinders = map[types.HTTPMethod]func(fiber.Router, string, fiber.Handler) fiber.Router{
+ types.GET: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Get(path, h) },
+ types.POST: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Post(path, h) },
+ types.PUT: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Put(path, h) },
+ types.PATCH: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Patch(path, h) },
+ types.DELETE: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Delete(path, h) },
+ types.OPTIONS: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Options(path, h) },
+ types.HEAD: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Head(path, h) },
+}
+
+func Attach(app *fiber.App) {
+ namespaceGroups := make(map[string]fiber.Router)
+
+ for fullName, route := range registry.routes {
+ group, exists := namespaceGroups[route.namespace]
+ if !exists {
+ group = app.Group("/" + route.namespace)
+ namespaceGroups[route.namespace] = group
+ }
+
+ binder, ok := methodBinders[route.method]
+ if !ok {
+ log.Fatalf("%s", "unsupported HTTP method: "+string(route.method))
+ }
+
+ fiberRoute := binder(group, route.path, route.handler)
+ fiberRoute.Name(fullName)
+ }
+}
diff --git a/utils/urls/namespace.go b/utils/urls/namespace.go
new file mode 100644
index 0000000..7bb5311
--- /dev/null
+++ b/utils/urls/namespace.go
@@ -0,0 +1,7 @@
+package urls
+
+func SetNamespace(namespace string) {
+ registry.mutex.Lock()
+ defer registry.mutex.Unlock()
+ registry.currentNamespace = namespace
+}
diff --git a/utils/urls/path.go b/utils/urls/path.go
new file mode 100644
index 0000000..e6d4ed7
--- /dev/null
+++ b/utils/urls/path.go
@@ -0,0 +1,51 @@
+package urls
+
+import (
+ "lain/types"
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Path(method types.HTTPMethod, path string, handler fiber.Handler, name string) {
+ registry.mutex.Lock()
+ defer registry.mutex.Unlock()
+
+ namespace := registry.currentNamespace
+ fullName := name
+ fullPath := path
+
+ if namespace != "" {
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+
+ fullName = namespace + "." + name
+ fullPath = "/" + namespace + path
+ } else {
+ if !strings.HasPrefix(fullPath, "/") {
+ fullPath = "/" + fullPath
+ }
+ }
+
+ registry.routes[fullName] = registeredRoute{
+ method: method,
+ path: path,
+ handler: handler,
+ namespace: namespace,
+ name: name,
+ fullPath: fullPath,
+ }
+}
+
+func GetFullPath(routeName string) (string, bool) {
+ registry.mutex.Lock()
+ defer registry.mutex.Unlock()
+
+ route, ok := registry.routes[routeName]
+ if !ok {
+ return "", false
+ }
+
+ return route.fullPath, true
+}
diff --git a/utils/urls/registery.go b/utils/urls/registery.go
new file mode 100644
index 0000000..d1bff93
--- /dev/null
+++ b/utils/urls/registery.go
@@ -0,0 +1,28 @@
+package urls
+
+import (
+ "sync"
+
+ "lain/types"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+type registeredRoute struct {
+ method types.HTTPMethod
+ path string
+ handler fiber.Handler
+ namespace string
+ name string
+ fullPath string
+}
+
+type routeRegistry struct {
+ mutex sync.Mutex
+ currentNamespace string
+ routes map[string]registeredRoute
+}
+
+var registry = &routeRegistry{
+ routes: make(map[string]registeredRoute),
+}