aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-06-15 00:05:43 +0530
committerBobby <[email protected]>2025-06-15 00:05:43 +0530
commit9f6aac08a8e9a0685a21fbe4ee292cb514485f22 (patch)
tree165b210ffd080c47d775596322868996d51413dc
parente79dcba2565c10f9a967a97e863fedb727a3a041 (diff)
downloadimageboard-9f6aac08a8e9a0685a21fbe4ee292cb514485f22.tar.xz
imageboard-9f6aac08a8e9a0685a21fbe4ee292cb514485f22.zip
setting up the imageboard
-rw-r--r--config/config.go47
-rw-r--r--config/functions.go115
-rw-r--r--config/types.go43
-rw-r--r--controllers/home.go17
-rw-r--r--go.mod33
-rw-r--r--go.sum65
-rw-r--r--imageboard/main.go47
-rw-r--r--middleware/json.go8
-rw-r--r--processors/meta.go15
-rw-r--r--processors/processors.go7
-rw-r--r--router/routes.go19
-rw-r--r--session/session.go37
-rw-r--r--templates/home.html7
-rw-r--r--templates/layout.html13
-rw-r--r--utils/shortcuts/render.go66
15 files changed, 539 insertions, 0 deletions
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..14c0bf2
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,47 @@
+package config
+
+import (
+ "log"
+
+ "github.com/joho/godotenv"
+)
+
+var (
+ AppSecret string = "default_secret"
+ Database DatabaseConfig
+ IsDevelopmentMode bool = true
+ Image ImageConfig
+ S3 S3Config
+ Server ServerConfig
+ Session SessionConfig
+)
+
+func init() {
+ if err := godotenv.Load(); err != nil {
+ log.Println("no .env file found, using environment variables")
+ }
+
+ AppSecret = getEnv("APP_SECRET", AppSecret)
+
+ if err := Parse(&Database); err != nil {
+ log.Fatalf("failed to parse configuration: %v", err)
+ }
+
+ IsDevelopmentMode = getEnvBool("DEV_MODE", IsDevelopmentMode)
+
+ if err := Parse(&Image); err != nil {
+ log.Fatalf("failed to parse configuration: %v", err)
+ }
+
+ if err := Parse(&S3); err != nil {
+ log.Fatalf("failed to parse configuration: %v", err)
+ }
+
+ if err := Parse(&Server); err != nil {
+ log.Fatalf("failed to parse configuration: %v", err)
+ }
+
+ if err := Parse(&Session); err != nil {
+ log.Fatalf("failed to parse configuration: %v", err)
+ }
+}
diff --git a/config/functions.go b/config/functions.go
new file mode 100644
index 0000000..0ec2d8e
--- /dev/null
+++ b/config/functions.go
@@ -0,0 +1,115 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+ "strconv"
+ "time"
+)
+
+func getEnv(key, defaultVal string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultVal
+}
+
+func getEnvBool(key string, defaultVal bool) bool {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseBool(value); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := time.ParseDuration(value); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func getEnvInt64(key string, defaultVal int64) int64 {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func getEnvFloat64(key string, defaultVal float64) float64 {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseFloat(value, 64); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
+
+func Parse(config interface{}) error {
+ v := reflect.ValueOf(config)
+ if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
+ return fmt.Errorf("config must be a pointer to struct")
+ }
+
+ v = v.Elem()
+ t := v.Type()
+
+ for i := range v.NumField() {
+ field := v.Field(i)
+ fieldType := t.Field(i)
+
+ if !field.CanSet() {
+ continue
+ }
+
+ envKey := fieldType.Tag.Get("env")
+ defaultVal := fieldType.Tag.Get("default")
+
+ if envKey == "" {
+ continue
+ }
+
+ switch field.Kind() {
+ case reflect.String:
+ field.SetString(getEnv(envKey, defaultVal))
+
+ case reflect.Bool:
+ defaultBool, _ := strconv.ParseBool(defaultVal)
+ field.SetBool(getEnvBool(envKey, defaultBool))
+
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ defaultInt, _ := strconv.ParseInt(defaultVal, 10, 64)
+ field.SetInt(getEnvInt64(envKey, defaultInt))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ defaultUint, _ := strconv.ParseUint(defaultVal, 10, 64)
+ if value := os.Getenv(envKey); value != "" {
+ if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+ field.SetUint(parsed)
+ continue
+ }
+ }
+ field.SetUint(defaultUint)
+
+ case reflect.Float32, reflect.Float64:
+ defaultFloat, _ := strconv.ParseFloat(defaultVal, 64)
+ field.SetFloat(getEnvFloat64(envKey, defaultFloat))
+
+ default:
+ if field.Type() == reflect.TypeOf(time.Duration(0)) {
+ defaultDuration, _ := time.ParseDuration(defaultVal)
+ field.Set(reflect.ValueOf(getEnvDuration(envKey, defaultDuration)))
+ } else {
+ return fmt.Errorf("unsupported field type: %s", field.Kind())
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/config/types.go b/config/types.go
new file mode 100644
index 0000000..994b93e
--- /dev/null
+++ b/config/types.go
@@ -0,0 +1,43 @@
+package config
+
+import "time"
+
+type ServerConfig struct {
+ Host string `env:"SERVER_HOST" default:"localhost"`
+ Port string `env:"SERVER_PORT" default:"8080"`
+ AppName string `env:"APP_NAME" default:"ImageBoard"`
+}
+
+type DatabaseConfig struct {
+ Host string `env:"DB_HOST" default:"localhost"`
+ Port int `env:"DB_PORT" default:"5432"`
+ Username string `env:"DB_USERNAME" default:"postgres"`
+ Password string `env:"DB_PASSWORD" default:""`
+ DatabaseName string `env:"DB_NAME" default:"imageboard"`
+ SSLMode string `env:"DB_SSLMODE" default:"disable"`
+}
+
+type SessionConfig struct {
+ Expiration time.Duration `env:"SESSION_EXPIRATION" default:"24h"`
+ CookieName string `env:"SESSION_COOKIE_NAME" default:"session_id"`
+ CookieDomain string `env:"SESSION_COOKIE_DOMAIN" default:""`
+ CookiePath string `env:"SESSION_COOKIE_PATH" default:"/"`
+ CookieSecure bool `env:"SESSION_COOKIE_SECURE" default:"false"`
+ CookieSameSite string `env:"SESSION_COOKIE_SAMESITE" default:"Lax"`
+}
+
+type ImageConfig struct {
+ MaxSize int `env:"IMAGE_MAX_SIZE" default:"10485760"`
+ AllowedTypes string `env:"IMAGE_ALLOWED_TYPES" default:"image/jpeg,image/png,image/gif,image/webp"`
+}
+
+type S3Config struct {
+ Endpoint string `env:"S3_ENDPOINT" default:"localhost:9000"`
+ AccessKey string `env:"S3_ACCESS_KEY" default:"minioadmin"`
+ SecretAccessKey string `env:"S3_SECRET_KEY" default:"minioadmin"`
+ BucketName string `env:"S3_BUCKET_NAME" default:"shifoo"`
+ FolderPath string `env:"S3_FOLDER_PATH" default:"imageboard"`
+ Region string `env:"S3_REGION" default:"us-east-1"`
+ UseSSL bool `env:"S3_USE_SSL" default:"false"`
+ PublicURL string `env:"S3_PUBLIC_URL" default:""`
+}
diff --git a/controllers/home.go b/controllers/home.go
new file mode 100644
index 0000000..1b513c8
--- /dev/null
+++ b/controllers/home.go
@@ -0,0 +1,17 @@
+package controllers
+
+import (
+ "imageboard/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func HomeController(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", "Home Page")
+ customdata := struct {
+ Custommessage string
+ }{
+ Custommessage: "Welcome to the Imageboard!",
+ }
+ return shortcuts.Render(ctx, "home", customdata)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..7a64fbb
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,33 @@
+module imageboard
+
+go 1.24.4
+
+require (
+ github.com/gofiber/fiber/v2 v2.52.8
+ github.com/gofiber/storage/postgres/v2 v2.0.3
+ github.com/gofiber/template/html/v2 v2.1.3
+ github.com/joho/godotenv v1.5.1
+)
+
+require (
+ github.com/andybalholm/brotli v1.1.0 // 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-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.4.3 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/klauspost/compress v1.17.9 // 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
+ github.com/rivo/uniseg v0.2.0 // indirect
+ 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.14.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..2c5bd46
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,65 @@
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+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/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
+github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/gofiber/storage/postgres/v2 v2.0.3 h1:pN2PAKZMhy7oUkyZ3zS4fPZOVYa8gH/pciBkCw150K0=
+github.com/gofiber/storage/postgres/v2 v2.0.3/go.mod h1:6Hr+F+1/gslAsdpiJY2jwSJaJe368oTIJoCrUewfbRo=
+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/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
+github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
+github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
+github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
+github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+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/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=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+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/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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/imageboard/main.go b/imageboard/main.go
new file mode 100644
index 0000000..bce8cd1
--- /dev/null
+++ b/imageboard/main.go
@@ -0,0 +1,47 @@
+package main
+
+import (
+ "imageboard/config"
+ "imageboard/middleware"
+ "imageboard/processors"
+ "imageboard/router"
+ "log"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/cors"
+ "github.com/gofiber/fiber/v2/middleware/helmet"
+ "github.com/gofiber/fiber/v2/middleware/logger"
+ "github.com/gofiber/fiber/v2/middleware/recover"
+ "github.com/gofiber/template/html/v2"
+)
+
+func main() {
+ if config.AppSecret == "default_secret" {
+ 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.")
+ }
+
+ engine := html.New("./templates", ".html")
+ engine.Reload(config.IsDevelopmentMode)
+ app := fiber.New(fiber.Config{
+ Views: engine,
+ ErrorHandler: func(ctx *fiber.Ctx, err error) error {
+ log.Printf("Error: %v", err)
+ return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
+ },
+ BodyLimit: config.Image.MaxSize,
+ })
+
+ app.Use(recover.New())
+ app.Use(logger.New())
+ app.Use(helmet.New())
+ app.Use(cors.New())
+
+ processors.Initialize(app)
+
+ app.Use(middleware.JSON)
+ app.Static("/", "./static")
+
+ router.Initialize(app)
+
+ log.Fatalf("Server failed to start: %v", app.Listen(config.Server.Host+":"+config.Server.Port))
+}
diff --git a/middleware/json.go b/middleware/json.go
new file mode 100644
index 0000000..903ab43
--- /dev/null
+++ b/middleware/json.go
@@ -0,0 +1,8 @@
+package middleware
+
+import "github.com/gofiber/fiber/v2"
+
+func JSON(context *fiber.Ctx) error {
+ context.Accepts("application/json")
+ return context.Next()
+}
diff --git a/processors/meta.go b/processors/meta.go
new file mode 100644
index 0000000..ebd1c6b
--- /dev/null
+++ b/processors/meta.go
@@ -0,0 +1,15 @@
+package processors
+
+import (
+ "imageboard/config"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+const defaultTitle = "default"
+
+func MetaContextProcessor(ctx *fiber.Ctx) error {
+ ctx.Locals("Title", defaultTitle)
+ ctx.Locals("Appname", config.Server.AppName)
+ return ctx.Next()
+}
diff --git a/processors/processors.go b/processors/processors.go
new file mode 100644
index 0000000..69fbbec
--- /dev/null
+++ b/processors/processors.go
@@ -0,0 +1,7 @@
+package processors
+
+import "github.com/gofiber/fiber/v2"
+
+func Initialize(app *fiber.App) {
+ app.Use(MetaContextProcessor)
+}
diff --git a/router/routes.go b/router/routes.go
new file mode 100644
index 0000000..814b767
--- /dev/null
+++ b/router/routes.go
@@ -0,0 +1,19 @@
+package router
+
+import (
+ "imageboard/controllers"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Initialize(router *fiber.App) {
+ router.Get("/", controllers.HomeController)
+
+ router.Use(func(c *fiber.Ctx) error {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
+ "error": "Not Found",
+ "message": "The requested resource could not be found.",
+ "status": fiber.StatusNotFound,
+ })
+ })
+}
diff --git a/session/session.go b/session/session.go
new file mode 100644
index 0000000..ed89c5a
--- /dev/null
+++ b/session/session.go
@@ -0,0 +1,37 @@
+package session
+
+import (
+ "imageboard/config"
+ "log"
+
+ "github.com/gofiber/fiber/v2/middleware/session"
+ "github.com/gofiber/storage/postgres/v2"
+)
+
+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.DatabaseName,
+ Table: "sessions",
+ SSLMode: config.Database.SSLMode,
+ })
+
+ Store = session.New(session.Config{
+ Storage: storage,
+ Expiration: config.Session.Expiration,
+ KeyLookup: "cookie:" + config.Session.CookieName,
+ CookieName: config.Session.CookieName,
+ CookieDomain: config.Session.CookieDomain,
+ CookiePath: config.Session.CookiePath,
+ CookieSecure: config.Session.CookieSecure,
+ CookieSameSite: config.Session.CookieSameSite,
+ CookieHTTPOnly: true,
+ })
+
+ log.Println("Session store initialized successfully")
+}
diff --git a/templates/home.html b/templates/home.html
new file mode 100644
index 0000000..708a02e
--- /dev/null
+++ b/templates/home.html
@@ -0,0 +1,7 @@
+{{define "content"}}
+<main>
+ <h2>{{.Title}}</h2>
+ <p>Welcome to {{.Appname}}</p>
+ <p>Custom Data: {{ .Custommessage }}</p>
+</main>
+{{end}} {{template "layout" .}}
diff --git a/templates/layout.html b/templates/layout.html
new file mode 100644
index 0000000..d2a5b98
--- /dev/null
+++ b/templates/layout.html
@@ -0,0 +1,13 @@
+{{define "layout"}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>{{.Title}} - {{.Appname}}</title>
+ </head>
+ <body>
+ {{template "content" .}}
+ </body>
+</html>
+{{end}}
diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go
new file mode 100644
index 0000000..ba83a81
--- /dev/null
+++ b/utils/shortcuts/render.go
@@ -0,0 +1,66 @@
+package shortcuts
+
+import (
+ "reflect"
+ "strings"
+
+ "maps"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Render(ctx *fiber.Ctx, name string, bind any) error {
+ finalData := fiber.Map{}
+
+ ctx.Context().VisitUserValues(func(key []byte, value any) {
+ finalData[string(key)] = value
+ })
+
+ if bind != nil {
+ switch v := bind.(type) {
+ case fiber.Map:
+ maps.Copy(finalData, v)
+ case map[string]any:
+ maps.Copy(finalData, v)
+ default:
+ structData := structToMap(bind)
+ maps.Copy(finalData, structData)
+ }
+ }
+
+ return ctx.Render(name, finalData)
+}
+
+func structToMap(obj any) map[string]any {
+ result := make(map[string]any)
+
+ v := reflect.ValueOf(obj)
+ if v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ if v.Kind() != reflect.Struct {
+ return result
+ }
+
+ t := v.Type()
+ for i := range v.NumField() {
+ field := t.Field(i)
+ if !field.IsExported() {
+ continue
+ }
+
+ key := field.Name
+ if tag := field.Tag.Get("json"); tag != "" && tag != "-" {
+ if commaIdx := strings.Index(tag, ","); commaIdx > 0 {
+ key = tag[:commaIdx]
+ } else if commaIdx == -1 {
+ key = tag
+ }
+ }
+
+ result[key] = v.Field(i).Interface()
+ }
+
+ return result
+}