summaryrefslogtreecommitdiff
path: root/nexus/utils
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-29 22:52:46 +0530
committerBobby <[email protected]>2026-03-29 22:52:46 +0530
commit9eb9b7f4bd552a641235764f66483e1f940fcfd9 (patch)
treeda520b923b5e6758d5457b6233dd6671fc640914 /nexus/utils
parent65a143a0871c35989b7c7ea6723d39a0585c089e (diff)
downloadechoes-of-vaelun-main.tar.xz
echoes-of-vaelun-main.zip
feat: nexus account manager scaffold with auth, characters, realmsHEADmain
Diffstat (limited to 'nexus/utils')
-rw-r--r--nexus/utils/auth/auth.go54
-rw-r--r--nexus/utils/collections/maps.go37
-rw-r--r--nexus/utils/collections/record.go3
-rw-r--r--nexus/utils/env/defaults.go6
-rw-r--r--nexus/utils/env/extract.go33
-rw-r--r--nexus/utils/env/getenv.go75
-rw-r--r--nexus/utils/env/messages.go5
-rw-r--r--nexus/utils/env/parse.go27
-rw-r--r--nexus/utils/env/setenv.go62
-rw-r--r--nexus/utils/logger/defaults.go25
-rw-r--r--nexus/utils/logger/format.go63
-rw-r--r--nexus/utils/logger/logger.go81
-rw-r--r--nexus/utils/logger/messages.go5
-rw-r--r--nexus/utils/meta/account.go15
-rw-r--r--nexus/utils/meta/body.go11
-rw-r--r--nexus/utils/meta/defaults.go8
-rw-r--r--nexus/utils/meta/messages.go5
-rw-r--r--nexus/utils/meta/request.go108
-rw-r--r--nexus/utils/meta/session.go16
-rw-r--r--nexus/utils/meta/title.go7
-rw-r--r--nexus/utils/shortcuts/errors.go20
-rw-r--r--nexus/utils/shortcuts/flash.go36
-rw-r--r--nexus/utils/shortcuts/json.go12
-rw-r--r--nexus/utils/shortcuts/messages.go5
-rw-r--r--nexus/utils/shortcuts/redirect.go11
-rw-r--r--nexus/utils/shortcuts/render.go103
-rw-r--r--nexus/utils/shortcuts/token.go16
-rw-r--r--nexus/utils/token/token.go14
-rw-r--r--nexus/utils/urls/attach.go12
-rw-r--r--nexus/utils/urls/path.go114
-rw-r--r--nexus/utils/urls/registry.go34
-rw-r--r--nexus/utils/validate/email.go9
-rw-r--r--nexus/utils/validate/messages.go5
33 files changed, 1037 insertions, 0 deletions
diff --git a/nexus/utils/auth/auth.go b/nexus/utils/auth/auth.go
new file mode 100644
index 0000000..4e44d5e
--- /dev/null
+++ b/nexus/utils/auth/auth.go
@@ -0,0 +1,54 @@
+package auth
+
+import (
+ "nexus/repositories/account"
+ "nexus/repositories/session"
+ "nexus/utils/meta"
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func APIAuth(handler fiber.Handler) fiber.Handler {
+ return func(context *fiber.Ctx) error {
+ authHeader := context.Get("Authorization")
+ if authHeader == "" {
+ return fiber.ErrUnauthorized
+ }
+
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return fiber.ErrUnauthorized
+ }
+
+ s, err := session.FindByAuthToken(parts[1])
+ if err != nil || s == nil {
+ return fiber.ErrUnauthorized
+ }
+
+ if s.IsAuthExpired() {
+ return fiber.ErrUnauthorized
+ }
+
+ a, err := account.FindByID(s.AccountID)
+ if err != nil || a == nil {
+ return fiber.ErrUnauthorized
+ }
+
+ if !a.IsActive {
+ return fiber.ErrUnauthorized
+ }
+
+ context.Locals(meta.AccountKey, a)
+ return handler(context)
+ }
+}
+
+func WebAuth(handler fiber.Handler) fiber.Handler {
+ return func(context *fiber.Ctx) error {
+ if meta.Account(context) == nil {
+ return context.Redirect("/login")
+ }
+ return handler(context)
+ }
+}
diff --git a/nexus/utils/collections/maps.go b/nexus/utils/collections/maps.go
new file mode 100644
index 0000000..4536515
--- /dev/null
+++ b/nexus/utils/collections/maps.go
@@ -0,0 +1,37 @@
+package collections
+
+type OrderedMap[K comparable, V any] struct {
+ keys []K
+ values map[K]V
+}
+
+func OrderedMapOf[K comparable, V any]() OrderedMap[K, V] {
+ return OrderedMap[K, V]{
+ keys: make([]K, 0),
+ values: make(map[K]V),
+ }
+}
+
+func (self *OrderedMap[K, V]) Set(key K, value V) {
+ if _, exists := self.values[key]; !exists {
+ self.keys = append(self.keys, key)
+ }
+ self.values[key] = value
+}
+
+func (self *OrderedMap[K, V]) Get(key K) (V, bool) {
+ value, exists := self.values[key]
+ return value, exists
+}
+
+func (self *OrderedMap[K, V]) All() []V {
+ result := make([]V, 0, len(self.keys))
+ for _, key := range self.keys {
+ result = append(result, self.values[key])
+ }
+ return result
+}
+
+func (self *OrderedMap[K, V]) Len() int {
+ return len(self.keys)
+}
diff --git a/nexus/utils/collections/record.go b/nexus/utils/collections/record.go
new file mode 100644
index 0000000..18dbc2d
--- /dev/null
+++ b/nexus/utils/collections/record.go
@@ -0,0 +1,3 @@
+package collections
+
+type Record[K comparable, V any] = map[K]V
diff --git a/nexus/utils/env/defaults.go b/nexus/utils/env/defaults.go
new file mode 100644
index 0000000..70c3b41
--- /dev/null
+++ b/nexus/utils/env/defaults.go
@@ -0,0 +1,6 @@
+package env
+
+const (
+ keyEnv = "env"
+ keyDefault = "default"
+)
diff --git a/nexus/utils/env/extract.go b/nexus/utils/env/extract.go
new file mode 100644
index 0000000..72db69f
--- /dev/null
+++ b/nexus/utils/env/extract.go
@@ -0,0 +1,33 @@
+package env
+
+import (
+ "errors"
+ "reflect"
+)
+
+func extractConfig(config any) (reflect.Value, reflect.Type, error) {
+ v := reflect.ValueOf(config)
+ if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct {
+ return reflect.Value{}, nil, errors.New(ConfigMustBePointer)
+ }
+ elem := v.Elem()
+ return elem, elem.Type(), nil
+}
+
+func extractFieldEnvInfo(element reflect.Value, elementType reflect.Type, index int) (*reflect.Value, string, string, bool) {
+ field := element.Field(index)
+ fieldType := elementType.Field(index)
+
+ if !field.CanSet() {
+ return nil, "", "", false
+ }
+
+ envKey := fieldType.Tag.Get(keyEnv)
+ defaultVal := fieldType.Tag.Get(keyDefault)
+
+ if envKey == "" {
+ return nil, "", "", false
+ }
+
+ return &field, envKey, defaultVal, true
+}
diff --git a/nexus/utils/env/getenv.go b/nexus/utils/env/getenv.go
new file mode 100644
index 0000000..fde01ae
--- /dev/null
+++ b/nexus/utils/env/getenv.go
@@ -0,0 +1,75 @@
+package env
+
+import (
+ "os"
+ "strconv"
+ "strings"
+ "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 getEnvInt(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 getEnvFloat(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 getEnvStringSlice(key string, defaultVal []string) []string {
+ if value := os.Getenv(key); value != "" {
+ parts := strings.Split(value, ",")
+ result := make([]string, 0, len(parts))
+ for _, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if trimmed != "" {
+ result = append(result, trimmed)
+ }
+ }
+ return result
+ }
+ return defaultVal
+}
+
+func getEnvUint(key string, defaultVal uint64) uint64 {
+ if value := os.Getenv(key); value != "" {
+ if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
+ return parsed
+ }
+ }
+ return defaultVal
+}
diff --git a/nexus/utils/env/messages.go b/nexus/utils/env/messages.go
new file mode 100644
index 0000000..f5c7c91
--- /dev/null
+++ b/nexus/utils/env/messages.go
@@ -0,0 +1,5 @@
+package env
+
+const (
+ ConfigMustBePointer = "config must be a pointer to struct"
+)
diff --git a/nexus/utils/env/parse.go b/nexus/utils/env/parse.go
new file mode 100644
index 0000000..2c494b5
--- /dev/null
+++ b/nexus/utils/env/parse.go
@@ -0,0 +1,27 @@
+package env
+
+import (
+ "github.com/joho/godotenv"
+)
+
+func init() {
+ godotenv.Load()
+}
+
+func Parse(config any) error {
+ element, elementType, err := extractConfig(config)
+ if err != nil {
+ return err
+ }
+
+ for index := range element.NumField() {
+ field, envKey, defaultVal, ok := extractFieldEnvInfo(element, elementType, index)
+ if !ok {
+ continue
+ }
+
+ setFieldFromEnv(*field, envKey, defaultVal)
+ }
+
+ return nil
+}
diff --git a/nexus/utils/env/setenv.go b/nexus/utils/env/setenv.go
new file mode 100644
index 0000000..9001e68
--- /dev/null
+++ b/nexus/utils/env/setenv.go
@@ -0,0 +1,62 @@
+package env
+
+import (
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func setFieldFromEnv(field reflect.Value, envKey, defaultVal string) {
+ if field.Type() == reflect.TypeFor[time.Duration]() {
+ setDurationField(field, envKey, defaultVal)
+ return
+ }
+
+ 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(getEnvInt(envKey, defaultInt))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ defaultUint, _ := strconv.ParseUint(defaultVal, 10, 64)
+ setUintField(field, envKey, defaultUint)
+ case reflect.Float32, reflect.Float64:
+ defaultFloat, _ := strconv.ParseFloat(defaultVal, 64)
+ field.SetFloat(getEnvFloat(envKey, defaultFloat))
+ case reflect.Slice:
+ setSliceField(field, envKey, defaultVal)
+ }
+}
+
+func setUintField(field reflect.Value, envKey string, defaultVal uint64) {
+ field.SetUint(getEnvUint(envKey, defaultVal))
+}
+
+func setDurationField(field reflect.Value, envKey, defaultVal string) {
+ if field.Type() == reflect.TypeFor[time.Duration]() {
+ defaultDuration, _ := time.ParseDuration(defaultVal)
+ field.Set(reflect.ValueOf(getEnvDuration(envKey, defaultDuration)))
+ }
+}
+
+func setSliceField(field reflect.Value, envKey, defaultVal string) {
+ if field.Type().Elem().Kind() == reflect.String {
+ var defaultSlice []string
+ if defaultVal != "" {
+ parts := strings.Split(defaultVal, ",")
+ for _, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if trimmed != "" {
+ defaultSlice = append(defaultSlice, trimmed)
+ }
+ }
+ }
+ result := getEnvStringSlice(envKey, defaultSlice)
+ field.Set(reflect.ValueOf(result))
+ }
+}
diff --git a/nexus/utils/logger/defaults.go b/nexus/utils/logger/defaults.go
new file mode 100644
index 0000000..3b00803
--- /dev/null
+++ b/nexus/utils/logger/defaults.go
@@ -0,0 +1,25 @@
+package logger
+
+const (
+ AnsiReset = "\033[0m"
+)
+
+const (
+ LevelColorDebug = "\033[35mDEBUG \033[0m"
+ LevelColorError = "\033[31mERROR \033[0m"
+ LevelColorInfo = "\033[34mINFO \033[0m"
+ LevelColorWarn = "\033[33mWARN \033[0m"
+)
+
+const (
+ MessageColorDebug = "\033[90m"
+ MessageColorError = "\033[31m"
+ MessageColorInfo = "\033[97m"
+ MessageColorSuccess = "\033[32m"
+ MessageColorWarn = "\033[33m"
+)
+
+const (
+ PrefixColor = "\033[36m"
+ PrefixWidth = 18
+)
diff --git a/nexus/utils/logger/format.go b/nexus/utils/logger/format.go
new file mode 100644
index 0000000..f3503df
--- /dev/null
+++ b/nexus/utils/logger/format.go
@@ -0,0 +1,63 @@
+package logger
+
+import (
+ "fmt"
+ "strings"
+
+ "go.uber.org/zap/zapcore"
+)
+
+type LogLevel string
+
+const (
+ LevelDebug LogLevel = "debug"
+ LevelInfo LogLevel = "info"
+ LevelWarn LogLevel = "warn"
+ LevelError LogLevel = "error"
+ LevelSuccess LogLevel = "success"
+)
+
+func formatLevel(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
+ switch level {
+ case zapcore.DebugLevel:
+ encoder.AppendString(LevelColorDebug)
+ case zapcore.WarnLevel:
+ encoder.AppendString(LevelColorWarn)
+ case zapcore.ErrorLevel:
+ encoder.AppendString(LevelColorError)
+ default:
+ encoder.AppendString(LevelColorInfo)
+ }
+}
+
+func formatPrefix(prefix string) string {
+ if prefix == "" {
+ return ""
+ }
+
+ padding := ""
+ if len(prefix) < PrefixWidth {
+ padding = strings.Repeat(" ", PrefixWidth-len(prefix))
+ }
+
+ return PrefixColor + "[" + prefix + "]" + AnsiReset + padding
+}
+
+func colorizeMessage(level LogLevel, message string) string {
+ switch level {
+ case LevelDebug:
+ return MessageColorDebug + message + AnsiReset
+ case LevelWarn:
+ return MessageColorWarn + message + AnsiReset
+ case LevelError:
+ return MessageColorError + message + AnsiReset
+ case LevelSuccess:
+ return MessageColorSuccess + message + AnsiReset
+ default:
+ return MessageColorInfo + message + AnsiReset
+ }
+}
+
+func buildFullMessage(level LogLevel, prefix string, message any) string {
+ return formatPrefix(prefix) + colorizeMessage(level, fmt.Sprint(message))
+}
diff --git a/nexus/utils/logger/logger.go b/nexus/utils/logger/logger.go
new file mode 100644
index 0000000..f1cbbd1
--- /dev/null
+++ b/nexus/utils/logger/logger.go
@@ -0,0 +1,81 @@
+package logger
+
+import (
+ "fmt"
+ "os"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+var (
+ instance *zap.Logger
+ atomicLevel zap.AtomicLevel
+)
+
+func init() {
+ atomicLevel = zap.NewAtomicLevelAt(zapcore.InfoLevel)
+
+ encoderConfig := zapcore.EncoderConfig{
+ LevelKey: "level",
+ MessageKey: "msg",
+ LineEnding: "\n",
+ EncodeLevel: formatLevel,
+ }
+
+ encoder := zapcore.NewConsoleEncoder(encoderConfig)
+ stdoutSink := zapcore.AddSync(os.Stdout)
+ stderrSink := zapcore.AddSync(os.Stderr)
+
+ core := zapcore.NewTee(
+ zapcore.NewCore(encoder, stdoutSink, zap.LevelEnablerFunc(func(level zapcore.Level) bool {
+ return level < zapcore.WarnLevel && atomicLevel.Enabled(level)
+ })),
+ zapcore.NewCore(encoder, stderrSink, zap.LevelEnablerFunc(func(level zapcore.Level) bool {
+ return level >= zapcore.WarnLevel && atomicLevel.Enabled(level)
+ })),
+ )
+
+ instance = zap.New(core, zap.AddCaller())
+}
+
+func SetDebug(enabled bool) {
+ if enabled {
+ atomicLevel.SetLevel(zapcore.DebugLevel)
+ } else {
+ atomicLevel.SetLevel(zapcore.InfoLevel)
+ }
+}
+
+func Debugf(prefix string, format string, arguments ...any) {
+ emit(LevelDebug, zapcore.DebugLevel, prefix, fmt.Sprintf(format, arguments...))
+}
+
+func Infof(prefix string, format string, arguments ...any) {
+ emit(LevelInfo, zapcore.InfoLevel, prefix, fmt.Sprintf(format, arguments...))
+}
+
+func Successf(prefix string, format string, arguments ...any) {
+ emit(LevelSuccess, zapcore.InfoLevel, prefix, fmt.Sprintf(format, arguments...))
+}
+
+func Warnf(prefix string, format string, arguments ...any) {
+ emit(LevelWarn, zapcore.WarnLevel, prefix, fmt.Sprintf(format, arguments...))
+}
+
+func Errorf(prefix string, format string, arguments ...any) {
+ emit(LevelError, zapcore.ErrorLevel, prefix, fmt.Sprintf(format, arguments...))
+}
+
+func Fatalf(prefix string, format string, arguments ...any) {
+ emit(LevelError, zapcore.ErrorLevel, prefix, fmt.Sprintf(format, arguments...))
+ os.Exit(1)
+}
+
+func emit(levelLabel LogLevel, zapLevel zapcore.Level, prefix string, message any) {
+ if instance == nil {
+ panic(NotInitialized)
+ }
+
+ instance.Log(zapLevel, buildFullMessage(levelLabel, prefix, message))
+}
diff --git a/nexus/utils/logger/messages.go b/nexus/utils/logger/messages.go
new file mode 100644
index 0000000..784cc43
--- /dev/null
+++ b/nexus/utils/logger/messages.go
@@ -0,0 +1,5 @@
+package logger
+
+const (
+ NotInitialized = "Logger was not initialized."
+)
diff --git a/nexus/utils/meta/account.go b/nexus/utils/meta/account.go
new file mode 100644
index 0000000..a881b36
--- /dev/null
+++ b/nexus/utils/meta/account.go
@@ -0,0 +1,15 @@
+package meta
+
+import (
+ "nexus/models"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Account(context *fiber.Ctx) *models.Account {
+ account, ok := context.Locals(AccountKey).(*models.Account)
+ if !ok {
+ return nil
+ }
+ return account
+}
diff --git a/nexus/utils/meta/body.go b/nexus/utils/meta/body.go
new file mode 100644
index 0000000..bc63546
--- /dev/null
+++ b/nexus/utils/meta/body.go
@@ -0,0 +1,11 @@
+package meta
+
+import "github.com/gofiber/fiber/v2"
+
+func Body[T any](context *fiber.Ctx) (T, error) {
+ var body T
+ if err := context.BodyParser(&body); err != nil {
+ return body, err
+ }
+ return body, nil
+}
diff --git a/nexus/utils/meta/defaults.go b/nexus/utils/meta/defaults.go
new file mode 100644
index 0000000..8f0e27a
--- /dev/null
+++ b/nexus/utils/meta/defaults.go
@@ -0,0 +1,8 @@
+package meta
+
+const (
+ LogPrefix = "Meta"
+ RequestKey = "Request"
+ AccountKey = "Account"
+ SessionKey = "Session"
+)
diff --git a/nexus/utils/meta/messages.go b/nexus/utils/meta/messages.go
new file mode 100644
index 0000000..a188b46
--- /dev/null
+++ b/nexus/utils/meta/messages.go
@@ -0,0 +1,5 @@
+package meta
+
+const (
+ RequestContextMissing = "Request context missing in fiber locals."
+)
diff --git a/nexus/utils/meta/request.go b/nexus/utils/meta/request.go
new file mode 100644
index 0000000..e9c649d
--- /dev/null
+++ b/nexus/utils/meta/request.go
@@ -0,0 +1,108 @@
+package meta
+
+import (
+ "nexus/utils/logger"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+type Param struct {
+ Key string
+ Value string
+}
+
+type RequestInfo struct {
+ Path string
+ Method string
+ Query []Param
+ Params []Param
+ Headers []Param
+ QueryString string
+ IP string
+ URL string
+}
+
+type RequestData struct {
+ RequestInfo
+ Context *fiber.Ctx
+}
+
+func BuildRequest(context *fiber.Ctx) RequestInfo {
+ return RequestInfo{
+ Path: context.Path(),
+ Method: context.Method(),
+ Query: buildQueryParams(context),
+ Params: buildRouteParams(context),
+ Headers: buildHeaders(context),
+ QueryString: string(context.Request().URI().QueryString()),
+ IP: context.IP(),
+ URL: context.OriginalURL(),
+ }
+}
+
+func Request(context *fiber.Ctx) *RequestData {
+ data, ok := context.Locals(RequestKey).(RequestInfo)
+ if !ok {
+ logger.Errorf(LogPrefix, RequestContextMissing)
+ return nil
+ }
+
+ return &RequestData{
+ RequestInfo: data,
+ Context: context,
+ }
+}
+
+func (self *RequestData) Param(key string) string {
+ if self == nil || self.Context == nil {
+ return ""
+ }
+ return self.Context.Params(key)
+}
+
+func (self *RequestData) Query(key string) string {
+ if self == nil {
+ return ""
+ }
+ return findParam(self.RequestInfo.Query, key)
+}
+
+func (self *RequestData) Header(key string) string {
+ if self == nil {
+ return ""
+ }
+ return findParam(self.RequestInfo.Headers, key)
+}
+
+func buildQueryParams(context *fiber.Ctx) []Param {
+ params := make([]Param, 0)
+ context.Request().URI().QueryArgs().VisitAll(func(name []byte, value []byte) {
+ params = append(params, Param{Key: string(name), Value: string(value)})
+ })
+ return params
+}
+
+func buildRouteParams(context *fiber.Ctx) []Param {
+ params := make([]Param, 0)
+ for name, value := range context.AllParams() {
+ params = append(params, Param{Key: name, Value: value})
+ }
+ return params
+}
+
+func buildHeaders(context *fiber.Ctx) []Param {
+ params := make([]Param, 0)
+ context.Request().Header.VisitAll(func(name []byte, value []byte) {
+ params = append(params, Param{Key: string(name), Value: string(value)})
+ })
+ return params
+}
+
+func findParam(params []Param, key string) string {
+ for _, param := range params {
+ if param.Key == key {
+ return param.Value
+ }
+ }
+ return ""
+}
diff --git a/nexus/utils/meta/session.go b/nexus/utils/meta/session.go
new file mode 100644
index 0000000..ccc7adf
--- /dev/null
+++ b/nexus/utils/meta/session.go
@@ -0,0 +1,16 @@
+package meta
+
+import (
+ "nexus/sessions"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/session"
+)
+
+func Session(context *fiber.Ctx) *session.Session {
+ sess, ok := context.Locals(sessions.SessionLocalKey).(*session.Session)
+ if !ok {
+ return nil
+ }
+ return sess
+}
diff --git a/nexus/utils/meta/title.go b/nexus/utils/meta/title.go
new file mode 100644
index 0000000..3e6506f
--- /dev/null
+++ b/nexus/utils/meta/title.go
@@ -0,0 +1,7 @@
+package meta
+
+import "github.com/gofiber/fiber/v2"
+
+func SetPageTitle(context *fiber.Ctx, title string) {
+ context.Locals("Title", title)
+}
diff --git a/nexus/utils/shortcuts/errors.go b/nexus/utils/shortcuts/errors.go
new file mode 100644
index 0000000..cdb2ad2
--- /dev/null
+++ b/nexus/utils/shortcuts/errors.go
@@ -0,0 +1,20 @@
+package shortcuts
+
+import "github.com/gofiber/fiber/v2"
+
+func RouteError(context *fiber.Ctx, err *fiber.Error) error {
+ if isAPIRequest(context) {
+ return context.Status(err.Code).JSON(fiber.Map{
+ "error": err.Message,
+ })
+ }
+ return RenderWithStatus(context, "errors/error", err, err.Code)
+}
+
+func ServiceError(code int, message string) *fiber.Error {
+ return fiber.NewError(code, message)
+}
+
+func isAPIRequest(context *fiber.Ctx) bool {
+ return len(context.Path()) >= 4 && context.Path()[:4] == "/api"
+}
diff --git a/nexus/utils/shortcuts/flash.go b/nexus/utils/shortcuts/flash.go
new file mode 100644
index 0000000..c82b37d
--- /dev/null
+++ b/nexus/utils/shortcuts/flash.go
@@ -0,0 +1,36 @@
+package shortcuts
+
+import (
+ "nexus/sessions"
+ "nexus/utils/collections"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/session"
+)
+
+func Flash(context *fiber.Ctx, data collections.Record[string, any]) error {
+ sess, ok := context.Locals(sessions.SessionLocalKey).(*session.Session)
+ if !ok {
+ return nil
+ }
+ return sessions.SetFlash(sess, data)
+}
+
+func ConsumeFlash(context *fiber.Ctx) collections.Record[string, any] {
+ sess, ok := context.Locals(sessions.SessionLocalKey).(*session.Session)
+ if !ok {
+ return nil
+ }
+ data := sessions.GetFlash(sess)
+ if data != nil {
+ _ = sessions.ClearFlash(sess)
+ }
+ return data
+}
+
+func RedirectWithFlash(context *fiber.Ctx, routeName string, data collections.Record[string, any]) error {
+ if err := Flash(context, data); err != nil {
+ return err
+ }
+ return Redirect(context, routeName)
+}
diff --git a/nexus/utils/shortcuts/json.go b/nexus/utils/shortcuts/json.go
new file mode 100644
index 0000000..210d1a9
--- /dev/null
+++ b/nexus/utils/shortcuts/json.go
@@ -0,0 +1,12 @@
+package shortcuts
+
+import "github.com/gofiber/fiber/v2"
+
+func JSON(context *fiber.Ctx, data any) error {
+ return context.JSON(data)
+}
+
+func Created(context *fiber.Ctx, data any) error {
+ context.Status(fiber.StatusCreated)
+ return context.JSON(data)
+}
diff --git a/nexus/utils/shortcuts/messages.go b/nexus/utils/shortcuts/messages.go
new file mode 100644
index 0000000..819e536
--- /dev/null
+++ b/nexus/utils/shortcuts/messages.go
@@ -0,0 +1,5 @@
+package shortcuts
+
+const (
+ UnsupportedBindType = "Unsupported data type for binding. Only struct, collections.Record[string, any] are supported."
+)
diff --git a/nexus/utils/shortcuts/redirect.go b/nexus/utils/shortcuts/redirect.go
new file mode 100644
index 0000000..8999b7c
--- /dev/null
+++ b/nexus/utils/shortcuts/redirect.go
@@ -0,0 +1,11 @@
+package shortcuts
+
+import "github.com/gofiber/fiber/v2"
+
+func Redirect(context *fiber.Ctx, path string) error {
+ return context.Redirect(path)
+}
+
+func RedirectWithStatus(context *fiber.Ctx, path string, statusCode int) error {
+ return context.Redirect(path, statusCode)
+}
diff --git a/nexus/utils/shortcuts/render.go b/nexus/utils/shortcuts/render.go
new file mode 100644
index 0000000..5f3678f
--- /dev/null
+++ b/nexus/utils/shortcuts/render.go
@@ -0,0 +1,103 @@
+package shortcuts
+
+import (
+ "maps"
+ "nexus/utils/collections"
+ "reflect"
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Render(context *fiber.Ctx, templateName string, data any) error {
+ templateData := make(collections.Record[string, any])
+
+ if flash := ConsumeFlash(context); flash != nil {
+ maps.Copy(templateData, flash)
+ }
+
+ mergeContextValues(context, templateData)
+
+ if data != nil {
+ if mergeError := mergeBindData(templateData, data); mergeError != nil {
+ return mergeError
+ }
+ }
+
+ return context.Render(templateName, templateData)
+}
+
+func RenderWithStatus(context *fiber.Ctx, templateName string, data any, statusCode int) error {
+ context.Status(statusCode)
+ return Render(context, templateName, data)
+}
+
+func NoContent(context *fiber.Ctx) error {
+ return context.SendStatus(fiber.StatusNoContent)
+}
+
+func mergeContextValues(context *fiber.Ctx, target collections.Record[string, any]) {
+ context.Context().VisitUserValuesAll(func(key any, value any) {
+ switch typedKey := key.(type) {
+ case string:
+ if typedKey != "" {
+ target[typedKey] = value
+ }
+ case []byte:
+ if len(typedKey) > 0 {
+ target[string(typedKey)] = value
+ }
+ }
+ })
+}
+
+func mergeBindData(target collections.Record[string, any], data any) error {
+ normalized, err := normalizeToMap(data)
+ if err != nil {
+ return err
+ }
+ maps.Copy(target, normalized)
+ return nil
+}
+
+func normalizeToMap(data any) (collections.Record[string, any], error) {
+ switch v := data.(type) {
+ case collections.Record[string, any]:
+ return v, nil
+ default:
+ return convertStructToMap(data)
+ }
+}
+
+func convertStructToMap(data any) (collections.Record[string, any], error) {
+ v := reflect.ValueOf(data)
+ if v.Kind() == reflect.Pointer {
+ v = v.Elem()
+ }
+ if v.Kind() != reflect.Struct {
+ return nil, fiber.NewError(fiber.StatusInternalServerError, UnsupportedBindType)
+ }
+
+ t := v.Type()
+ result := make(collections.Record[string, any], t.NumField())
+
+ for i := range t.NumField() {
+ field := t.Field(i)
+ if !field.IsExported() {
+ continue
+ }
+
+ key := field.Name
+ if tag := field.Tag.Get("json"); tag != "" && tag != "-" {
+ if idx := strings.IndexByte(tag, ','); idx > 0 {
+ key = tag[:idx]
+ } else if idx < 0 {
+ key = tag
+ }
+ }
+
+ result[key] = v.Field(i).Interface()
+ }
+
+ return result, nil
+}
diff --git a/nexus/utils/shortcuts/token.go b/nexus/utils/shortcuts/token.go
new file mode 100644
index 0000000..be9990f
--- /dev/null
+++ b/nexus/utils/shortcuts/token.go
@@ -0,0 +1,16 @@
+package shortcuts
+
+import (
+ "strings"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func BearerToken(context *fiber.Ctx) string {
+ authHeader := context.Get("Authorization")
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ return ""
+ }
+ return parts[1]
+}
diff --git a/nexus/utils/token/token.go b/nexus/utils/token/token.go
new file mode 100644
index 0000000..e517a15
--- /dev/null
+++ b/nexus/utils/token/token.go
@@ -0,0 +1,14 @@
+package token
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+)
+
+func Generate() (string, error) {
+ bytes := make([]byte, 32)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(bytes), nil
+}
diff --git a/nexus/utils/urls/attach.go b/nexus/utils/urls/attach.go
new file mode 100644
index 0000000..b8a1458
--- /dev/null
+++ b/nexus/utils/urls/attach.go
@@ -0,0 +1,12 @@
+package urls
+
+import "github.com/gofiber/fiber/v2"
+
+func Attach(application *fiber.App) {
+ registry.Mutex.Lock()
+ defer registry.Mutex.Unlock()
+
+ for _, route := range registry.Routes.All() {
+ bindPath(application, route)
+ }
+}
diff --git a/nexus/utils/urls/path.go b/nexus/utils/urls/path.go
new file mode 100644
index 0000000..ca44726
--- /dev/null
+++ b/nexus/utils/urls/path.go
@@ -0,0 +1,114 @@
+package urls
+
+import (
+ "strings"
+
+ "nexus/utils/collections"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+type HTTPMethod string
+
+const (
+ Delete HTTPMethod = "DELETE"
+ Get HTTPMethod = "GET"
+ Head HTTPMethod = "HEAD"
+ Options HTTPMethod = "OPTIONS"
+ Patch HTTPMethod = "PATCH"
+ Post HTTPMethod = "POST"
+ Put HTTPMethod = "PUT"
+)
+
+func Path(method HTTPMethod, path string, handler fiber.Handler, name string) {
+ registry.Mutex.Lock()
+ defer registry.Mutex.Unlock()
+
+ namespace := registry.CurrentNamespace
+ fullName := resolveFullName(namespace, name)
+ fullPath := resolveFullPath(namespace, path)
+
+ registry.Routes.Set(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, exists := registry.Routes.Get(routeName)
+ if !exists {
+ return "", false
+ }
+
+ return route.FullPath, true
+}
+
+func ResolvePath(routeName string, params collections.Record[string, string]) (string, bool) {
+ registry.Mutex.Lock()
+ defer registry.Mutex.Unlock()
+
+ route, exists := registry.Routes.Get(routeName)
+ if !exists {
+ return "", false
+ }
+
+ resolved := route.FullPath
+ for key, value := range params {
+ resolved = strings.ReplaceAll(resolved, ":"+key, value)
+ }
+
+ return resolved, true
+}
+
+func resolveFullName(namespace string, name string) string {
+ switch namespace {
+ case "":
+ return name
+ default:
+ return namespace + "." + name
+ }
+}
+
+func resolveFullPath(namespace string, path string) string {
+ switch namespace {
+ case "":
+ return ensureLeadingSlash(path)
+ default:
+ return "/" + namespace + ensureLeadingSlash(path)
+ }
+}
+
+func bindPath(application *fiber.App, route RegisteredRoute) {
+ switch route.Method {
+ case Delete:
+ application.Delete(route.FullPath, route.Handler)
+ case Get:
+ application.Get(route.FullPath, route.Handler)
+ case Head:
+ application.Head(route.FullPath, route.Handler)
+ case Options:
+ application.Options(route.FullPath, route.Handler)
+ case Patch:
+ application.Patch(route.FullPath, route.Handler)
+ case Post:
+ application.Post(route.FullPath, route.Handler)
+ case Put:
+ application.Put(route.FullPath, route.Handler)
+ }
+}
+
+func ensureLeadingSlash(path string) string {
+ switch strings.HasPrefix(path, "/") {
+ case true:
+ return path
+ default:
+ return "/" + path
+ }
+}
diff --git a/nexus/utils/urls/registry.go b/nexus/utils/urls/registry.go
new file mode 100644
index 0000000..548a820
--- /dev/null
+++ b/nexus/utils/urls/registry.go
@@ -0,0 +1,34 @@
+package urls
+
+import (
+ "sync"
+
+ "nexus/utils/collections"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+type RegisteredRoute struct {
+ Method HTTPMethod
+ Path string
+ Handler fiber.Handler
+ Namespace string
+ Name string
+ FullPath string
+}
+
+type RouteRegistry struct {
+ Mutex sync.Mutex
+ CurrentNamespace string
+ Routes collections.OrderedMap[string, RegisteredRoute]
+}
+
+var registry = &RouteRegistry{
+ Routes: collections.OrderedMapOf[string, RegisteredRoute](),
+}
+
+func SetNamespace(namespace string) {
+ registry.Mutex.Lock()
+ defer registry.Mutex.Unlock()
+ registry.CurrentNamespace = namespace
+}
diff --git a/nexus/utils/validate/email.go b/nexus/utils/validate/email.go
new file mode 100644
index 0000000..70c6e8f
--- /dev/null
+++ b/nexus/utils/validate/email.go
@@ -0,0 +1,9 @@
+package validate
+
+import "regexp"
+
+var emailPattern = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
+
+func Email(email string) bool {
+ return emailPattern.MatchString(email)
+}
diff --git a/nexus/utils/validate/messages.go b/nexus/utils/validate/messages.go
new file mode 100644
index 0000000..defe6cf
--- /dev/null
+++ b/nexus/utils/validate/messages.go
@@ -0,0 +1,5 @@
+package validate
+
+const (
+ InvalidEmail = "invalid email address"
+)