diff options
| -rw-r--r-- | dove/main.go | 3 | ||||
| -rw-r--r-- | messages/meta.go | 6 | ||||
| -rw-r--r-- | messages/tags.go | 10 | ||||
| -rw-r--r-- | middleware/middleware.go | 1 | ||||
| -rw-r--r-- | middleware/request.go | 12 | ||||
| -rw-r--r-- | pages/mailbox.go | 17 | ||||
| -rw-r--r-- | pages/mailboxes.go | 13 | ||||
| -rw-r--r-- | pages/users.go | 13 | ||||
| -rw-r--r-- | router/dashboard.go | 5 | ||||
| -rw-r--r-- | static/css/tailwind.css | 124 | ||||
| -rw-r--r-- | tags/constants.go | 5 | ||||
| -rw-r--r-- | tags/tags.go | 20 | ||||
| -rw-r--r-- | tags/types.go | 17 | ||||
| -rw-r--r-- | tags/url.go | 76 | ||||
| -rw-r--r-- | templates/auth/login.django | 60 | ||||
| -rw-r--r-- | templates/layouts/base.django | 6 | ||||
| -rw-r--r-- | templates/layouts/dashboard.django | 63 | ||||
| -rw-r--r-- | types/mailbox.go | 5 | ||||
| -rw-r--r-- | types/request.go | 17 | ||||
| -rw-r--r-- | utils/meta/builder.go | 20 | ||||
| -rw-r--r-- | utils/meta/constants.go | 6 | ||||
| -rw-r--r-- | utils/meta/functions.go | 53 | ||||
| -rw-r--r-- | utils/meta/request.go | 43 | ||||
| -rw-r--r-- | utils/meta/types.go | 17 | ||||
| -rw-r--r-- | utils/meta/value.go | 30 |
25 files changed, 608 insertions, 34 deletions
diff --git a/dove/main.go b/dove/main.go index 207565e..e3da2ce 100644 --- a/dove/main.go +++ b/dove/main.go @@ -10,6 +10,7 @@ import ( "dove/messages" "dove/middleware" "dove/router" + "dove/tags" "dove/utils/logger" "github.com/gofiber/fiber/v2" @@ -33,6 +34,8 @@ func main() { } func serve(command *cobra.Command, arguments []string) error { + tags.Initialize() + engine := django.New("./templates", ".django") engine.Reload(config.DevMode) diff --git a/messages/meta.go b/messages/meta.go new file mode 100644 index 0000000..d0349eb --- /dev/null +++ b/messages/meta.go @@ -0,0 +1,6 @@ +package messages + +const ( + MetaRequestContextMissing = "Request context missing in fiber locals." + MetaRequiredValueMissing = "Required value not found." +) diff --git a/messages/tags.go b/messages/tags.go new file mode 100644 index 0000000..6d23e71 --- /dev/null +++ b/messages/tags.go @@ -0,0 +1,10 @@ +package messages + +const ( + TagExpectedEquals = "Expected '=' after parameter key." + TagExpectedParamKey = "Expected parameter key identifier." + TagExpectedRouteName = "Expected route name string." + TagRegistrationFailed = "Failed to register tag: %s." + TagRouteNotFound = "Route not found: %s." + TagTemplateWriteFailed = "Failed to write template output." +) diff --git a/middleware/middleware.go b/middleware/middleware.go index 01cb1f0..1a4f1ed 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -4,5 +4,6 @@ import "github.com/gofiber/fiber/v2" func Initialize(application *fiber.App) { application.Use(httpLogger) + application.Use(requestBuilder) application.Use(globals) } diff --git a/middleware/request.go b/middleware/request.go new file mode 100644 index 0000000..21f8357 --- /dev/null +++ b/middleware/request.go @@ -0,0 +1,12 @@ +package middleware + +import ( + "dove/utils/meta" + + "github.com/gofiber/fiber/v2" +) + +func requestBuilder(context *fiber.Ctx) error { + context.Locals(meta.REQUEST_KEY, meta.BuildRequest(context)) + return context.Next() +} diff --git a/pages/mailbox.go b/pages/mailbox.go new file mode 100644 index 0000000..a444a73 --- /dev/null +++ b/pages/mailbox.go @@ -0,0 +1,17 @@ +package pages + +import ( + "dove/types" + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Mailbox(context *fiber.Ctx) error { + address := meta.Request(context).Param("address").Required() + meta.SetPageTitle(context, address) + return shortcuts.Render(context, "dashboard/mailbox", types.Mailbox{ + Address: address, + }) +} diff --git a/pages/mailboxes.go b/pages/mailboxes.go new file mode 100644 index 0000000..c70574e --- /dev/null +++ b/pages/mailboxes.go @@ -0,0 +1,13 @@ +package pages + +import ( + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Mailboxes(context *fiber.Ctx) error { + meta.SetPageTitle(context, "Mailboxes") + return shortcuts.Render(context, "dashboard/mailboxes", nil) +} diff --git a/pages/users.go b/pages/users.go new file mode 100644 index 0000000..ed25cd0 --- /dev/null +++ b/pages/users.go @@ -0,0 +1,13 @@ +package pages + +import ( + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func Users(context *fiber.Ctx) error { + meta.SetPageTitle(context, "Users") + return shortcuts.Render(context, "dashboard/users", nil) +} diff --git a/router/dashboard.go b/router/dashboard.go index 1ab4dda..2a593dc 100644 --- a/router/dashboard.go +++ b/router/dashboard.go @@ -11,4 +11,7 @@ func init() { urls.SetNamespace("dashboard") urls.Path(enums.Get, "/", auth.RequireAuthentication(pages.Dashboard), "index") -} + urls.Path(enums.Get, "/mailboxes", auth.RequireAuthentication(pages.Mailboxes), "mailboxes") + urls.Path(enums.Get, "/mailboxes/:address", auth.RequireAuthentication(pages.Mailbox), "mailbox") + urls.Path(enums.Get, "/users", auth.RequireAuthentication(pages.Users), "users") +}
\ No newline at end of file diff --git a/static/css/tailwind.css b/static/css/tailwind.css index f1d8c73..2983d29 100644 --- a/static/css/tailwind.css +++ b/static/css/tailwind.css @@ -1 +1,125 @@ @import "tailwindcss"; + +@theme { + --color-surface-950: #09090b; + --color-surface-900: #111113; + --color-surface-800: #1a1a1f; + --color-surface-700: #252529; + --color-surface-600: #35353b; + --color-accent-500: #6366f1; + --color-accent-400: #818cf8; + --color-accent-600: #4f46e5; +} + +@utility glass { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.03), + rgba(255, 255, 255, 0.01) + ); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +@utility glow-border { + border: 1px solid rgba(99, 102, 241, 0.2); + box-shadow: + 0 0 20px rgba(99, 102, 241, 0.05), + inset 0 0 20px rgba(99, 102, 241, 0.02); +} + +@utility gradient-text { + background: linear-gradient(135deg, #e4e4e7, #818cf8); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +@utility input-field { + width: 100%; + padding: 0.75rem 1rem; + border-radius: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.08); + background: var(--color-surface-800); + color: #e4e4e7; + font-size: 0.875rem; + line-height: 1.25rem; + outline: none; + transition: all 0.2s ease; + &::placeholder { + color: #52525b; + } + &:focus { + border-color: var(--color-accent-500); + box-shadow: + 0 0 0 3px rgba(99, 102, 241, 0.1), + 0 0 20px rgba(99, 102, 241, 0.05); + } +} + +@utility btn-primary { + width: 100%; + padding: 0.75rem 1rem; + border-radius: 0.75rem; + font-size: 0.875rem; + font-weight: 500; + color: white; + background: linear-gradient( + 135deg, + var(--color-accent-500), + var(--color-accent-600) + ); + outline: none; + transition: all 0.2s ease; + cursor: pointer; + &:hover { + box-shadow: + 0 0 30px rgba(99, 102, 241, 0.3), + 0 4px 15px rgba(99, 102, 241, 0.2); + transform: translateY(-1px); + } + &:active { + transform: translateY(0); + } +} + +@utility fade-in { + animation: fadeIn 0.4s ease-out; +} + +@utility slide-up { + animation: slideUp 0.5s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +::selection { + background-color: var(--color-accent-500); + color: white; +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus { + -webkit-text-fill-color: #e4e4e7; + -webkit-box-shadow: 0 0 0px 1000px var(--color-surface-800) inset; + transition: background-color 5000s ease-in-out 0s; +} diff --git a/tags/constants.go b/tags/constants.go new file mode 100644 index 0000000..f90fdaf --- /dev/null +++ b/tags/constants.go @@ -0,0 +1,5 @@ +package tags + +const ( + LOG_PREFIX = "Tags" +) diff --git a/tags/tags.go b/tags/tags.go new file mode 100644 index 0000000..0b864e5 --- /dev/null +++ b/tags/tags.go @@ -0,0 +1,20 @@ +package tags + +import ( + "dove/messages" + "dove/utils/logger" + + "github.com/flosch/pongo2/v6" +) + +func Initialize() { + tags := []templateTag{ + {Name: "url", Parser: url}, + } + + for _, tag := range tags { + if registrationError := pongo2.RegisterTag(tag.Name, tag.Parser); registrationError != nil { + logger.Errorf(LOG_PREFIX, messages.TagRegistrationFailed, tag.Name) + } + } +} diff --git a/tags/types.go b/tags/types.go new file mode 100644 index 0000000..b031904 --- /dev/null +++ b/tags/types.go @@ -0,0 +1,17 @@ +package tags + +import ( + "dove/utils/collections" + + "github.com/flosch/pongo2/v6" +) + +type templateTag struct { + Name string + Parser pongo2.TagParser +} + +type urlNode struct { + routeName string + params collections.Record[pongo2.IEvaluator] +} diff --git a/tags/url.go b/tags/url.go new file mode 100644 index 0000000..3b899c2 --- /dev/null +++ b/tags/url.go @@ -0,0 +1,76 @@ +package tags + +import ( + "fmt" + "strings" + + "dove/messages" + "dove/utils/collections" + "dove/utils/errors" + "dove/utils/urls" + + "github.com/flosch/pongo2/v6" +) + +func url(document *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) { + routeNameToken := arguments.MatchType(pongo2.TokenString) + if routeNameToken == nil { + return nil, arguments.Error(messages.TagExpectedRouteName, nil) + } + + params := make(collections.Record[pongo2.IEvaluator]) + + for arguments.Remaining() > 0 { + keyToken := arguments.MatchType(pongo2.TokenIdentifier) + if keyToken == nil { + return nil, arguments.Error(messages.TagExpectedParamKey, nil) + } + + if arguments.Match(pongo2.TokenSymbol, "=") == nil { + return nil, arguments.Error(messages.TagExpectedEquals, nil) + } + + valueExpression, parseError := arguments.ParseExpression() + if parseError != nil { + return nil, parseError + } + + params[keyToken.Val] = valueExpression + } + + return &urlNode{ + routeName: routeNameToken.Val, + params: params, + }, nil +} + +func (self *urlNode) Execute(executionContext *pongo2.ExecutionContext, writer pongo2.TemplateWriter) *pongo2.Error { + path, exists := urls.GetFullPath(self.routeName) + if !exists { + return &pongo2.Error{ + Sender: "tag:url", + OrigError: errors.Error(messages.TagRouteNotFound, self.routeName), + } + } + + for key, expression := range self.params { + evaluatedValue, evaluationError := expression.Evaluate(executionContext) + if evaluationError != nil { + return evaluationError + } + + placeholder := fmt.Sprintf(":%s", key) + replacement := fmt.Sprintf("%v", evaluatedValue.Interface()) + path = strings.ReplaceAll(path, placeholder, replacement) + } + + _, writeError := writer.WriteString(path) + if writeError != nil { + return &pongo2.Error{ + Sender: "tag:url", + OrigError: errors.Error(messages.TagTemplateWriteFailed), + } + } + + return nil +} diff --git a/templates/auth/login.django b/templates/auth/login.django index 3981abd..babff22 100644 --- a/templates/auth/login.django +++ b/templates/auth/login.django @@ -1,45 +1,41 @@ {% extends "layouts/base.django" %} {% block content %} -<div class="min-h-screen flex items-center justify-center bg-gray-950"> - <div class="w-full max-w-sm space-y-8"> - <div class="text-center"> - <h1 class="text-4xl font-bold text-white tracking-tight">Dove</h1> - <p class="mt-2 text-sm text-gray-400">Local SMTP server for peaceful email testing</p> +<div class="min-h-screen flex items-center justify-center px-4"> + <div class="w-full max-w-sm slide-up"> + <div class="text-center mb-10"> + <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl glass glow-border mb-5"> + <svg class="w-7 h-7 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" /> + </svg> + </div> + <h1 class="text-3xl font-bold gradient-text tracking-tight">Dove</h1> + <p class="mt-2 text-sm text-zinc-500">Local SMTP server for peaceful email testing</p> </div> {% if ErrorMessage %} - <div class="rounded-lg bg-red-500/10 border border-red-500/20 px-4 py-3 text-sm text-red-400"> + <div class="mb-6 rounded-xl bg-red-500/5 border border-red-500/15 px-4 py-3 text-sm text-red-400 fade-in"> {{ ErrorMessage }} </div> {% endif %} - <form method="POST" action="/auth/login" class="space-y-5"> - <div> - <input - type="text" - name="username" - placeholder="Username" - required - class="w-full rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-sm text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition" - > - </div> - <div> - <input - type="password" - name="password" - placeholder="Password" - required - class="w-full rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-sm text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition" - > - </div> - <button - type="submit" - class="w-full rounded-lg bg-blue-600 px-4 py-3 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-950 transition" - > - Sign in - </button> - </form> + <div class="glass rounded-2xl p-6 glow-border"> + <form method="POST" action="/auth/login" class="space-y-4"> + <div> + <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Username</label> + <input type="text" name="username" required autocomplete="username" class="input-field"> + </div> + <div> + <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Password</label> + <input type="password" name="password" required autocomplete="current-password" class="input-field"> + </div> + <div class="pt-2"> + <button type="submit" class="btn-primary">Sign in</button> + </div> + </form> + </div> + + <p class="mt-6 text-center text-xs text-zinc-600">Credentials are configured in config.toml</p> </div> </div> {% endblock %} diff --git a/templates/layouts/base.django b/templates/layouts/base.django index 567b81d..1ccc991 100644 --- a/templates/layouts/base.django +++ b/templates/layouts/base.django @@ -7,7 +7,11 @@ <link rel="stylesheet" href="/static/css/style.css"> {% block head %}{% endblock %} </head> -<body class="antialiased"> +<body class="antialiased bg-surface-950 text-zinc-200"> + <div class="fixed inset-0 -z-10 overflow-hidden"> + <div class="absolute -top-40 -right-40 h-[500px] w-[500px] rounded-full bg-accent-500/[0.03] blur-[120px]"></div> + <div class="absolute -bottom-40 -left-40 h-[400px] w-[400px] rounded-full bg-accent-400/[0.02] blur-[100px]"></div> + </div> {% block content %}{% endblock %} <script src="/static/js/htmx.min.js"></script> {% block scripts %}{% endblock %} diff --git a/templates/layouts/dashboard.django b/templates/layouts/dashboard.django new file mode 100644 index 0000000..22f03bc --- /dev/null +++ b/templates/layouts/dashboard.django @@ -0,0 +1,63 @@ +{% extends "layouts/base.django" %} + +{% block content %} +<div class="min-h-screen flex fade-in"> + <aside class="w-60 shrink-0 border-r border-white/[0.04] glass"> + <div class="flex items-center gap-3 px-5 h-14 border-b border-white/[0.04]"> + <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10"> + <svg class="w-4 h-4 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" /> + </svg> + </div> + <span class="text-sm font-semibold text-zinc-100 tracking-tight">Dove</span> + </div> + + <nav class="flex flex-col gap-1 p-3"> + <a href="/dashboard" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04] transition-colors duration-150 {% if ActiveNav == 'overview' %}bg-white/[0.06] text-zinc-100{% endif %}"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25a2.25 2.25 0 0 1-2.25-2.25v-2.25Z" /> + </svg> + Overview + </a> + <a href="/dashboard/mailboxes" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04] transition-colors duration-150 {% if ActiveNav == 'mailboxes' %}bg-white/[0.06] text-zinc-100{% endif %}"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" /> + </svg> + Mailboxes + </a> + <a href="/dashboard/users" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-zinc-200 hover:bg-white/[0.04] transition-colors duration-150 {% if ActiveNav == 'users' %}bg-white/[0.06] text-zinc-100{% endif %}"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /> + </svg> + Users + </a> + </nav> + + <div class="mt-auto p-3 border-t border-white/[0.04]"> + <div class="flex items-center gap-2 px-3 py-2 text-xs text-zinc-600"> + <span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span> + SMTP listening + </div> + {% if AuthEnabled %} + <a href="/auth/logout" class="flex items-center gap-3 px-3 py-2 rounded-lg text-xs text-zinc-500 hover:text-zinc-300 hover:bg-white/[0.04] transition-colors duration-150"> + <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" /> + </svg> + Logout + </a> + {% endif %} + </div> + </aside> + + <div class="flex-1 flex flex-col min-h-screen"> + <header class="h-14 flex items-center justify-between px-8 border-b border-white/[0.04]"> + <h1 class="text-sm font-medium text-zinc-100">{{ PageTitle }}</h1> + {% block header_actions %}{% endblock %} + </header> + + <main class="flex-1 p-8"> + {% block dashboard %}{% endblock %} + </main> + </div> +</div> +{% endblock %} diff --git a/types/mailbox.go b/types/mailbox.go new file mode 100644 index 0000000..5d213fd --- /dev/null +++ b/types/mailbox.go @@ -0,0 +1,5 @@ +package types + +type Mailbox struct { + Address string +} diff --git a/types/request.go b/types/request.go new file mode 100644 index 0000000..5d6a1a1 --- /dev/null +++ b/types/request.go @@ -0,0 +1,17 @@ +package types + +type Param struct { + Key string + Value string +} + +type Request struct { + Path string + Method string + Query []Param + Params []Param + Headers []Param + QueryString string + IP string + URL string +} diff --git a/utils/meta/builder.go b/utils/meta/builder.go new file mode 100644 index 0000000..4ae2648 --- /dev/null +++ b/utils/meta/builder.go @@ -0,0 +1,20 @@ +package meta + +import ( + "dove/types" + + "github.com/gofiber/fiber/v2" +) + +func BuildRequest(context *fiber.Ctx) types.Request { + return types.Request{ + 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(), + } +} diff --git a/utils/meta/constants.go b/utils/meta/constants.go new file mode 100644 index 0000000..3f2ce68 --- /dev/null +++ b/utils/meta/constants.go @@ -0,0 +1,6 @@ +package meta + +const ( + LOG_PREFIX = "Meta" + REQUEST_KEY = "__request" +) diff --git a/utils/meta/functions.go b/utils/meta/functions.go new file mode 100644 index 0000000..0b8f309 --- /dev/null +++ b/utils/meta/functions.go @@ -0,0 +1,53 @@ +package meta + +import ( + "dove/types" + + "github.com/gofiber/fiber/v2" +) + +func findParam(params []types.Param, key string) (string, bool) { + for _, param := range params { + if param.Key == key { + return param.Value, true + } + } + + return "", false +} + +func buildQueryParams(context *fiber.Ctx) []types.Param { + params := make([]types.Param, 0) + context.Request().URI().QueryArgs().VisitAll(func(name []byte, paramValue []byte) { + params = append(params, types.Param{ + Key: string(name), + Value: string(paramValue), + }) + }) + + return params +} + +func buildRouteParams(context *fiber.Ctx) []types.Param { + params := make([]types.Param, 0) + for name, routeValue := range context.AllParams() { + params = append(params, types.Param{ + Key: name, + Value: routeValue, + }) + } + + return params +} + +func buildHeaders(context *fiber.Ctx) []types.Param { + params := make([]types.Param, 0) + context.Request().Header.VisitAll(func(name []byte, headerValue []byte) { + params = append(params, types.Param{ + Key: string(name), + Value: string(headerValue), + }) + }) + + return params +} diff --git a/utils/meta/request.go b/utils/meta/request.go new file mode 100644 index 0000000..3098970 --- /dev/null +++ b/utils/meta/request.go @@ -0,0 +1,43 @@ +package meta + +import ( + "dove/messages" + "dove/types" + "dove/utils/logger" + + "github.com/gofiber/fiber/v2" +) + +func Request(context *fiber.Ctx) request { + data, ok := context.Locals(REQUEST_KEY).(types.Request) + if !ok { + logger.Errorf(LOG_PREFIX, messages.MetaRequestContextMissing) + return request{} + } + + return request{ + Request: data, + context: context, + } +} + +func (self request) Param(key string) value { + if self.context != nil { + result := self.context.Params(key) + if result != "" { + return value{data: result, found: true} + } + } + + return value{} +} + +func (self request) Query(key string) value { + result, found := findParam(self.Request.Query, key) + return value{data: result, found: found} +} + +func (self request) Header(key string) value { + result, found := findParam(self.Request.Headers, key) + return value{data: result, found: found} +} diff --git a/utils/meta/types.go b/utils/meta/types.go new file mode 100644 index 0000000..8aa710f --- /dev/null +++ b/utils/meta/types.go @@ -0,0 +1,17 @@ +package meta + +import ( + "dove/types" + + "github.com/gofiber/fiber/v2" +) + +type request struct { + types.Request + context *fiber.Ctx +} + +type value struct { + data string + found bool +} diff --git a/utils/meta/value.go b/utils/meta/value.go new file mode 100644 index 0000000..fb90500 --- /dev/null +++ b/utils/meta/value.go @@ -0,0 +1,30 @@ +package meta + +import ( + "dove/messages" + "dove/utils/logger" +) + +func (self value) String() string { + return self.data +} + +func (self value) Exists() bool { + return self.found +} + +func (self value) Or(fallback string) string { + if self.found { + return self.data + } + + return fallback +} + +func (self value) Required() string { + if !self.found { + logger.Errorf(LOG_PREFIX, messages.MetaRequiredValueMissing) + } + + return self.data +} |
