aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-07 16:15:34 +0530
committerBobby <[email protected]>2026-03-07 16:15:34 +0530
commit6dd57549df7b6679a1aa9888f4d59edaaec5b3f9 (patch)
tree05b37b22e659cfb5f8b97b12abf857f22df4f2be
parent1f3a99dcc410f31ac247b55ae9880f6045ab46b4 (diff)
downloaddove-6dd57549df7b6679a1aa9888f4d59edaaec5b3f9.tar.xz
dove-6dd57549df7b6679a1aa9888f4d59edaaec5b3f9.zip
feat: implement request handling and dashboard features with new tags and utilities
-rw-r--r--dove/main.go3
-rw-r--r--messages/meta.go6
-rw-r--r--messages/tags.go10
-rw-r--r--middleware/middleware.go1
-rw-r--r--middleware/request.go12
-rw-r--r--pages/mailbox.go17
-rw-r--r--pages/mailboxes.go13
-rw-r--r--pages/users.go13
-rw-r--r--router/dashboard.go5
-rw-r--r--static/css/tailwind.css124
-rw-r--r--tags/constants.go5
-rw-r--r--tags/tags.go20
-rw-r--r--tags/types.go17
-rw-r--r--tags/url.go76
-rw-r--r--templates/auth/login.django60
-rw-r--r--templates/layouts/base.django6
-rw-r--r--templates/layouts/dashboard.django63
-rw-r--r--types/mailbox.go5
-rw-r--r--types/request.go17
-rw-r--r--utils/meta/builder.go20
-rw-r--r--utils/meta/constants.go6
-rw-r--r--utils/meta/functions.go53
-rw-r--r--utils/meta/request.go43
-rw-r--r--utils/meta/types.go17
-rw-r--r--utils/meta/value.go30
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
+}