diff options
| -rw-r--r-- | config/config.go | 12 | ||||
| -rw-r--r-- | config/env.go (renamed from config/types.go) | 24 | ||||
| -rw-r--r-- | controllers/login.go | 17 | ||||
| -rw-r--r-- | go.mod | 15 | ||||
| -rw-r--r-- | go.sum | 118 | ||||
| -rw-r--r-- | lain/main.go | 6 | ||||
| -rw-r--r-- | middleware/auth.go | 15 | ||||
| -rw-r--r-- | middleware/middleware.go | 7 | ||||
| -rw-r--r-- | processors/metadata.go | 18 | ||||
| -rw-r--r-- | processors/processors.go | 8 | ||||
| -rw-r--r-- | processors/request.go | 12 | ||||
| -rw-r--r-- | router/auth.go | 13 | ||||
| -rw-r--r-- | router/router.go | 10 | ||||
| -rw-r--r-- | sessions/session.go | 40 | ||||
| -rw-r--r-- | static/css/style.css | 86 | ||||
| -rw-r--r-- | tags/tags.go | 25 | ||||
| -rw-r--r-- | tags/url.go | 75 | ||||
| -rw-r--r-- | templates/auth/login.django | 37 | ||||
| -rw-r--r-- | types/http.go | 28 | ||||
| -rw-r--r-- | utils/auth/auth.go | 17 | ||||
| -rw-r--r-- | utils/crypto/crypto.go | 71 | ||||
| -rw-r--r-- | utils/meta/request.go | 47 | ||||
| -rw-r--r-- | utils/meta/title.go | 13 | ||||
| -rw-r--r-- | utils/shortcuts/helpers.go | 50 | ||||
| -rw-r--r-- | utils/shortcuts/redirect.go | 23 | ||||
| -rw-r--r-- | utils/shortcuts/render.go | 38 | ||||
| -rw-r--r-- | utils/urls/attach.go | 38 | ||||
| -rw-r--r-- | utils/urls/namespace.go | 7 | ||||
| -rw-r--r-- | utils/urls/path.go | 51 | ||||
| -rw-r--r-- | utils/urls/registery.go | 28 |
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, + }) +} @@ -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 ) @@ -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 }} - © <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(¶ms)) + 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), +} |
