aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-08 02:27:15 +0530
committerBobby <[email protected]>2026-03-08 02:27:15 +0530
commitcca905d35412f1549400fc3d1aca6dc704d8cae6 (patch)
tree0c0231f5c2ebaeb7700e08a2c1f07373d3251658
parent547384c41181c034a5eaf340c5e569d36eb013be (diff)
downloaddove-cca905d35412f1549400fc3d1aca6dc704d8cae6.tar.xz
dove-cca905d35412f1549400fc3d1aca6dc704d8cae6.zip
feat(domains): add new TLD creation page and update sidebar
- Introduced a new HTMX template for creating TLDs. - Created a new Django template for the new TLD page. - Updated the sidebar to include a link to the domains section. refactor(types): remove unused types and consolidate request handling - Deleted unused type definitions related to authentication, errors, mailboxes, overview, requests, responses, and users. - Introduced a new collections package for generic data structures. - Refactored request handling to use a more streamlined approach with RequestInfo and Param types. fix(meta): improve pagination and sorting functionality - Updated pagination logic to handle default values and edge cases. - Introduced a new Sorting type for better sorting management in queries. chore(urls): refactor URL handling and registry - Replaced enums with string constants for HTTP methods. - Consolidated route registration logic and improved type safety with RegisteredRoute. style(shortcuts): clean up error handling and rendering functions - Enhanced error handling functions for better readability and maintainability. - Removed deprecated functions and improved the structure of rendering logic.
-rw-r--r--controllers/domain.go46
-rw-r--r--database/constants.go11
-rw-r--r--database/database.go13
-rw-r--r--database/defaults.go11
-rw-r--r--database/functions.go2
-rw-r--r--database/messages.go8
-rw-r--r--database/migration.go6
-rw-r--r--enums/error.go12
-rw-r--r--enums/http.go13
-rw-r--r--enums/mailbox.go8
-rw-r--r--messages/auth.go7
-rw-r--r--messages/config.go13
-rw-r--r--messages/database.go8
-rw-r--r--messages/email.go10
-rw-r--r--messages/logger.go5
-rw-r--r--messages/mailbox.go11
-rw-r--r--messages/meta.go6
-rw-r--r--messages/server.go9
-rw-r--r--messages/session.go5
-rw-r--r--messages/shortcuts.go5
-rw-r--r--messages/smtp.go15
-rw-r--r--messages/tags.go11
-rw-r--r--messages/toml.go5
-rw-r--r--messages/user.go8
-rw-r--r--middleware/request.go2
-rw-r--r--models/domain/domain.go10
-rw-r--r--models/domain/tld.go10
-rw-r--r--models/mail/alias.go10
-rw-r--r--models/mail/attachment.go14
-rw-r--r--models/mail/email.go25
-rw-r--r--models/mail/mailbox.go12
-rw-r--r--models/mail/tag.go9
-rw-r--r--models/mail/user.go10
-rw-r--r--pages/domain.go24
-rw-r--r--repositories/domain/domain.go38
-rw-r--r--repositories/domain/tld.go35
-rw-r--r--repositories/init/init.go7
-rw-r--r--repositories/mail/alias.go18
-rw-r--r--repositories/mail/email.go50
-rw-r--r--repositories/mail/mailbox.go46
-rw-r--r--repositories/mail/user.go62
-rw-r--r--repositories/seed/defaults.go5
-rw-r--r--repositories/seed/messages.go7
-rw-r--r--repositories/seed/seed.go45
-rw-r--r--router/auth.go5
-rw-r--r--router/base.go3
-rw-r--r--router/dashboard.go19
-rw-r--r--router/domain.go19
-rw-r--r--services/auth/auth.go43
-rw-r--r--services/auth/messages.go7
-rw-r--r--services/domain/domain.go68
-rw-r--r--services/domain/messages.go16
-rw-r--r--services/domain/tld.go56
-rw-r--r--services/mail/mailboxes.go64
-rw-r--r--services/mail/messages.go14
-rw-r--r--services/mail/users.go52
-rw-r--r--tags/types.go2
-rw-r--r--tags/url.go2
-rw-r--r--templates/domains/domains.django5
-rw-r--r--templates/domains/htmx/domains.htmx.django79
-rw-r--r--templates/domains/htmx/newdomain.htmx.django42
-rw-r--r--templates/domains/htmx/newtld.htmx.django25
-rw-r--r--templates/domains/newdomain.django5
-rw-r--r--templates/domains/newtld.django5
-rw-r--r--templates/partials/sidebar.django7
-rw-r--r--types/auth.go6
-rw-r--r--types/errors.go12
-rw-r--r--types/mailbox.go16
-rw-r--r--types/overview.go7
-rw-r--r--types/request.go17
-rw-r--r--types/response.go13
-rw-r--r--types/user.go6
-rw-r--r--utils/collections/record.go3
-rw-r--r--utils/collections/set.go38
-rw-r--r--utils/collections/types.go3
-rw-r--r--utils/meta/builder.go46
-rw-r--r--utils/meta/constants.go9
-rw-r--r--utils/meta/defaults.go9
-rw-r--r--utils/meta/functions.go53
-rw-r--r--utils/meta/messages.go5
-rw-r--r--utils/meta/pagination.go71
-rw-r--r--utils/meta/request.go78
-rw-r--r--utils/meta/sorting.go39
-rw-r--r--utils/meta/types.go26
-rw-r--r--utils/meta/value.go35
-rw-r--r--utils/shortcuts/error.go51
-rw-r--r--utils/shortcuts/functions.go110
-rw-r--r--utils/shortcuts/messages.go5
-rw-r--r--utils/shortcuts/render.go108
-rw-r--r--utils/toml/load.go4
-rw-r--r--utils/urls/attach.go22
-rw-r--r--utils/urls/functions.go30
-rw-r--r--utils/urls/path.go45
-rw-r--r--utils/urls/registry.go27
-rw-r--r--utils/urls/types.go25
95 files changed, 1460 insertions, 684 deletions
diff --git a/controllers/domain.go b/controllers/domain.go
new file mode 100644
index 0000000..90a5727
--- /dev/null
+++ b/controllers/domain.go
@@ -0,0 +1,46 @@
+package controllers
+
+import (
+ domainService "dove/services/domain"
+ "dove/utils/meta"
+ "dove/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func CreateDomain(context *fiber.Ctx) error {
+ body, parseError := meta.Body[domainService.CreateDomainRequest](context)
+ if parseError != nil {
+ return shortcuts.BadRequest(context, parseError)
+ }
+
+ serviceError := domainService.CreateDomain(body)
+ if serviceError != nil {
+ return shortcuts.HandleError(context, serviceError)
+ }
+
+ return shortcuts.Redirect(context, "domains.index")
+}
+
+func CreateTLD(context *fiber.Ctx) error {
+ body, parseError := meta.Body[domainService.CreateTLDRequest](context)
+ if parseError != nil {
+ return shortcuts.BadRequest(context, parseError)
+ }
+
+ serviceError := domainService.CreateTLD(body)
+ if serviceError != nil {
+ return shortcuts.HandleError(context, serviceError)
+ }
+
+ return shortcuts.Redirect(context, "domains.index")
+}
+
+func DeleteTLD(context *fiber.Ctx) error {
+ serviceError := domainService.DeleteTLD(meta.Request(context).Param("name"))
+ if serviceError != nil {
+ return shortcuts.HandleError(context, serviceError)
+ }
+
+ return shortcuts.Redirect(context, "domains.index")
+} \ No newline at end of file
diff --git a/database/constants.go b/database/constants.go
deleted file mode 100644
index e16dcde..0000000
--- a/database/constants.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package database
-
-import "time"
-
-const (
- DATABASE_FILE_NAME = "dove.db"
- LOG_PREFIX = "Database"
- MAX_IDLE_CONNECTIONS = 5
- MAX_OPEN_CONNECTIONS = 25
- MAX_CONNECTION_LIFETIME = time.Hour
-)
diff --git a/database/database.go b/database/database.go
index ac06c98..888388c 100644
--- a/database/database.go
+++ b/database/database.go
@@ -1,7 +1,6 @@
package database
import (
- "dove/messages"
"dove/utils/logger"
"gorm.io/driver/sqlite"
@@ -20,19 +19,19 @@ func init() {
})
if connectionError != nil {
- logger.Fatalf(LOG_PREFIX, messages.DatabaseConnectionFailed, connectionError)
+ logger.Fatalf(LogPrefix, ConnectionFailed, connectionError)
}
sqlDB, poolError := DB.DB()
if poolError != nil {
- logger.Fatalf(LOG_PREFIX, messages.DatabasePoolConfigFailed, poolError)
+ logger.Fatalf(LogPrefix, PoolConfigFailed, poolError)
}
- sqlDB.SetMaxOpenConns(MAX_OPEN_CONNECTIONS)
- sqlDB.SetMaxIdleConns(MAX_IDLE_CONNECTIONS)
- sqlDB.SetConnMaxLifetime(MAX_CONNECTION_LIFETIME)
+ sqlDB.SetMaxOpenConns(MaxOpenConnections)
+ sqlDB.SetMaxIdleConns(MaxIdleConnections)
+ sqlDB.SetConnMaxLifetime(MaxConnectionLifetime)
- logger.Successf(LOG_PREFIX, messages.DatabaseConnected, databaseDSN)
+ logger.Successf(LogPrefix, Connected, databaseDSN)
migrate()
}
diff --git a/database/defaults.go b/database/defaults.go
new file mode 100644
index 0000000..821509b
--- /dev/null
+++ b/database/defaults.go
@@ -0,0 +1,11 @@
+package database
+
+import "time"
+
+const (
+ FileName = "dove.db"
+ LogPrefix = "Database"
+ MaxConnectionLifetime = time.Hour
+ MaxIdleConnections = 5
+ MaxOpenConnections = 25
+)
diff --git a/database/functions.go b/database/functions.go
index 71723ca..09d6ae4 100644
--- a/database/functions.go
+++ b/database/functions.go
@@ -9,7 +9,7 @@ import (
)
func resolveDatabasePath() string {
- return filepath.Join(config.DataDir, DATABASE_FILE_NAME)
+ return filepath.Join(config.DataDir, FileName)
}
func resolveGORMLogLevel() logger.Interface {
diff --git a/database/messages.go b/database/messages.go
new file mode 100644
index 0000000..2c51076
--- /dev/null
+++ b/database/messages.go
@@ -0,0 +1,8 @@
+package database
+
+const (
+ Connected = "Connected to %s."
+ ConnectionFailed = "Failed to connect to database: %v"
+ MigrationFailed = "Failed to run database migrations: %v"
+ PoolConfigFailed = "Failed to configure connection pool: %v"
+)
diff --git a/database/migration.go b/database/migration.go
index 4d8363c..e18f2d6 100644
--- a/database/migration.go
+++ b/database/migration.go
@@ -1,13 +1,15 @@
package database
import (
- "dove/messages"
"dove/models"
+ "dove/models/domain"
"dove/utils/logger"
)
func migrate() {
migrationError := DB.AutoMigrate(
+ &domain.TLD{},
+ &domain.Domain{},
&models.User{},
&models.Mailbox{},
&models.Alias{},
@@ -17,6 +19,6 @@ func migrate() {
)
if migrationError != nil {
- logger.Fatalf(LOG_PREFIX, messages.DatabaseMigrationFailed, migrationError)
+ logger.Fatalf(LogPrefix, MigrationFailed, migrationError)
}
}
diff --git a/enums/error.go b/enums/error.go
deleted file mode 100644
index b3dd10a..0000000
--- a/enums/error.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package enums
-
-type ErrorKind string
-
-const (
- BadRequest ErrorKind = "bad_request"
- Forbidden ErrorKind = "forbidden"
- Internal ErrorKind = "internal"
- NotFound ErrorKind = "not_found"
- Unauthorized ErrorKind = "unauthorized"
- Unprocessable ErrorKind = "unprocessable"
-)
diff --git a/enums/http.go b/enums/http.go
deleted file mode 100644
index fbd7591..0000000
--- a/enums/http.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package enums
-
-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"
-)
diff --git a/enums/mailbox.go b/enums/mailbox.go
deleted file mode 100644
index 24b2f4b..0000000
--- a/enums/mailbox.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package enums
-
-type MailboxMode string
-
-const (
- Registered MailboxMode = "registered"
- Catchall MailboxMode = "catchall"
-)
diff --git a/messages/auth.go b/messages/auth.go
deleted file mode 100644
index 0cad598..0000000
--- a/messages/auth.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package messages
-
-const (
- AuthAuthenticated = "Authenticated successfully."
- AuthInvalidCredentials = "Invalid username or password."
- AuthLoggedOut = "Logged out successfully."
-)
diff --git a/messages/config.go b/messages/config.go
deleted file mode 100644
index 43b40a6..0000000
--- a/messages/config.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package messages
-
-const (
- ConfigCreated = "Created default config at %s."
- ConfigCreateFailed = "Failed to create config file: %v"
- ConfigFileLoadFailed = "Failed to load config: %v"
- ConfigFileReadFailed = "Failed to read config file %s: %s."
- ConfigLoaded = "Configuration loaded successfully."
- ConfigPortCollision = "Port collision: %s and %s both configured on %s."
- ConfigPortValidFailed = "Port validation failed: %v"
- ConfigSectionInvalid = "Config section '%s' has invalid data."
- ConfigSectionParseFailed = "Failed to parse config section: %v"
-)
diff --git a/messages/database.go b/messages/database.go
deleted file mode 100644
index 36ba275..0000000
--- a/messages/database.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package messages
-
-const (
- DatabaseConnected = "Connected to %s."
- DatabaseConnectionFailed = "Failed to connect to database: %v"
- DatabaseMigrationFailed = "Failed to run database migrations: %v"
- DatabasePoolConfigFailed = "Failed to configure connection pool: %v"
-)
diff --git a/messages/email.go b/messages/email.go
deleted file mode 100644
index 28d6795..0000000
--- a/messages/email.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package messages
-
-const (
- EmailDirectoryCreateFailed = "Failed to create email directory: %v"
- EmailFileSaveFailed = "Failed to save email file: %v"
- EmailIndexFailed = "Failed to index email: %v"
- EmailParseFailed = "Failed to parse email: %v"
- EmailProcessed = "Email processed for %d recipient(s)."
- EmailStoreFailed = "Failed to store email: %v"
-)
diff --git a/messages/logger.go b/messages/logger.go
deleted file mode 100644
index 7f6e99b..0000000
--- a/messages/logger.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package messages
-
-const (
- LoggerNotInitialized = "Logger was not initialized."
-)
diff --git a/messages/mailbox.go b/messages/mailbox.go
deleted file mode 100644
index 9ead4f0..0000000
--- a/messages/mailbox.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package messages
-
-const (
- MailboxAutoCreated = "Auto-created mailbox for %s."
- MailboxNotRegistered = "No registered mailbox or alias for address: %s"
- MailboxAddressRequired = "Mailbox address is required."
- MailboxUserRequired = "A user must be selected for the mailbox."
- MailboxAlreadyExists = "A mailbox with this address already exists."
- MailboxCreationFailed = "Failed to create mailbox."
- MailboxUserNotFound = "The selected user does not exist."
-)
diff --git a/messages/meta.go b/messages/meta.go
deleted file mode 100644
index d0349eb..0000000
--- a/messages/meta.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package messages
-
-const (
- MetaRequestContextMissing = "Request context missing in fiber locals."
- MetaRequiredValueMissing = "Required value not found."
-)
diff --git a/messages/server.go b/messages/server.go
deleted file mode 100644
index 9f2b086..0000000
--- a/messages/server.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package messages
-
-const (
- ServerListenFailed = "Failed to start server: %v"
- ServerShutdownComplete = "Shutdown complete."
- ServerShutdownFailed = "Error during server shutdown: %v"
- ServerShuttingDown = "Shutting down gracefully..."
- ServerStarting = "Server started on %s."
-)
diff --git a/messages/session.go b/messages/session.go
deleted file mode 100644
index b35b85f..0000000
--- a/messages/session.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package messages
-
-const (
- SessionInitialized = "Session store initialized."
-)
diff --git a/messages/shortcuts.go b/messages/shortcuts.go
deleted file mode 100644
index b0c0d91..0000000
--- a/messages/shortcuts.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package messages
-
-const (
- ShortcutUnsupportedBindType = "Bind data must be a struct, *struct, fiber.Map, or map[string]any."
-) \ No newline at end of file
diff --git a/messages/smtp.go b/messages/smtp.go
deleted file mode 100644
index eef071c..0000000
--- a/messages/smtp.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package messages
-
-const (
- SMTPListenFailed = "Failed to start %s listener: %v"
- SMTPSessionStarted = "New session from %s."
- SMTPMailFrom = "Mail from: %s"
- SMTPRecipient = "Recipient: %s"
- SMTPMessageReceived = "Message received (%d bytes)."
- SMTPMessageStoreFailed = "Failed to store message: %v"
- SMTPServerStarting = "%s listener started on %s."
- SMTPShutdownFailed = "Failed to shutdown %s listener: %v"
- SMTPShutdownComplete = "All listeners stopped."
- SMTPAuthFailed = "Authentication failed for user: %s"
- SMTPInvalidCredentials = "Invalid credentials."
-)
diff --git a/messages/tags.go b/messages/tags.go
deleted file mode 100644
index b54090a..0000000
--- a/messages/tags.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package messages
-
-const (
- TagExpectedEquals = "Expected '=' after parameter key."
- TagExpectedParamKey = "Expected parameter key identifier."
- TagExpectedRouteName = "Expected route name string."
- TagExpectedVariableName = "Expected variable name after 'as'."
- TagRegistrationFailed = "Failed to register tag: %s."
- TagRouteNotFound = "Route not found: %s."
- TagTemplateWriteFailed = "Failed to write template output."
-)
diff --git a/messages/toml.go b/messages/toml.go
deleted file mode 100644
index 9ecc069..0000000
--- a/messages/toml.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package messages
-
-const (
- ParseTargetMustBeStructPointer = "Parse target must be a pointer to a struct."
-)
diff --git a/messages/user.go b/messages/user.go
deleted file mode 100644
index 5c3fe93..0000000
--- a/messages/user.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package messages
-
-const (
- UserUsernameRequired = "Username is required."
- UserDisplayNameRequired = "Display name is required."
- UserAlreadyExists = "A user with this username already exists."
- UserCreationFailed = "Failed to create user."
-)
diff --git a/middleware/request.go b/middleware/request.go
index 21f8357..da3aec1 100644
--- a/middleware/request.go
+++ b/middleware/request.go
@@ -7,6 +7,6 @@ import (
)
func requestBuilder(context *fiber.Ctx) error {
- context.Locals(meta.REQUEST_KEY, meta.BuildRequest(context))
+ context.Locals(meta.RequestKey, meta.BuildRequest(context))
return context.Next()
}
diff --git a/models/domain/domain.go b/models/domain/domain.go
new file mode 100644
index 0000000..9a1a0ef
--- /dev/null
+++ b/models/domain/domain.go
@@ -0,0 +1,10 @@
+package domain
+
+import "gorm.io/gorm"
+
+type Domain struct {
+ gorm.Model
+ Name string `gorm:"not null;uniqueIndex:idx_domain_tld" json:"name"`
+ TLDID uint `gorm:"not null;uniqueIndex:idx_domain_tld" json:"tld_id"`
+ TLD TLD `gorm:"foreignKey:TLDID" json:"tld"`
+}
diff --git a/models/domain/tld.go b/models/domain/tld.go
new file mode 100644
index 0000000..5a47f32
--- /dev/null
+++ b/models/domain/tld.go
@@ -0,0 +1,10 @@
+package domain
+
+import "gorm.io/gorm"
+
+type TLD struct {
+ gorm.Model
+ Name string `gorm:"uniqueIndex;not null" json:"name"`
+ IsDefault bool `gorm:"default:false" json:"is_default"`
+ Domains []Domain `gorm:"foreignKey:TLDID" json:"domains"`
+}
diff --git a/models/mail/alias.go b/models/mail/alias.go
new file mode 100644
index 0000000..c2cd752
--- /dev/null
+++ b/models/mail/alias.go
@@ -0,0 +1,10 @@
+package mail
+
+import "gorm.io/gorm"
+
+type Alias struct {
+ gorm.Model
+ SourceAddress string `gorm:"uniqueIndex;not null" json:"source_address"`
+ MailboxID uint `gorm:"not null" json:"mailbox_id"`
+ Mailbox Mailbox `gorm:"foreignKey:MailboxID" json:"mailbox"`
+}
diff --git a/models/mail/attachment.go b/models/mail/attachment.go
new file mode 100644
index 0000000..fcff37e
--- /dev/null
+++ b/models/mail/attachment.go
@@ -0,0 +1,14 @@
+package mail
+
+import "gorm.io/gorm"
+
+type Attachment struct {
+ gorm.Model
+ EmailID uint `gorm:"not null;index" json:"email_id"`
+ Email Email `gorm:"foreignKey:EmailID" json:"email"`
+ Filename string `gorm:"not null" json:"filename"`
+ ContentType string `gorm:"not null" json:"content_type"`
+ ContentID string `json:"content_id"`
+ Size int64 `json:"size"`
+ IsInline bool `gorm:"default:false" json:"is_inline"`
+}
diff --git a/models/mail/email.go b/models/mail/email.go
new file mode 100644
index 0000000..4b8b55e
--- /dev/null
+++ b/models/mail/email.go
@@ -0,0 +1,25 @@
+package mail
+
+import "gorm.io/gorm"
+
+type Email struct {
+ gorm.Model
+ MailboxID uint `gorm:"not null;index" json:"mailbox_id"`
+ Mailbox Mailbox `gorm:"foreignKey:MailboxID" json:"mailbox"`
+ MessageID string `gorm:"index" json:"message_id"`
+ Filename string `gorm:"uniqueIndex;not null" json:"filename"`
+ FromAddress string `gorm:"not null" json:"from_address"`
+ FromName string `json:"from_name"`
+ ToAddresses string `gorm:"not null" json:"to_addresses"`
+ CcAddresses string `json:"cc_addresses"`
+ BccAddresses string `json:"bcc_addresses"`
+ ReplyToAddress string `json:"reply_to_address"`
+ ReturnPath string `json:"return_path"`
+ Subject string `json:"subject"`
+ Snippet string `json:"snippet"`
+ Size int64 `json:"size"`
+ IsRead bool `gorm:"default:false;index" json:"is_read"`
+ AttachmentCount int `gorm:"default:0" json:"attachment_count"`
+ InlineCount int `gorm:"default:0" json:"inline_count"`
+ Tags []Tag `gorm:"many2many:email_tags" json:"tags"`
+}
diff --git a/models/mail/mailbox.go b/models/mail/mailbox.go
new file mode 100644
index 0000000..62e3f1d
--- /dev/null
+++ b/models/mail/mailbox.go
@@ -0,0 +1,12 @@
+package mail
+
+import "gorm.io/gorm"
+
+type Mailbox struct {
+ gorm.Model
+ Address string `gorm:"uniqueIndex;not null" json:"address"`
+ UserID uint `gorm:"not null" json:"user_id"`
+ User User `gorm:"foreignKey:UserID" json:"user"`
+ Aliases []Alias `gorm:"foreignKey:MailboxID" json:"aliases"`
+ Emails []Email `gorm:"foreignKey:MailboxID" json:"emails"`
+}
diff --git a/models/mail/tag.go b/models/mail/tag.go
new file mode 100644
index 0000000..65d2bd3
--- /dev/null
+++ b/models/mail/tag.go
@@ -0,0 +1,9 @@
+package mail
+
+import "gorm.io/gorm"
+
+type Tag struct {
+ gorm.Model
+ Name string `gorm:"uniqueIndex;not null" json:"name"`
+ Emails []Email `gorm:"many2many:email_tags" json:"emails"`
+}
diff --git a/models/mail/user.go b/models/mail/user.go
new file mode 100644
index 0000000..68241f6
--- /dev/null
+++ b/models/mail/user.go
@@ -0,0 +1,10 @@
+package mail
+
+import "gorm.io/gorm"
+
+type User struct {
+ gorm.Model
+ Username string `gorm:"uniqueIndex;not null" json:"username"`
+ DisplayName string `json:"display_name"`
+ Mailboxes []Mailbox `gorm:"foreignKey:UserID" json:"mailboxes"`
+}
diff --git a/pages/domain.go b/pages/domain.go
new file mode 100644
index 0000000..d46af9c
--- /dev/null
+++ b/pages/domain.go
@@ -0,0 +1,24 @@
+package pages
+
+import (
+ domainService "dove/services/domain"
+ "dove/utils/meta"
+ "dove/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func Domains(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "Domains")
+ return shortcuts.Render(context, "domains/domains", domainService.ListDomains())
+}
+
+func NewDomain(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "New Domain")
+ return shortcuts.Render(context, "domains/newdomain", domainService.DomainFormData())
+}
+
+func NewTLD(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "New TLD")
+ return shortcuts.Render(context, "domains/newtld", nil)
+} \ No newline at end of file
diff --git a/repositories/domain/domain.go b/repositories/domain/domain.go
new file mode 100644
index 0000000..406e272
--- /dev/null
+++ b/repositories/domain/domain.go
@@ -0,0 +1,38 @@
+package domain
+
+import (
+ "dove/database"
+ "dove/models/domain"
+
+ "gorm.io/gorm"
+)
+
+func AllDomains() []domain.Domain {
+ var domains []domain.Domain
+ database.DB.Preload("TLD").Order("name ASC").Find(&domains)
+ return domains
+}
+
+func FindDomainByFullName(name string, tldName string) *domain.Domain {
+ var foundDomain domain.Domain
+ result := database.DB.
+ Joins("TLD").
+ Where("domains.name = ? AND TLD.name = ?", name, tldName).
+ First(&foundDomain)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+ return &foundDomain
+}
+
+func CreateDomain(newDomain *domain.Domain) error {
+ return database.DB.Create(newDomain).Error
+}
+
+func UpdateDomain(updatedDomain *domain.Domain) error {
+ return database.DB.Save(updatedDomain).Error
+}
+
+func DeleteDomain(targetDomain *domain.Domain) error {
+ return database.DB.Delete(targetDomain).Error
+}
diff --git a/repositories/domain/tld.go b/repositories/domain/tld.go
new file mode 100644
index 0000000..c48e213
--- /dev/null
+++ b/repositories/domain/tld.go
@@ -0,0 +1,35 @@
+package domain
+
+import (
+ "dove/database"
+ "dove/models/domain"
+
+ "gorm.io/gorm"
+)
+
+func AllTLDs() []domain.TLD {
+ var tlds []domain.TLD
+ database.DB.Order("name ASC").Find(&tlds)
+ return tlds
+}
+
+func FindTLDByName(name string) *domain.TLD {
+ var tld domain.TLD
+ result := database.DB.Where("name = ?", name).First(&tld)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+ return &tld
+}
+
+func CreateTLD(tld *domain.TLD) error {
+ return database.DB.Create(tld).Error
+}
+
+func UpdateTLD(tld *domain.TLD) error {
+ return database.DB.Save(tld).Error
+}
+
+func DeleteTLD(tld *domain.TLD) error {
+ return database.DB.Delete(tld).Error
+}
diff --git a/repositories/init/init.go b/repositories/init/init.go
new file mode 100644
index 0000000..bdfe0fa
--- /dev/null
+++ b/repositories/init/init.go
@@ -0,0 +1,7 @@
+package init
+
+import "dove/repositories/seed"
+
+func init() {
+ seed.Run()
+}
diff --git a/repositories/mail/alias.go b/repositories/mail/alias.go
new file mode 100644
index 0000000..49a5396
--- /dev/null
+++ b/repositories/mail/alias.go
@@ -0,0 +1,18 @@
+package mail
+
+import (
+ "dove/database"
+ "dove/models/mail"
+
+ "gorm.io/gorm"
+)
+
+func FindAliasByAddress(address string) *mail.Alias {
+ var alias mail.Alias
+ result := database.DB.Preload("Mailbox").Where("source_address = ?", address).First(&alias)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &alias
+}
diff --git a/repositories/mail/email.go b/repositories/mail/email.go
new file mode 100644
index 0000000..3a80b92
--- /dev/null
+++ b/repositories/mail/email.go
@@ -0,0 +1,50 @@
+package mail
+
+import (
+ "dove/database"
+ "dove/models/mail"
+ "dove/utils/meta"
+)
+
+func CreateEmail(email *mail.Email) error {
+ return database.DB.Create(email).Error
+}
+
+func CreateAttachment(attachment *mail.Attachment) error {
+ return database.DB.Create(attachment).Error
+}
+
+func ListEmails(pagination meta.Pagination, sorting meta.Sorting, search string) ([]mail.Email, int64) {
+ var emails []mail.Email
+ var total int64
+
+ query := database.DB.Model(&mail.Email{})
+
+ if search != "" {
+ like := "%" + search + "%"
+ query = query.Where("from_address LIKE ? OR to_addresses LIKE ? OR subject LIKE ?", like, like, like)
+ }
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("Tags").Find(&emails)
+
+ return emails, total
+}
+
+func CountEmails() int64 {
+ var count int64
+ database.DB.Model(&mail.Email{}).Count(&count)
+ return count
+}
+
+func ListEmailsByMailbox(mailboxID uint, pagination meta.Pagination, sorting meta.Sorting) ([]mail.Email, int64) {
+ var emails []mail.Email
+ var total int64
+
+ query := database.DB.Model(&mail.Email{}).Where("mailbox_id = ?", mailboxID)
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("Tags").Find(&emails)
+
+ return emails, total
+}
diff --git a/repositories/mail/mailbox.go b/repositories/mail/mailbox.go
new file mode 100644
index 0000000..56025a6
--- /dev/null
+++ b/repositories/mail/mailbox.go
@@ -0,0 +1,46 @@
+package mail
+
+import (
+ "dove/database"
+ "dove/models/mail"
+ "dove/utils/meta"
+
+ "gorm.io/gorm"
+)
+
+func FindMailboxByAddress(address string) *mail.Mailbox {
+ var mailbox mail.Mailbox
+ result := database.DB.Where("address = ?", address).First(&mailbox)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &mailbox
+}
+
+func CreateMailbox(mailbox *mail.Mailbox) error {
+ return database.DB.Create(mailbox).Error
+}
+
+func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) ([]mail.Mailbox, int64) {
+ var mailboxes []mail.Mailbox
+ var total int64
+
+ query := database.DB.Model(&mail.Mailbox{})
+
+ if search != "" {
+ like := "%" + search + "%"
+ query = query.Where("address LIKE ?", like)
+ }
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("User").Find(&mailboxes)
+
+ return mailboxes, total
+}
+
+func CountMailboxes() int64 {
+ var count int64
+ database.DB.Model(&mail.Mailbox{}).Count(&count)
+ return count
+}
diff --git a/repositories/mail/user.go b/repositories/mail/user.go
new file mode 100644
index 0000000..a045dc5
--- /dev/null
+++ b/repositories/mail/user.go
@@ -0,0 +1,62 @@
+package mail
+
+import (
+ "dove/database"
+ "dove/models/mail"
+ "dove/utils/meta"
+
+ "gorm.io/gorm"
+)
+
+func FindUserByID(userID uint) *mail.User {
+ var user mail.User
+ result := database.DB.First(&user, userID)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &user
+}
+
+func FindUserByUsername(username string) *mail.User {
+ var user mail.User
+ result := database.DB.Where("username = ?", username).First(&user)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &user
+}
+
+func CreateUser(user *mail.User) error {
+ return database.DB.Create(user).Error
+}
+
+func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) ([]mail.User, int64) {
+ var users []mail.User
+ var total int64
+
+ query := database.DB.Model(&mail.User{})
+
+ if search != "" {
+ like := "%" + search + "%"
+ query = query.Where("username LIKE ? OR display_name LIKE ?", like, like)
+ }
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("Mailboxes").Find(&users)
+
+ return users, total
+}
+
+func AllUsers() []mail.User {
+ var users []mail.User
+ database.DB.Order("username ASC").Find(&users)
+ return users
+}
+
+func CountUsers() int64 {
+ var count int64
+ database.DB.Model(&mail.User{}).Count(&count)
+ return count
+}
diff --git a/repositories/seed/defaults.go b/repositories/seed/defaults.go
new file mode 100644
index 0000000..6055c9e
--- /dev/null
+++ b/repositories/seed/defaults.go
@@ -0,0 +1,5 @@
+package seed
+
+import "dove/utils/collections"
+
+var DefaultTLDs = collections.SetOf("dove", "local", "nest", "test")
diff --git a/repositories/seed/messages.go b/repositories/seed/messages.go
new file mode 100644
index 0000000..d57e101
--- /dev/null
+++ b/repositories/seed/messages.go
@@ -0,0 +1,7 @@
+package seed
+
+const (
+ LogPrefix = "Seed"
+ SeedComplete = "Seeded %d default TLDs."
+ SeedFailed = "Failed to seed TLD %s: %v"
+)
diff --git a/repositories/seed/seed.go b/repositories/seed/seed.go
new file mode 100644
index 0000000..c180bb6
--- /dev/null
+++ b/repositories/seed/seed.go
@@ -0,0 +1,45 @@
+package seed
+
+import (
+ "dove/database"
+ "dove/models/domain"
+ "dove/utils/logger"
+)
+
+func Run() {
+ seededCount := 0
+
+ for _, tldName := range DefaultTLDs.All() {
+ if tldExists(tldName) {
+ continue
+ }
+
+ if seedTLD(tldName) {
+ seededCount++
+ }
+ }
+
+ if seededCount > 0 {
+ logger.Successf(LogPrefix, SeedComplete, seededCount)
+ }
+}
+
+func tldExists(name string) bool {
+ var count int64
+ database.DB.Model(&domain.TLD{}).Where("name = ?", name).Count(&count)
+ return count > 0
+}
+
+func seedTLD(name string) bool {
+ tld := &domain.TLD{
+ Name: name,
+ IsDefault: true,
+ }
+
+ if createError := database.DB.Create(tld).Error; createError != nil {
+ logger.Errorf(LogPrefix, SeedFailed, name, createError)
+ return false
+ }
+
+ return true
+}
diff --git a/router/auth.go b/router/auth.go
index f8f41fc..a0261f1 100644
--- a/router/auth.go
+++ b/router/auth.go
@@ -2,13 +2,12 @@ package router
import (
"dove/controllers"
- "dove/enums"
"dove/utils/urls"
)
func init() {
urls.SetNamespace("auth")
- urls.Path(enums.Post, "/login", controllers.Login, "login")
- urls.Path(enums.Get, "/logout", controllers.Logout, "logout")
+ urls.Path(urls.Post, "/login", controllers.Login, "login")
+ urls.Path(urls.Get, "/logout", controllers.Logout, "logout")
}
diff --git a/router/base.go b/router/base.go
index 3a5e7ee..c70c685 100644
--- a/router/base.go
+++ b/router/base.go
@@ -1,7 +1,6 @@
package router
import (
- "dove/enums"
"dove/pages"
"dove/utils/urls"
)
@@ -9,5 +8,5 @@ import (
func init() {
urls.SetNamespace("")
- urls.Path(enums.Get, "/", pages.Home, "home")
+ urls.Path(urls.Get, "/", pages.Home, "home")
}
diff --git a/router/dashboard.go b/router/dashboard.go
index ff7aa52..8baa4b8 100644
--- a/router/dashboard.go
+++ b/router/dashboard.go
@@ -2,7 +2,6 @@ package router
import (
"dove/controllers"
- "dove/enums"
"dove/pages"
"dove/utils/auth"
"dove/utils/urls"
@@ -11,12 +10,12 @@ import (
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/new", auth.RequireAuthentication(pages.NewMailbox), "mailboxes.new")
- urls.Path(enums.Post, "/mailboxes", auth.RequireAuthentication(controllers.CreateMailbox), "mailboxes.create")
- urls.Path(enums.Get, "/mailboxes/:address", auth.RequireAuthentication(pages.Mailbox), "mailbox")
- urls.Path(enums.Get, "/users", auth.RequireAuthentication(pages.Users), "users")
- urls.Path(enums.Get, "/users/new", auth.RequireAuthentication(pages.NewUser), "users.new")
- urls.Path(enums.Post, "/users", auth.RequireAuthentication(controllers.CreateUser), "users.create")
-} \ No newline at end of file
+ urls.Path(urls.Get, "/", auth.RequireAuthentication(pages.Dashboard), "index")
+ urls.Path(urls.Get, "/mailboxes", auth.RequireAuthentication(pages.Mailboxes), "mailboxes")
+ urls.Path(urls.Get, "/mailboxes/new", auth.RequireAuthentication(pages.NewMailbox), "mailboxes.new")
+ urls.Path(urls.Post, "/mailboxes", auth.RequireAuthentication(controllers.CreateMailbox), "mailboxes.create")
+ urls.Path(urls.Get, "/mailboxes/:address", auth.RequireAuthentication(pages.Mailbox), "mailbox")
+ urls.Path(urls.Get, "/users", auth.RequireAuthentication(pages.Users), "users")
+ urls.Path(urls.Get, "/users/new", auth.RequireAuthentication(pages.NewUser), "users.new")
+ urls.Path(urls.Post, "/users", auth.RequireAuthentication(controllers.CreateUser), "users.create")
+}
diff --git a/router/domain.go b/router/domain.go
new file mode 100644
index 0000000..00ecf36
--- /dev/null
+++ b/router/domain.go
@@ -0,0 +1,19 @@
+package router
+
+import (
+ "dove/controllers"
+ "dove/pages"
+ "dove/utils/auth"
+ "dove/utils/urls"
+)
+
+func init() {
+ urls.SetNamespace("domains")
+
+ urls.Path(urls.Get, "/domains", auth.RequireAuthentication(pages.Domains), "index")
+ urls.Path(urls.Get, "/domains/new", auth.RequireAuthentication(pages.NewDomain), "new")
+ urls.Path(urls.Post, "/domains", auth.RequireAuthentication(controllers.CreateDomain), "create")
+ urls.Path(urls.Get, "/domains/tlds/new", auth.RequireAuthentication(pages.NewTLD), "tlds.new")
+ urls.Path(urls.Post, "/domains/tlds", auth.RequireAuthentication(controllers.CreateTLD), "tlds.create")
+ urls.Path(urls.Delete, "/domains/tlds/:name", auth.RequireAuthentication(controllers.DeleteTLD), "tlds.delete")
+}
diff --git a/services/auth/auth.go b/services/auth/auth.go
new file mode 100644
index 0000000..89ef205
--- /dev/null
+++ b/services/auth/auth.go
@@ -0,0 +1,43 @@
+package auth
+
+import (
+ "dove/config"
+ authUtils "dove/utils/auth"
+ "dove/utils/shortcuts"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+type LoginRequest struct {
+ Username string `form:"username"`
+ Password string `form:"password"`
+}
+
+type MessageResponse struct {
+ Message string
+}
+
+func Authenticate(context *fiber.Ctx, request LoginRequest) (*MessageResponse, *shortcuts.Error) {
+ switch request.Username == config.Server.Username && request.Password == config.Server.Password {
+ case true:
+ if sessionError := authUtils.Authenticate(context); sessionError != nil {
+ return nil, shortcuts.ServiceError(shortcuts.Internal, sessionError.Error())
+ }
+
+ return &MessageResponse{
+ Message: Authenticated,
+ }, nil
+ default:
+ return nil, shortcuts.ServiceError(shortcuts.Unauthorized, InvalidCredentials)
+ }
+}
+
+func Deauthenticate(context *fiber.Ctx) (*MessageResponse, *shortcuts.Error) {
+ if sessionError := authUtils.Deauthenticate(context); sessionError != nil {
+ return nil, shortcuts.ServiceError(shortcuts.Internal, sessionError.Error())
+ }
+
+ return &MessageResponse{
+ Message: LoggedOut,
+ }, nil
+} \ No newline at end of file
diff --git a/services/auth/messages.go b/services/auth/messages.go
new file mode 100644
index 0000000..f295dce
--- /dev/null
+++ b/services/auth/messages.go
@@ -0,0 +1,7 @@
+package auth
+
+const (
+ Authenticated = "Authenticated successfully."
+ InvalidCredentials = "Invalid username or password."
+ LoggedOut = "Logged out successfully."
+)
diff --git a/services/domain/domain.go b/services/domain/domain.go
new file mode 100644
index 0000000..39d5894
--- /dev/null
+++ b/services/domain/domain.go
@@ -0,0 +1,68 @@
+package domain
+
+import (
+ "strings"
+
+ domainModel "dove/models/domain"
+ domainRepo "dove/repositories/domain"
+ "dove/utils/shortcuts"
+)
+
+type CreateDomainRequest struct {
+ Name string `form:"name"`
+ TLDName string `form:"tld_name"`
+}
+
+type DomainListResponse struct {
+ Domains []domainModel.Domain `json:"domains"`
+ TLDs []domainModel.TLD `json:"tlds"`
+}
+
+type DomainFormResponse struct {
+ TLDs []domainModel.TLD `json:"tlds"`
+}
+
+func ListDomains() DomainListResponse {
+ return DomainListResponse{
+ Domains: domainRepo.AllDomains(),
+ TLDs: domainRepo.AllTLDs(),
+ }
+}
+
+func DomainFormData() DomainFormResponse {
+ return DomainFormResponse{
+ TLDs: domainRepo.AllTLDs(),
+ }
+}
+
+func CreateDomain(request CreateDomainRequest) *shortcuts.Error {
+ name := strings.TrimSpace(strings.ToLower(request.Name))
+ tldName := strings.TrimSpace(strings.ToLower(request.TLDName))
+
+ switch {
+ case name == "":
+ return shortcuts.ServiceError(shortcuts.BadRequest, DomainNameRequired)
+ case tldName == "":
+ return shortcuts.ServiceError(shortcuts.BadRequest, DomainTLDRequired)
+ }
+
+ tld := domainRepo.FindTLDByName(tldName)
+
+ switch {
+ case tld == nil:
+ return shortcuts.ServiceError(shortcuts.Unprocessable, TLDNotFound)
+ case domainRepo.FindDomainByFullName(name, tldName) != nil:
+ return shortcuts.ServiceError(shortcuts.Unprocessable, DomainAlreadyExists)
+ }
+
+ newDomain := &domainModel.Domain{
+ Name: name,
+ TLDID: tld.ID,
+ }
+
+ if createError := domainRepo.CreateDomain(newDomain); createError != nil {
+ return shortcuts.ServiceError(shortcuts.Internal, DomainCreationFailed)
+ }
+
+ return nil
+}
diff --git a/services/domain/messages.go b/services/domain/messages.go
new file mode 100644
index 0000000..5e42a3a
--- /dev/null
+++ b/services/domain/messages.go
@@ -0,0 +1,16 @@
+package domain
+
+const (
+ DomainAlreadyExists = "A domain with this name already exists under this TLD."
+ DomainCreationFailed = "Failed to create domain."
+ DomainNameRequired = "Domain name is required."
+ DomainNotFound = "Domain not found."
+ DomainTLDRequired = "A TLD must be selected for the domain."
+ TLDAlreadyExists = "A TLD with this name already exists."
+ TLDCreationFailed = "Failed to create TLD."
+ TLDDeletionFailed = "Failed to delete TLD."
+ TLDNameRequired = "TLD name is required."
+ TLDNotFound = "TLD not found."
+ TLDProtected = "Default TLDs cannot be deleted."
+ TLDUpdateFailed = "Failed to update TLD."
+)
diff --git a/services/domain/tld.go b/services/domain/tld.go
new file mode 100644
index 0000000..eb80c14
--- /dev/null
+++ b/services/domain/tld.go
@@ -0,0 +1,56 @@
+package domain
+
+import (
+ "strings"
+
+ domainModel "dove/models/domain"
+ domainRepo "dove/repositories/domain"
+ "dove/utils/shortcuts"
+)
+
+type CreateTLDRequest struct {
+ Name string `form:"name"`
+}
+
+func AllTLDs() []domainModel.TLD {
+ return domainRepo.AllTLDs()
+}
+
+func CreateTLD(request CreateTLDRequest) *shortcuts.Error {
+ name := strings.TrimSpace(strings.ToLower(request.Name))
+
+ switch {
+ case name == "":
+ return shortcuts.ServiceError(shortcuts.BadRequest, TLDNameRequired)
+ case domainRepo.FindTLDByName(name) != nil:
+ return shortcuts.ServiceError(shortcuts.Unprocessable, TLDAlreadyExists)
+ }
+
+ tld := &domainModel.TLD{
+ Name: name,
+ IsDefault: false,
+ }
+
+ if createError := domainRepo.CreateTLD(tld); createError != nil {
+ return shortcuts.ServiceError(shortcuts.Internal, TLDCreationFailed)
+ }
+
+ return nil
+}
+
+func DeleteTLD(name string) *shortcuts.Error {
+ tld := domainRepo.FindTLDByName(name)
+
+ switch {
+ case tld == nil:
+ return shortcuts.ServiceError(shortcuts.NotFound, TLDNotFound)
+ case tld.IsDefault:
+ return shortcuts.ServiceError(shortcuts.Forbidden, TLDProtected)
+ }
+
+ if deleteError := domainRepo.DeleteTLD(tld); deleteError != nil {
+ return shortcuts.ServiceError(shortcuts.Internal, TLDDeletionFailed)
+ }
+
+ return nil
+}
diff --git a/services/mail/mailboxes.go b/services/mail/mailboxes.go
new file mode 100644
index 0000000..b124dc9
--- /dev/null
+++ b/services/mail/mailboxes.go
@@ -0,0 +1,64 @@
+package mail
+
+import (
+ "strings"
+
+ "dove/models"
+ "dove/repositories"
+ "dove/utils/meta"
+ "dove/utils/shortcuts"
+)
+
+type CreateMailboxRequest struct {
+ Address string `form:"address"`
+ UserID uint `form:"user_id"`
+}
+
+type MailboxFormResponse struct {
+ Users []models.User `json:"users"`
+}
+
+type MailboxView struct {
+ Address string
+}
+
+func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) meta.PaginatedResponse {
+ mailboxes, total := repositories.ListMailboxes(pagination, sorting, search)
+ return pagination.Response(mailboxes, total)
+}
+
+func MailboxFormData() MailboxFormResponse {
+ return MailboxFormResponse{
+ Users: repositories.AllUsers(),
+ }
+}
+
+func CreateMailbox(request CreateMailboxRequest) *shortcuts.Error {
+ address := strings.TrimSpace(request.Address)
+
+ switch {
+ case address == "":
+ return shortcuts.ServiceError(shortcuts.BadRequest, AddressRequired)
+ case request.UserID == 0:
+ return shortcuts.ServiceError(shortcuts.BadRequest, UserRequired)
+ }
+
+ if repositories.FindUserByID(request.UserID) == nil {
+ return shortcuts.ServiceError(shortcuts.Unprocessable, UserNotFound)
+ }
+
+ if repositories.FindMailboxByAddress(address) != nil {
+ return shortcuts.ServiceError(shortcuts.Unprocessable, AlreadyExists)
+ }
+
+ mailbox := &models.Mailbox{
+ Address: address,
+ UserID: request.UserID,
+ }
+
+ if createError := repositories.CreateMailbox(mailbox); createError != nil {
+ return shortcuts.ServiceError(shortcuts.Internal, CreationFailed)
+ }
+
+ return nil
+}
diff --git a/services/mail/messages.go b/services/mail/messages.go
new file mode 100644
index 0000000..300f716
--- /dev/null
+++ b/services/mail/messages.go
@@ -0,0 +1,14 @@
+package mail
+
+const (
+ AddressRequired = "Mailbox address is required."
+ AlreadyExists = "A mailbox with this address already exists."
+ CreationFailed = "Failed to create mailbox."
+ UserNotFound = "The selected user does not exist."
+ UserRequired = "A user must be selected for the mailbox."
+
+ DisplayNameRequired = "Display name is required."
+ UserAlreadyExists = "A user with this username already exists."
+ UserCreationFailed = "Failed to create user."
+ UsernameRequired = "Username is required."
+)
diff --git a/services/mail/users.go b/services/mail/users.go
new file mode 100644
index 0000000..9520219
--- /dev/null
+++ b/services/mail/users.go
@@ -0,0 +1,52 @@
+package mail
+
+import (
+ "strings"
+
+ "dove/models"
+ "dove/repositories"
+ "dove/utils/meta"
+ "dove/utils/shortcuts"
+)
+
+type CreateUserRequest struct {
+ Username string `form:"username"`
+ DisplayName string `form:"display_name"`
+}
+
+func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) meta.PaginatedResponse {
+ users, total := repositories.ListUsers(pagination, sorting, search)
+ return pagination.Response(users, total)
+}
+
+func CreateUser(request CreateUserRequest) *shortcuts.Error {
+ username := strings.TrimSpace(request.Username)
+ displayName := strings.TrimSpace(request.DisplayName)
+
+ if username == "" {
+ return shortcuts.ServiceError(shortcuts.BadRequest, UsernameRequired)
+ }
+
+ if displayName == "" {
+ return shortcuts.ServiceError(shortcuts.BadRequest, DisplayNameRequired)
+ }
+
+ if repositories.FindUserByUsername(username) != nil {
+ return shortcuts.ServiceError(shortcuts.Unprocessable, UserAlreadyExists)
+ }
+
+ newUser := &models.User{
+ Username: username,
+ DisplayName: displayName,
+ }
+
+ if createError := repositories.CreateUser(newUser); createError != nil {
+ return shortcuts.ServiceError(shortcuts.Internal, UserCreationFailed)
+ }
+
+ return nil
+}
+
+func AllUsers() []models.User {
+ return repositories.AllUsers()
+}
diff --git a/tags/types.go b/tags/types.go
index 144e05a..8e44dbe 100644
--- a/tags/types.go
+++ b/tags/types.go
@@ -13,6 +13,6 @@ type templateTag struct {
type urlNode struct {
routeName string
- params collections.Record[pongo2.IEvaluator]
+ params collections.Record[string, pongo2.IEvaluator]
variableName string
}
diff --git a/tags/url.go b/tags/url.go
index 0085679..10aa6be 100644
--- a/tags/url.go
+++ b/tags/url.go
@@ -18,7 +18,7 @@ func url(document *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser)
return nil, arguments.Error(messages.TagExpectedRouteName, nil)
}
- params := make(collections.Record[pongo2.IEvaluator])
+ params := make(collections.Record[string, pongo2.IEvaluator])
var variableName string
diff --git a/templates/domains/domains.django b/templates/domains/domains.django
new file mode 100644
index 0000000..8152eb4
--- /dev/null
+++ b/templates/domains/domains.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "domains/htmx/domains.htmx.django" %}
+{% endblock %} \ No newline at end of file
diff --git a/templates/domains/htmx/domains.htmx.django b/templates/domains/htmx/domains.htmx.django
new file mode 100644
index 0000000..972bbf5
--- /dev/null
+++ b/templates/domains/htmx/domains.htmx.django
@@ -0,0 +1,79 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up space-y-6">
+ <div class="glass rounded-xl glow-border">
+ <div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">TLDs</h2>
+ <div class="flex items-center gap-3">
+ <span class="text-xs text-zinc-600">{{ tlds|length }} total</span>
+ {% url "domains.tlds.new" as new_tld_path %}
+ <a href="{{ new_tld_path }}" hx-get="{{ new_tld_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New TLD</a>
+ </div>
+ </div>
+ {% if tlds %}
+ <div class="divide-y divide-white/[0.04]">
+ {% for tld in tlds %}
+ <div class="flex items-center justify-between px-5 py-3">
+ <div class="flex items-center gap-3">
+ <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="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
+ </svg>
+ </div>
+ <div>
+ <p class="text-sm text-zinc-200">.{{ tld.Name }}</p>
+ {% if tld.IsDefault %}
+ <p class="text-xs text-zinc-600">Default</p>
+ {% endif %}
+ </div>
+ </div>
+ {% if not tld.IsDefault %}
+ {% url "domains.tlds.delete" tld.Name as delete_tld_path %}
+ <button hx-delete="{{ delete_tld_path }}" hx-confirm="Delete .{{ tld.Name }}?" hx-target="#content" hx-swap="innerHTML" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
+ {% endif %}
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="flex flex-col items-center justify-center py-16 text-center">
+ <p class="text-sm text-zinc-400">No TLDs configured</p>
+ </div>
+ {% endif %}
+ </div>
+
+ <div class="glass rounded-xl glow-border">
+ <div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Domains</h2>
+ <div class="flex items-center gap-3">
+ <span class="text-xs text-zinc-600">{{ domains|length }} total</span>
+ {% url "domains.new" as new_domain_path %}
+ <a href="{{ new_domain_path }}" hx-get="{{ new_domain_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New Domain</a>
+ </div>
+ </div>
+ {% if domains %}
+ <div class="divide-y divide-white/[0.04]">
+ {% for domain in domains %}
+ <div class="flex items-center justify-between px-5 py-3">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-emerald-500/10">
+ <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z" />
+ </svg>
+ </div>
+ <p class="text-sm text-zinc-200">{{ domain.Name }}.{{ domain.TLD.Name }}</p>
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="flex flex-col items-center justify-center py-16 text-center">
+ <div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
+ <svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
+ </svg>
+ </div>
+ <p class="text-sm text-zinc-400">No domains registered</p>
+ <p class="mt-1 text-xs text-zinc-600">Register a domain to get started</p>
+ </div>
+ {% endif %}
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/domains/htmx/newdomain.htmx.django b/templates/domains/htmx/newdomain.htmx.django
new file mode 100644
index 0000000..5f188c1
--- /dev/null
+++ b/templates/domains/htmx/newdomain.htmx.django
@@ -0,0 +1,42 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up flex items-start justify-center pt-12">
+ <div class="glass rounded-xl glow-border w-full max-w-lg">
+ <div class="px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Register Domain</h2>
+ </div>
+ <div class="p-5">
+ {% url "domains.create" as create_path %}
+ <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Domain Name</label>
+ <input type="text" name="name" required autocomplete="off" placeholder="myproject" class="input-field">
+ </div>
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">TLD</label>
+ <div data-dropdown>
+ <input type="hidden" name="tld_name" data-dropdown-value>
+ <button type="button" data-dropdown-trigger class="input-field text-left flex items-center justify-between">
+ <span data-dropdown-label class="text-zinc-500">Select a TLD</span>
+ <svg class="w-4 h-4 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div data-dropdown-menu class="dropdown-menu">
+ <input type="text" data-dropdown-search class="dropdown-search" placeholder="Search TLDs...">
+ <div data-dropdown-options class="dropdown-options">
+ {% for tld in tlds %}
+ <div data-dropdown-option data-value="{{ tld.Name }}" class="dropdown-option">.{{ tld.Name }}</div>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Register Domain</button>
+ {% url "domains.index" as domains_path %}
+ <a href="{{ domains_path }}" hx-get="{{ domains_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/domains/htmx/newtld.htmx.django b/templates/domains/htmx/newtld.htmx.django
new file mode 100644
index 0000000..39d09d2
--- /dev/null
+++ b/templates/domains/htmx/newtld.htmx.django
@@ -0,0 +1,25 @@
+<h1 id="page-title" class="text-sm font-medium text-zinc-100" hx-swap-oob="true">{{ PageTitle }}</h1>
+<div class="slide-up flex items-start justify-center pt-12">
+ <div class="glass rounded-xl glow-border w-full max-w-lg">
+ <div class="px-5 py-4 border-b border-white/[0.04]">
+ <h2 class="text-sm font-medium text-zinc-200">Create TLD</h2>
+ </div>
+ <div class="p-5">
+ {% url "domains.tlds.create" as create_path %}
+ <form method="POST" action="{{ create_path }}" class="space-y-4">
+ <div>
+ <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">TLD Name</label>
+ <div class="flex items-center gap-2">
+ <span class="text-sm text-zinc-500">.</span>
+ <input type="text" name="name" required autocomplete="off" placeholder="example" class="input-field">
+ </div>
+ </div>
+ <div class="flex items-center gap-3 pt-2">
+ <button type="submit" class="btn-primary">Create TLD</button>
+ {% url "domains.index" as domains_path %}
+ <a href="{{ domains_path }}" hx-get="{{ domains_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
+ </div>
+ </form>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/domains/newdomain.django b/templates/domains/newdomain.django
new file mode 100644
index 0000000..1a3a413
--- /dev/null
+++ b/templates/domains/newdomain.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "domains/htmx/newdomain.htmx.django" %}
+{% endblock %} \ No newline at end of file
diff --git a/templates/domains/newtld.django b/templates/domains/newtld.django
new file mode 100644
index 0000000..7737192
--- /dev/null
+++ b/templates/domains/newtld.django
@@ -0,0 +1,5 @@
+{% extends "layouts/dashboard.django" %}
+
+{% block dashboard %}
+{% include "domains/htmx/newtld.htmx.django" %}
+{% endblock %} \ No newline at end of file
diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django
index c1e9623..b3d017e 100644
--- a/templates/partials/sidebar.django
+++ b/templates/partials/sidebar.django
@@ -10,6 +10,7 @@
<nav class="flex flex-col gap-1 p-3" id="sidebar-nav" hx-target="#content" hx-swap="innerHTML" hx-push-url="true">
{% url "dashboard.index" as overview_path %}
+ {% url "domains.index" as domains_path %}
{% url "dashboard.mailboxes" as mailboxes_path %}
{% url "dashboard.users" as users_path %}
<a href="{{ overview_path }}" hx-get="{{ overview_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150 {% if Request.Path == overview_path %}active{% endif %}">
@@ -18,6 +19,12 @@
</svg>
Overview
</a>
+ <a href="{{ domains_path }}" hx-get="{{ domains_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150 {% if Request.Path == domains_path %}active{% 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="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
+ </svg>
+ Domains
+ </a>
<a href="{{ mailboxes_path }}" hx-get="{{ mailboxes_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150 {% if Request.Path == mailboxes_path %}active{% 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" />
diff --git a/types/auth.go b/types/auth.go
deleted file mode 100644
index 1792011..0000000
--- a/types/auth.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package types
-
-type LoginRequest struct {
- Username string `form:"username"`
- Password string `form:"password"`
-}
diff --git a/types/errors.go b/types/errors.go
deleted file mode 100644
index 1e5a562..0000000
--- a/types/errors.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package types
-
-import "dove/enums"
-
-type ServiceError struct {
- Kind enums.ErrorKind
- Message string
-}
-
-func (self *ServiceError) Error() string {
- return self.Message
-}
diff --git a/types/mailbox.go b/types/mailbox.go
deleted file mode 100644
index adf1ce5..0000000
--- a/types/mailbox.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package types
-
-import "dove/models"
-
-type Mailbox struct {
- Address string
-}
-
-type CreateMailboxRequest struct {
- Address string `form:"address"`
- UserID uint `form:"user_id"`
-}
-
-type MailboxFormResponse struct {
- Users []models.User `json:"users"`
-}
diff --git a/types/overview.go b/types/overview.go
deleted file mode 100644
index ac983a8..0000000
--- a/types/overview.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package types
-
-type Overview struct {
- MailboxCount int64 `json:"MailboxCount"`
- EmailCount int64 `json:"EmailCount"`
- SMTPAddress string `json:"SMTPAddress"`
-}
diff --git a/types/request.go b/types/request.go
deleted file mode 100644
index 5d6a1a1..0000000
--- a/types/request.go
+++ /dev/null
@@ -1,17 +0,0 @@
-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/types/response.go b/types/response.go
deleted file mode 100644
index e576142..0000000
--- a/types/response.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package types
-
-type MessageResponse struct {
- Message string
-}
-
-type PaginatedResponse struct {
- Items any `json:"items"`
- Total int64 `json:"total"`
- Page int `json:"page"`
- PerPage int `json:"per_page"`
- TotalPages int `json:"total_pages"`
-}
diff --git a/types/user.go b/types/user.go
deleted file mode 100644
index 6686567..0000000
--- a/types/user.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package types
-
-type CreateUserRequest struct {
- Username string `form:"username"`
- DisplayName string `form:"display_name"`
-}
diff --git a/utils/collections/record.go b/utils/collections/record.go
new file mode 100644
index 0000000..a204436
--- /dev/null
+++ b/utils/collections/record.go
@@ -0,0 +1,3 @@
+package collections
+
+type Record[K comparable, V any] map[K]V
diff --git a/utils/collections/set.go b/utils/collections/set.go
new file mode 100644
index 0000000..79e1ae2
--- /dev/null
+++ b/utils/collections/set.go
@@ -0,0 +1,38 @@
+package collections
+
+type Set[T comparable] struct {
+ items []T
+ index map[T]bool
+}
+
+func SetOf[T comparable](items ...T) Set[T] {
+ index := make(map[T]bool, len(items))
+ uniqueItems := make([]T, 0, len(items))
+
+ for _, item := range items {
+ if index[item] {
+ continue
+ }
+ index[item] = true
+ uniqueItems = append(uniqueItems, item)
+ }
+
+ return Set[T]{
+ items: uniqueItems,
+ index: index,
+ }
+}
+
+func (set Set[T]) Contains(item T) bool {
+ return set.index[item]
+}
+
+func (set Set[T]) All() []T {
+ copied := make([]T, len(set.items))
+ copy(copied, set.items)
+ return copied
+}
+
+func (set Set[T]) Len() int {
+ return len(set.items)
+}
diff --git a/utils/collections/types.go b/utils/collections/types.go
deleted file mode 100644
index ac84e76..0000000
--- a/utils/collections/types.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package collections
-
-type Record[T any] map[string]T
diff --git a/utils/meta/builder.go b/utils/meta/builder.go
index 4ae2648..3e4de93 100644
--- a/utils/meta/builder.go
+++ b/utils/meta/builder.go
@@ -1,13 +1,9 @@
package meta
-import (
- "dove/types"
+import "github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2"
-)
-
-func BuildRequest(context *fiber.Ctx) types.Request {
- return types.Request{
+func BuildRequest(context *fiber.Ctx) RequestInfo {
+ return RequestInfo{
Path: context.Path(),
Method: context.Method(),
Query: buildQueryParams(context),
@@ -18,3 +14,39 @@ func BuildRequest(context *fiber.Ctx) types.Request {
URL: context.OriginalURL(),
}
}
+
+func buildQueryParams(context *fiber.Ctx) []Param {
+ params := make([]Param, 0)
+ context.Request().URI().QueryArgs().VisitAll(func(name []byte, paramValue []byte) {
+ params = append(params, Param{
+ Key: string(name),
+ Value: string(paramValue),
+ })
+ })
+
+ return params
+}
+
+func buildRouteParams(context *fiber.Ctx) []Param {
+ params := make([]Param, 0)
+ for name, routeValue := range context.AllParams() {
+ params = append(params, Param{
+ Key: name,
+ Value: routeValue,
+ })
+ }
+
+ return params
+}
+
+func buildHeaders(context *fiber.Ctx) []Param {
+ params := make([]Param, 0)
+ context.Request().Header.VisitAll(func(name []byte, headerValue []byte) {
+ params = append(params, Param{
+ Key: string(name),
+ Value: string(headerValue),
+ })
+ })
+
+ return params
+}
diff --git a/utils/meta/constants.go b/utils/meta/constants.go
deleted file mode 100644
index 13a2fde..0000000
--- a/utils/meta/constants.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package meta
-
-const (
- DEFAULT_PAGE = 1
- DEFAULT_PER_PAGE = 20
- LOG_PREFIX = "Meta"
- MAX_PER_PAGE = 50
- REQUEST_KEY = "Request"
-)
diff --git a/utils/meta/defaults.go b/utils/meta/defaults.go
new file mode 100644
index 0000000..9ba6dfe
--- /dev/null
+++ b/utils/meta/defaults.go
@@ -0,0 +1,9 @@
+package meta
+
+const (
+ DefaultPage = 1
+ DefaultPerPage = 20
+ LogPrefix = "Meta"
+ MaxPerPage = 50
+ RequestKey = "Request"
+)
diff --git a/utils/meta/functions.go b/utils/meta/functions.go
deleted file mode 100644
index 0b8f309..0000000
--- a/utils/meta/functions.go
+++ /dev/null
@@ -1,53 +0,0 @@
-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/messages.go b/utils/meta/messages.go
new file mode 100644
index 0000000..2814d7c
--- /dev/null
+++ b/utils/meta/messages.go
@@ -0,0 +1,5 @@
+package meta
+
+const (
+ RequestContextMissing = "Request context missing in fiber locals."
+)
diff --git a/utils/meta/pagination.go b/utils/meta/pagination.go
index 9244294..d0dda08 100644
--- a/utils/meta/pagination.go
+++ b/utils/meta/pagination.go
@@ -1,80 +1,59 @@
package meta
import (
- "dove/types"
"strconv"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
-func Paginate(context *fiber.Ctx) Pagination {
- requestData := Request(context)
-
- page := DEFAULT_PAGE
- perPage := DEFAULT_PER_PAGE
-
- if requestData != nil {
- if pageValue := requestData.Query("page"); pageValue != nil {
- parsed, _ := strconv.Atoi(pageValue.String())
- if parsed >= DEFAULT_PAGE {
- page = parsed
- }
- }
-
- if perPageValue := requestData.Query("per_page"); perPageValue != nil {
- parsed, _ := strconv.Atoi(perPageValue.String())
- if parsed >= DEFAULT_PAGE && parsed <= MAX_PER_PAGE {
- perPage = parsed
- }
- }
- }
+type Pagination struct {
+ Page int
+ PerPage int
+}
- return Pagination{Page: page, PerPage: perPage}
+type PaginatedResponse struct {
+ Items any `json:"items"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PerPage int `json:"per_page"`
+ TotalPages int `json:"total_pages"`
}
-func Sort(context *fiber.Ctx, allowedFields []string, fallbackField string) Sorting {
+func Paginate(context *fiber.Ctx) Pagination {
requestData := Request(context)
- field := fallbackField
- direction := "desc"
+ page := DefaultPage
+ perPage := DefaultPerPage
- if requestData != nil {
- if sortValue := requestData.Query("sort"); sortValue != nil {
- for _, allowedField := range allowedFields {
- if sortValue.String() == allowedField {
- field = sortValue.String()
- break
- }
- }
+ if pageValue := requestData.Query("page"); pageValue != "" {
+ parsed, _ := strconv.Atoi(pageValue)
+ if parsed >= DefaultPage {
+ page = parsed
}
+ }
- if orderValue := requestData.Query("order"); orderValue != nil {
- switch orderValue.String() {
- case "asc", "desc":
- direction = orderValue.String()
- }
+ if perPageValue := requestData.Query("per_page"); perPageValue != "" {
+ parsed, _ := strconv.Atoi(perPageValue)
+ if parsed >= DefaultPage && parsed <= MaxPerPage {
+ perPage = parsed
}
}
- return Sorting{Field: field, Direction: direction}
+ return Pagination{Page: page, PerPage: perPage}
}
func (self Pagination) Apply(query *gorm.DB) *gorm.DB {
return query.Offset((self.Page - 1) * self.PerPage).Limit(self.PerPage)
}
-func (self Sorting) Apply(query *gorm.DB) *gorm.DB {
- return query.Order(self.Field + " " + self.Direction)
-}
-
-func (self Pagination) Response(items any, total int64) types.PaginatedResponse {
+func (self Pagination) Response(items any, total int64) PaginatedResponse {
totalPages := int(total) / self.PerPage
if int(total)%self.PerPage > 0 {
totalPages++
}
- return types.PaginatedResponse{
+ return PaginatedResponse{
Items: items,
Total: total,
Page: self.Page,
diff --git a/utils/meta/request.go b/utils/meta/request.go
index 34a659a..e9a3720 100644
--- a/utils/meta/request.go
+++ b/utils/meta/request.go
@@ -1,63 +1,75 @@
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)
+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 Request(context *fiber.Ctx) *RequestData {
+ data, ok := context.Locals(RequestKey).(RequestInfo)
if !ok {
- logger.Errorf(LOG_PREFIX, messages.MetaRequestContextMissing)
+ logger.Errorf(LogPrefix, RequestContextMissing)
return nil
}
- return &request{
- Request: data,
- context: context,
+ return &RequestData{
+ RequestInfo: data,
+ Context: context,
}
}
-func (self *request) Param(key string) *value {
- if self == nil {
- return nil
- }
-
- if self.context != nil {
- result := self.context.Params(key)
- if result != "" {
- return &value{data: result}
- }
+func (self *RequestData) Param(key string) string {
+ if self == nil || self.Context == nil {
+ return ""
}
- return nil
+ return self.Context.Params(key)
}
-func (self *request) Query(key string) *value {
+func (self *RequestData) Query(key string) string {
if self == nil {
- return nil
- }
-
- result, found := findParam(self.Request.Query, key)
- if found {
- return &value{data: result}
+ return ""
}
- return nil
+ return findParam(self.RequestInfo.Query, key)
}
-func (self *request) Header(key string) *value {
+func (self *RequestData) Header(key string) string {
if self == nil {
- return nil
+ return ""
}
- result, found := findParam(self.Request.Headers, key)
- if found {
- return &value{data: result}
+ return findParam(self.RequestInfo.Headers, key)
+}
+
+func findParam(params []Param, key string) string {
+ for _, param := range params {
+ if param.Key == key {
+ return param.Value
+ }
}
- return nil
+ return ""
}
diff --git a/utils/meta/sorting.go b/utils/meta/sorting.go
new file mode 100644
index 0000000..93788f0
--- /dev/null
+++ b/utils/meta/sorting.go
@@ -0,0 +1,39 @@
+package meta
+
+import (
+ "slices"
+
+ "github.com/gofiber/fiber/v2"
+ "gorm.io/gorm"
+)
+
+type Sorting struct {
+ Field string
+ Direction string
+}
+
+func Sort(context *fiber.Ctx, allowedFields []string, fallbackField string) Sorting {
+ requestData := Request(context)
+
+ field := fallbackField
+ direction := "desc"
+
+ if sortValue := requestData.Query("sort"); sortValue != "" {
+ if slices.Contains(allowedFields, sortValue) {
+ field = sortValue
+ }
+ }
+
+ if orderValue := requestData.Query("order"); orderValue != "" {
+ switch orderValue {
+ case "asc", "desc":
+ direction = orderValue
+ }
+ }
+
+ return Sorting{Field: field, Direction: direction}
+}
+
+func (self Sorting) Apply(query *gorm.DB) *gorm.DB {
+ return query.Order(self.Field + " " + self.Direction)
+}
diff --git a/utils/meta/types.go b/utils/meta/types.go
deleted file mode 100644
index c7b8e4d..0000000
--- a/utils/meta/types.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package meta
-
-import (
- "dove/types"
-
- "github.com/gofiber/fiber/v2"
-)
-
-type request struct {
- types.Request
- context *fiber.Ctx
-}
-
-type value struct {
- data string
-}
-
-type Pagination struct {
- Page int
- PerPage int
-}
-
-type Sorting struct {
- Field string
- Direction string
-}
diff --git a/utils/meta/value.go b/utils/meta/value.go
deleted file mode 100644
index 889ba0e..0000000
--- a/utils/meta/value.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package meta
-
-import (
- "dove/messages"
- "dove/utils/logger"
-)
-
-func (self *value) String() string {
- if self == nil {
- return ""
- }
-
- return self.data
-}
-
-func (self *value) Exists() bool {
- return self != nil
-}
-
-func (self *value) Or(fallback string) string {
- if self == nil {
- return fallback
- }
-
- return self.data
-}
-
-func (self *value) Required() string {
- if self == nil {
- logger.Errorf(LOG_PREFIX, messages.MetaRequiredValueMissing)
- return ""
- }
-
- return self.data
-}
diff --git a/utils/shortcuts/error.go b/utils/shortcuts/error.go
index 71fa4bd..c22e6b2 100644
--- a/utils/shortcuts/error.go
+++ b/utils/shortcuts/error.go
@@ -1,29 +1,44 @@
package shortcuts
-import (
- "dove/enums"
- "dove/types"
+import "github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2"
+type ErrorKind string
+
+const (
+ BadRequest ErrorKind = "bad_request"
+ Forbidden ErrorKind = "forbidden"
+ Internal ErrorKind = "internal"
+ NotFound ErrorKind = "not_found"
+ Unauthorized ErrorKind = "unauthorized"
+ Unprocessable ErrorKind = "unprocessable"
)
-var statusMap = map[enums.ErrorKind]int{
- enums.BadRequest: fiber.StatusBadRequest,
- enums.Forbidden: fiber.StatusForbidden,
- enums.Internal: fiber.StatusInternalServerError,
- enums.NotFound: fiber.StatusNotFound,
- enums.Unauthorized: fiber.StatusUnauthorized,
- enums.Unprocessable: fiber.StatusUnprocessableEntity,
+type Error struct {
+ Kind ErrorKind
+ Message string
+}
+
+func (self *Error) Error() string {
+ return self.Message
+}
+
+var statusMap = map[ErrorKind]int{
+ BadRequest: fiber.StatusBadRequest,
+ Forbidden: fiber.StatusForbidden,
+ Internal: fiber.StatusInternalServerError,
+ NotFound: fiber.StatusNotFound,
+ Unauthorized: fiber.StatusUnauthorized,
+ Unprocessable: fiber.StatusUnprocessableEntity,
}
-func ServiceError(kind enums.ErrorKind, message string) *types.ServiceError {
- return &types.ServiceError{
+func ServiceError(kind ErrorKind, message string) *Error {
+ return &Error{
Kind: kind,
Message: message,
}
}
-func HandleError(context *fiber.Ctx, serviceError *types.ServiceError) error {
+func HandleError(context *fiber.Ctx, serviceError *Error) error {
statusCode, exists := statusMap[serviceError.Kind]
if !exists {
statusCode = fiber.StatusInternalServerError
@@ -34,13 +49,13 @@ func HandleError(context *fiber.Ctx, serviceError *types.ServiceError) error {
}, statusCode)
}
-func BadRequest(context *fiber.Ctx, err error) error {
+func BadRequestError(context *fiber.Ctx, err error) error {
return RenderWithStatus(context, "error", fiber.Map{
"ErrorMessage": err.Error(),
}, fiber.StatusBadRequest)
}
-func Forbidden(context *fiber.Ctx, err error) error {
+func ForbiddenError(context *fiber.Ctx, err error) error {
return RenderWithStatus(context, "error", fiber.Map{
"ErrorMessage": err.Error(),
}, fiber.StatusForbidden)
@@ -52,13 +67,13 @@ func InternalServerError(context *fiber.Ctx, err error) error {
}, fiber.StatusInternalServerError)
}
-func NotFound(context *fiber.Ctx, err error) error {
+func NotFoundError(context *fiber.Ctx, err error) error {
return RenderWithStatus(context, "error", fiber.Map{
"ErrorMessage": err.Error(),
}, fiber.StatusNotFound)
}
-func Unauthorized(context *fiber.Ctx, err error) error {
+func UnauthorizedError(context *fiber.Ctx, err error) error {
return RenderWithStatus(context, "error", fiber.Map{
"ErrorMessage": err.Error(),
}, fiber.StatusUnauthorized)
diff --git a/utils/shortcuts/functions.go b/utils/shortcuts/functions.go
deleted file mode 100644
index 3e76a9a..0000000
--- a/utils/shortcuts/functions.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package shortcuts
-
-import (
- "fmt"
- "maps"
- "path"
- "reflect"
- "strings"
-
- "dove/messages"
- "dove/utils/errors"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func resolveTemplate(context *fiber.Ctx, templateName string) string {
- switch {
- case context.Get("HX-Request") == "true" && context.Get("HX-Boosted") != "true":
- directory := path.Dir(templateName)
- filename := path.Base(templateName)
- return fmt.Sprintf("%s/htmx/%s.htmx", directory, filename)
- default:
- return templateName
- }
-}
-
-func mergeContextValues(context *fiber.Ctx, targetMap fiber.Map) {
- context.Context().VisitUserValues(func(key []byte, value any) {
- targetMap[string(key)] = value
- })
-}
-
-func mergeBindData(targetMap fiber.Map, data any) error {
- normalizedData, normalizeError := normalizeToMap(data)
- if normalizeError != nil {
- return normalizeError
- }
-
- maps.Copy(targetMap, normalizedData)
- return nil
-}
-
-func normalizeToMap(data any) (fiber.Map, error) {
- switch typedData := data.(type) {
- case fiber.Map:
- return typedData, nil
- case map[string]any:
- return fiber.Map(typedData), nil
- default:
- return convertStructToMap(data)
- }
-}
-
-func convertStructToMap(data any) (fiber.Map, error) {
- structValue := reflect.ValueOf(data)
-
- switch structValue.Kind() {
- case reflect.Pointer:
- structValue = structValue.Elem()
- }
-
- switch structValue.Kind() {
- case reflect.Struct:
- return extractStructFields(structValue), nil
- default:
- return nil, errors.Error(messages.ShortcutUnsupportedBindType)
- }
-}
-
-func extractStructFields(structValue reflect.Value) fiber.Map {
- structType := structValue.Type()
- fieldMap := make(fiber.Map, structValue.NumField())
-
- for fieldIndex := range structType.NumField() {
- fieldDescriptor := structType.Field(fieldIndex)
-
- if !fieldDescriptor.IsExported() {
- continue
- }
-
- fieldKey := resolveFieldKey(fieldDescriptor)
- fieldMap[fieldKey] = structValue.Field(fieldIndex).Interface()
- }
-
- return fieldMap
-}
-
-func resolveFieldKey(fieldDescriptor reflect.StructField) string {
- jsonTag := fieldDescriptor.Tag.Get("json")
-
- switch {
- case jsonTag == "" || jsonTag == "-":
- return fieldDescriptor.Name
- default:
- return extractTagName(jsonTag, fieldDescriptor.Name)
- }
-}
-
-func extractTagName(jsonTag string, fallbackName string) string {
- separatorIndex := strings.IndexByte(jsonTag, ',')
-
- switch {
- case separatorIndex < 0:
- return jsonTag
- case separatorIndex > 0:
- return jsonTag[:separatorIndex]
- default:
- return fallbackName
- }
-} \ No newline at end of file
diff --git a/utils/shortcuts/messages.go b/utils/shortcuts/messages.go
new file mode 100644
index 0000000..92df139
--- /dev/null
+++ b/utils/shortcuts/messages.go
@@ -0,0 +1,5 @@
+package shortcuts
+
+const (
+ UnsupportedBindType = "Bind data must be a struct, *struct, fiber.Map, or map[string]any."
+)
diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go
index e91ccfa..89779cf 100644
--- a/utils/shortcuts/render.go
+++ b/utils/shortcuts/render.go
@@ -1,6 +1,16 @@
package shortcuts
-import "github.com/gofiber/fiber/v2"
+import (
+ "fmt"
+ "maps"
+ "path"
+ "reflect"
+ "strings"
+
+ "dove/utils/errors"
+
+ "github.com/gofiber/fiber/v2"
+)
func Render(context *fiber.Ctx, templateName string, data any) error {
templateData := make(fiber.Map)
@@ -20,3 +30,99 @@ func RenderWithStatus(context *fiber.Ctx, templateName string, data any, statusC
context.Status(statusCode)
return Render(context, templateName, data)
}
+
+func resolveTemplate(context *fiber.Ctx, templateName string) string {
+ switch {
+ case context.Get("HX-Request") == "true" && context.Get("HX-Boosted") != "true":
+ directory := path.Dir(templateName)
+ filename := path.Base(templateName)
+ return fmt.Sprintf("%s/htmx/%s.htmx", directory, filename)
+ default:
+ return templateName
+ }
+}
+
+func mergeContextValues(context *fiber.Ctx, targetMap fiber.Map) {
+ context.Context().VisitUserValues(func(key []byte, value any) {
+ targetMap[string(key)] = value
+ })
+}
+
+func mergeBindData(targetMap fiber.Map, data any) error {
+ normalizedData, normalizeError := normalizeToMap(data)
+ if normalizeError != nil {
+ return normalizeError
+ }
+
+ maps.Copy(targetMap, normalizedData)
+ return nil
+}
+
+func normalizeToMap(data any) (fiber.Map, error) {
+ switch typedData := data.(type) {
+ case fiber.Map:
+ return typedData, nil
+ case map[string]any:
+ return fiber.Map(typedData), nil
+ default:
+ return convertStructToMap(data)
+ }
+}
+
+func convertStructToMap(data any) (fiber.Map, error) {
+ structValue := reflect.ValueOf(data)
+
+ switch structValue.Kind() {
+ case reflect.Pointer:
+ structValue = structValue.Elem()
+ }
+
+ switch structValue.Kind() {
+ case reflect.Struct:
+ return extractStructFields(structValue), nil
+ default:
+ return nil, errors.Error(UnsupportedBindType)
+ }
+}
+
+func extractStructFields(structValue reflect.Value) fiber.Map {
+ structType := structValue.Type()
+ fieldMap := make(fiber.Map, structValue.NumField())
+
+ for fieldIndex := range structType.NumField() {
+ fieldDescriptor := structType.Field(fieldIndex)
+
+ if !fieldDescriptor.IsExported() {
+ continue
+ }
+
+ fieldKey := resolveFieldKey(fieldDescriptor)
+ fieldMap[fieldKey] = structValue.Field(fieldIndex).Interface()
+ }
+
+ return fieldMap
+}
+
+func resolveFieldKey(fieldDescriptor reflect.StructField) string {
+ jsonTag := fieldDescriptor.Tag.Get("json")
+
+ switch {
+ case jsonTag == "" || jsonTag == "-":
+ return fieldDescriptor.Name
+ default:
+ return extractTagName(jsonTag, fieldDescriptor.Name)
+ }
+}
+
+func extractTagName(jsonTag string, fallbackName string) string {
+ separatorIndex := strings.IndexByte(jsonTag, ',')
+
+ switch {
+ case separatorIndex < 0:
+ return jsonTag
+ case separatorIndex > 0:
+ return jsonTag[:separatorIndex]
+ default:
+ return fallbackName
+ }
+}
diff --git a/utils/toml/load.go b/utils/toml/load.go
index 7b3c31a..71fd0c0 100644
--- a/utils/toml/load.go
+++ b/utils/toml/load.go
@@ -8,7 +8,7 @@ import (
"dove/utils/errors"
)
-var loadedData collections.Record[any]
+var loadedData collections.Record[string, any]
func LoadFile(filePath string) error {
fileContent, readError := os.ReadFile(filePath)
@@ -16,6 +16,6 @@ func LoadFile(filePath string) error {
return errors.Error(messages.ConfigFileReadFailed, filePath, readError.Error())
}
- loadedData = make(collections.Record[any])
+ loadedData = make(collections.Record[string, any])
return unmarshalContent(fileContent, &loadedData)
}
diff --git a/utils/urls/attach.go b/utils/urls/attach.go
index df8a11a..baf9929 100644
--- a/utils/urls/attach.go
+++ b/utils/urls/attach.go
@@ -1,10 +1,6 @@
package urls
-import (
- "dove/enums"
-
- "github.com/gofiber/fiber/v2"
-)
+import "github.com/gofiber/fiber/v2"
func Attach(application *fiber.App) {
registry.Mutex.Lock()
@@ -15,21 +11,21 @@ func Attach(application *fiber.App) {
}
}
-func bindRoute(application *fiber.App, route registeredRoute) {
+func bindRoute(application *fiber.App, route RegisteredRoute) {
switch route.Method {
- case enums.Delete:
+ case Delete:
application.Delete(route.FullPath, route.Handler)
- case enums.Get:
+ case Get:
application.Get(route.FullPath, route.Handler)
- case enums.Head:
+ case Head:
application.Head(route.FullPath, route.Handler)
- case enums.Options:
+ case Options:
application.Options(route.FullPath, route.Handler)
- case enums.Patch:
+ case Patch:
application.Patch(route.FullPath, route.Handler)
- case enums.Post:
+ case Post:
application.Post(route.FullPath, route.Handler)
- case enums.Put:
+ case Put:
application.Put(route.FullPath, route.Handler)
}
}
diff --git a/utils/urls/functions.go b/utils/urls/functions.go
deleted file mode 100644
index 6561025..0000000
--- a/utils/urls/functions.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package urls
-
-import "strings"
-
-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 ensureLeadingSlash(path string) string {
- switch strings.HasPrefix(path, "/") {
- case true:
- return path
- default:
- return "/" + path
- }
-}
diff --git a/utils/urls/path.go b/utils/urls/path.go
index b865676..a3b3ec9 100644
--- a/utils/urls/path.go
+++ b/utils/urls/path.go
@@ -1,12 +1,24 @@
package urls
import (
- "dove/enums"
+ "strings"
"github.com/gofiber/fiber/v2"
)
-func Path(method enums.HTTPMethod, path string, handler fiber.Handler, name string) {
+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()
@@ -14,7 +26,7 @@ func Path(method enums.HTTPMethod, path string, handler fiber.Handler, name stri
fullName := resolveFullName(namespace, name)
fullPath := resolveFullPath(namespace, path)
- registry.Routes[fullName] = registeredRoute{
+ registry.Routes[fullName] = RegisteredRoute{
Method: method,
Path: path,
Handler: handler,
@@ -35,3 +47,30 @@ func GetFullPath(routeName string) (string, bool) {
return route.FullPath, 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 ensureLeadingSlash(path string) string {
+ switch strings.HasPrefix(path, "/") {
+ case true:
+ return path
+ default:
+ return "/" + path
+ }
+}
diff --git a/utils/urls/registry.go b/utils/urls/registry.go
index a713315..e95d30c 100644
--- a/utils/urls/registry.go
+++ b/utils/urls/registry.go
@@ -1,9 +1,30 @@
package urls
-import "dove/utils/collections"
+import (
+ "sync"
-var registry = &routeRegistry{
- Routes: make(collections.Record[registeredRoute]),
+ "dove/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.Record[string, RegisteredRoute]
+}
+
+var registry = &RouteRegistry{
+ Routes: make(collections.Record[string, RegisteredRoute]),
}
func SetNamespace(namespace string) {
diff --git a/utils/urls/types.go b/utils/urls/types.go
deleted file mode 100644
index 499635b..0000000
--- a/utils/urls/types.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package urls
-
-import (
- "sync"
-
- "dove/enums"
- "dove/utils/collections"
-
- "github.com/gofiber/fiber/v2"
-)
-
-type registeredRoute struct {
- Method enums.HTTPMethod
- Path string
- Handler fiber.Handler
- Namespace string
- Name string
- FullPath string
-}
-
-type routeRegistry struct {
- Mutex sync.Mutex
- CurrentNamespace string
- Routes collections.Record[registeredRoute]
-}