diff options
| author | Bobby <[email protected]> | 2026-03-07 19:11:59 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-07 19:11:59 +0530 |
| commit | 547384c41181c034a5eaf340c5e569d36eb013be (patch) | |
| tree | 345341ac0df478fe51d11eeb6c45f2265afd7619 | |
| parent | 96c136f046d78c51210927e61483a36a220fedcb (diff) | |
| download | dove-547384c41181c034a5eaf340c5e569d36eb013be.tar.xz dove-547384c41181c034a5eaf340c5e569d36eb013be.zip | |
feat: implement mailbox and user creation features with validation and dropdowns
| -rw-r--r-- | controllers/mailbox.go | 24 | ||||
| -rw-r--r-- | controllers/user.go | 24 | ||||
| -rw-r--r-- | messages/mailbox.go | 9 | ||||
| -rw-r--r-- | messages/user.go | 8 | ||||
| -rw-r--r-- | pages/mailboxes.go | 5 | ||||
| -rw-r--r-- | pages/users.go | 5 | ||||
| -rw-r--r-- | repositories/user.go | 16 | ||||
| -rw-r--r-- | router/dashboard.go | 5 | ||||
| -rw-r--r-- | services/mailbox.go | 42 | ||||
| -rw-r--r-- | services/user.go | 38 | ||||
| -rw-r--r-- | static/css/tailwind.css | 110 | ||||
| -rw-r--r-- | static/js/dropdown.js | 62 | ||||
| -rw-r--r-- | templates/dashboard/htmx/mailboxes.htmx.django | 6 | ||||
| -rw-r--r-- | templates/dashboard/htmx/newmailbox.htmx.django | 52 | ||||
| -rw-r--r-- | templates/dashboard/htmx/newuser.htmx.django | 26 | ||||
| -rw-r--r-- | templates/dashboard/htmx/users.htmx.django | 6 | ||||
| -rw-r--r-- | templates/dashboard/newmailbox.django | 5 | ||||
| -rw-r--r-- | templates/dashboard/newuser.django | 5 | ||||
| -rw-r--r-- | templates/layouts/base.django | 1 | ||||
| -rw-r--r-- | types/mailbox.go | 11 | ||||
| -rw-r--r-- | types/user.go | 6 |
21 files changed, 462 insertions, 4 deletions
diff --git a/controllers/mailbox.go b/controllers/mailbox.go new file mode 100644 index 0000000..0878bd3 --- /dev/null +++ b/controllers/mailbox.go @@ -0,0 +1,24 @@ +package controllers + +import ( + "dove/services" + "dove/types" + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func CreateMailbox(context *fiber.Ctx) error { + body, parseError := meta.Body[types.CreateMailboxRequest](context) + if parseError != nil { + return shortcuts.BadRequest(context, parseError) + } + + serviceError := services.CreateMailbox(body) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + return shortcuts.Redirect(context, "dashboard.mailboxes") +}
\ No newline at end of file diff --git a/controllers/user.go b/controllers/user.go new file mode 100644 index 0000000..ea2008b --- /dev/null +++ b/controllers/user.go @@ -0,0 +1,24 @@ +package controllers + +import ( + "dove/services" + "dove/types" + "dove/utils/meta" + "dove/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func CreateUser(context *fiber.Ctx) error { + body, parseError := meta.Body[types.CreateUserRequest](context) + if parseError != nil { + return shortcuts.BadRequest(context, parseError) + } + + serviceError := services.CreateUser(body) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + return shortcuts.Redirect(context, "dashboard.users") +}
\ No newline at end of file diff --git a/messages/mailbox.go b/messages/mailbox.go index e193bf7..9ead4f0 100644 --- a/messages/mailbox.go +++ b/messages/mailbox.go @@ -1,6 +1,11 @@ package messages const ( - MailboxAutoCreated = "Auto-created mailbox for %s." - MailboxNotRegistered = "No registered mailbox or alias for address: %s" + 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/user.go b/messages/user.go new file mode 100644 index 0000000..5c3fe93 --- /dev/null +++ b/messages/user.go @@ -0,0 +1,8 @@ +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/pages/mailboxes.go b/pages/mailboxes.go index 317d9c9..611c2cb 100644 --- a/pages/mailboxes.go +++ b/pages/mailboxes.go @@ -8,6 +8,11 @@ import ( "github.com/gofiber/fiber/v2" ) +func NewMailbox(context *fiber.Ctx) error { + meta.SetPageTitle(context, "New Mailbox") + return shortcuts.Render(context, "dashboard/newmailbox", services.MailboxFormData()) +} + func Mailboxes(context *fiber.Ctx) error { meta.SetPageTitle(context, "Mailboxes") diff --git a/pages/users.go b/pages/users.go index 723b544..1fc6fcc 100644 --- a/pages/users.go +++ b/pages/users.go @@ -8,6 +8,11 @@ import ( "github.com/gofiber/fiber/v2" ) +func NewUser(context *fiber.Ctx) error { + meta.SetPageTitle(context, "New User") + return shortcuts.Render(context, "dashboard/newuser", nil) +} + func Users(context *fiber.Ctx) error { meta.SetPageTitle(context, "Users") diff --git a/repositories/user.go b/repositories/user.go index 0147f55..7a3e5e6 100644 --- a/repositories/user.go +++ b/repositories/user.go @@ -8,6 +8,16 @@ import ( "gorm.io/gorm" ) +func FindUserByID(userID uint) *models.User { + var user models.User + result := database.DB.First(&user, userID) + if result.Error == gorm.ErrRecordNotFound { + return nil + } + + return &user +} + func FindUserByUsername(username string) *models.User { var user models.User result := database.DB.Where("username = ?", username).First(&user) @@ -39,6 +49,12 @@ func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) return users, total } +func AllUsers() []models.User { + var users []models.User + database.DB.Order("username ASC").Find(&users) + return users +} + func CountUsers() int64 { var count int64 database.DB.Model(&models.User{}).Count(&count) diff --git a/router/dashboard.go b/router/dashboard.go index 2a593dc..ff7aa52 100644 --- a/router/dashboard.go +++ b/router/dashboard.go @@ -1,6 +1,7 @@ package router import ( + "dove/controllers" "dove/enums" "dove/pages" "dove/utils/auth" @@ -12,6 +13,10 @@ func init() { 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 diff --git a/services/mailbox.go b/services/mailbox.go index b84e19b..d1e954d 100644 --- a/services/mailbox.go +++ b/services/mailbox.go @@ -1,10 +1,15 @@ package services import ( + "strings" + + "dove/enums" + "dove/messages" "dove/models" "dove/repositories" "dove/types" "dove/utils/meta" + "dove/utils/shortcuts" ) func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse { @@ -12,6 +17,43 @@ func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search stri return pagination.Response(mailboxes, total) } +func MailboxFormData() types.MailboxFormResponse { + return types.MailboxFormResponse{ + Users: repositories.AllUsers(), + } +} + +func CreateMailbox(request types.CreateMailboxRequest) *types.ServiceError { + address := strings.TrimSpace(request.Address) + + if address == "" { + return shortcuts.ServiceError(enums.BadRequest, messages.MailboxAddressRequired) + } + + if request.UserID == 0 { + return shortcuts.ServiceError(enums.BadRequest, messages.MailboxUserRequired) + } + + if repositories.FindUserByID(request.UserID) == nil { + return shortcuts.ServiceError(enums.Unprocessable, messages.MailboxUserNotFound) + } + + if repositories.FindMailboxByAddress(address) != nil { + return shortcuts.ServiceError(enums.Unprocessable, messages.MailboxAlreadyExists) + } + + mailbox := &models.Mailbox{ + Address: address, + UserID: request.UserID, + } + + if createError := repositories.CreateMailbox(mailbox); createError != nil { + return shortcuts.ServiceError(enums.Internal, messages.MailboxCreationFailed) + } + + return nil +} + func ResolveMailboxes(recipientAddresses []string) []models.Mailbox { var resolvedMailboxes []models.Mailbox diff --git a/services/user.go b/services/user.go index 392aaa1..b2d43cf 100644 --- a/services/user.go +++ b/services/user.go @@ -1,12 +1,50 @@ package services import ( + "strings" + + "dove/enums" + "dove/messages" + "dove/models" "dove/repositories" "dove/types" "dove/utils/meta" + "dove/utils/shortcuts" ) func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse { users, total := repositories.ListUsers(pagination, sorting, search) return pagination.Response(users, total) } + +func CreateUser(request types.CreateUserRequest) *types.ServiceError { + username := strings.TrimSpace(request.Username) + displayName := strings.TrimSpace(request.DisplayName) + + if username == "" { + return shortcuts.ServiceError(enums.BadRequest, messages.UserUsernameRequired) + } + + if displayName == "" { + return shortcuts.ServiceError(enums.BadRequest, messages.UserDisplayNameRequired) + } + + if repositories.FindUserByUsername(username) != nil { + return shortcuts.ServiceError(enums.Unprocessable, messages.UserAlreadyExists) + } + + user := &models.User{ + Username: username, + DisplayName: displayName, + } + + if createError := repositories.CreateUser(user); createError != nil { + return shortcuts.ServiceError(enums.Internal, messages.UserCreationFailed) + } + + return nil +} + +func AllUsers() []models.User { + return repositories.AllUsers() +} diff --git a/static/css/tailwind.css b/static/css/tailwind.css index 477fdcd..a325d92 100644 --- a/static/css/tailwind.css +++ b/static/css/tailwind.css @@ -83,6 +83,31 @@ } } +@utility btn-small { + padding: 0.375rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.75rem; + 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 20px rgba(99, 102, 241, 0.3), + 0 2px 10px rgba(99, 102, 241, 0.2); + transform: translateY(-1px); + } + &:active { + transform: translateY(0); + } +} + @utility fade-in { animation: fadeIn 0.4s ease-out; } @@ -198,6 +223,91 @@ background: rgba(255, 255, 255, 0.06); } +.dropdown { + position: relative; +} + +.dropdown-menu { + display: none; + position: absolute; + top: calc(100% + 0.375rem); + left: 0; + right: 0; + z-index: 50; + border-radius: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.08); + background: var(--color-surface-800); + box-shadow: + 0 10px 40px rgba(0, 0, 0, 0.4), + 0 0 20px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +.dropdown.open .dropdown-menu { + display: block; +} + +.dropdown.open [data-dropdown-chevron] { + transform: rotate(180deg); +} + +.dropdown-search { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 0.5rem; + background: var(--color-surface-900); + color: #e4e4e7; + font-size: 0.8125rem; + outline: none; + transition: border-color 0.2s ease; +} + +.dropdown-search::placeholder { + color: #52525b; +} + +.dropdown-search:focus { + border-color: var(--color-accent-500); +} + +.dropdown-options { + max-height: 12rem; + overflow-y: auto; + padding: 0.25rem; +} + +.dropdown-options::-webkit-scrollbar { + width: 4px; +} + +.dropdown-options::-webkit-scrollbar-track { + background: transparent; +} + +.dropdown-options::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} + +.dropdown-option { + display: block; + width: 100%; + text-align: left; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + cursor: pointer; + transition: background 0.15s ease; +} + +.dropdown-option:hover { + background: rgba(255, 255, 255, 0.04); +} + +.dropdown-option.selected { + background: rgba(99, 102, 241, 0.1); +} + ::selection { background-color: var(--color-accent-500); color: white; diff --git a/static/js/dropdown.js b/static/js/dropdown.js new file mode 100644 index 0000000..0bfcc88 --- /dev/null +++ b/static/js/dropdown.js @@ -0,0 +1,62 @@ +function initDropdowns() { + document.querySelectorAll("[data-dropdown]").forEach(function (dropdown) { + if (dropdown.dataset.initialized) return; + dropdown.dataset.initialized = "true"; + + var trigger = dropdown.querySelector("[data-dropdown-trigger]"); + var menu = dropdown.querySelector("[data-dropdown-menu]"); + var search = dropdown.querySelector("[data-dropdown-search]"); + var options = dropdown.querySelectorAll("[data-dropdown-option]"); + var empty = dropdown.querySelector("[data-dropdown-empty]"); + var hiddenInput = dropdown.querySelector("[data-dropdown-value]"); + var label = dropdown.querySelector("[data-dropdown-label]"); + + trigger.addEventListener("click", function () { + dropdown.classList.toggle("open"); + if (dropdown.classList.contains("open")) { + search.value = ""; + filterOptions(""); + search.focus(); + } + }); + + search.addEventListener("input", function () { + filterOptions(search.value.toLowerCase()); + }); + + options.forEach(function (option) { + option.addEventListener("click", function () { + hiddenInput.value = option.dataset.value; + label.textContent = option.dataset.label; + label.classList.remove("text-zinc-500"); + label.classList.add("text-zinc-200"); + + options.forEach(function (other) { + other.classList.remove("selected"); + }); + option.classList.add("selected"); + + dropdown.classList.remove("open"); + }); + }); + + function filterOptions(query) { + var visibleCount = 0; + options.forEach(function (option) { + var matches = option.dataset.label.toLowerCase().indexOf(query) !== -1; + option.style.display = matches ? "" : "none"; + if (matches) visibleCount++; + }); + empty.classList.toggle("hidden", visibleCount > 0); + } + + document.addEventListener("click", function (event) { + if (!dropdown.contains(event.target)) { + dropdown.classList.remove("open"); + } + }); + }); +} + +document.addEventListener("DOMContentLoaded", initDropdowns); +document.body.addEventListener("htmx:afterSwap", initDropdowns); diff --git a/templates/dashboard/htmx/mailboxes.htmx.django b/templates/dashboard/htmx/mailboxes.htmx.django index 9ccedb1..1816951 100644 --- a/templates/dashboard/htmx/mailboxes.htmx.django +++ b/templates/dashboard/htmx/mailboxes.htmx.django @@ -3,7 +3,11 @@ <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">All Mailboxes</h2> - <span class="text-xs text-zinc-600">{{ total }} total</span> + <div class="flex items-center gap-3"> + <span class="text-xs text-zinc-600">{{ total }} total</span> + {% url "dashboard.mailboxes.new" as new_mailbox_path %} + <a href="{{ new_mailbox_path }}" hx-get="{{ new_mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New Mailbox</a> + </div> </div> {% if items %} <div class="divide-y divide-white/[0.04]"> diff --git a/templates/dashboard/htmx/newmailbox.htmx.django b/templates/dashboard/htmx/newmailbox.htmx.django new file mode 100644 index 0000000..4f201cd --- /dev/null +++ b/templates/dashboard/htmx/newmailbox.htmx.django @@ -0,0 +1,52 @@ +<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 Mailbox</h2> + </div> + <div class="p-5"> + {% url "dashboard.mailboxes.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">Address</label> + <input type="email" name="address" required autocomplete="off" class="input-field"> + </div> + <div> + <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Owner</label> + <div class="dropdown" data-dropdown> + <input type="hidden" name="user_id" data-dropdown-value> + <button type="button" class="input-field text-left flex items-center justify-between" data-dropdown-trigger> + <span class="truncate" data-dropdown-label>Select a user</span> + <svg class="w-4 h-4 text-zinc-500 shrink-0 ml-2 transition-transform duration-150" data-dropdown-chevron 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 class="dropdown-menu" data-dropdown-menu> + <div class="p-2 border-b border-white/[0.04]"> + <input type="text" placeholder="Search users..." class="dropdown-search" data-dropdown-search> + </div> + <div class="dropdown-options" data-dropdown-options> + {% for user in users %} + <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ user.ID }}" data-label="{{ user.DisplayName }} ({{ user.Username }})"> + <div> + <p class="text-sm text-zinc-200">{{ user.DisplayName }}</p> + <p class="text-xs text-zinc-500">{{ user.Username }}</p> + </div> + </button> + {% endfor %} + </div> + <div class="dropdown-empty hidden" data-dropdown-empty> + <p class="text-xs text-zinc-500 text-center py-3">No users found</p> + </div> + </div> + </div> + </div> + <div class="flex items-center gap-3 pt-2"> + <button type="submit" class="btn-primary">Create Mailbox</button> + {% url "dashboard.mailboxes" as mailboxes_path %} + <a href="{{ mailboxes_path }}" hx-get="{{ mailboxes_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> diff --git a/templates/dashboard/htmx/newuser.htmx.django b/templates/dashboard/htmx/newuser.htmx.django new file mode 100644 index 0000000..826dc72 --- /dev/null +++ b/templates/dashboard/htmx/newuser.htmx.django @@ -0,0 +1,26 @@ +<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 User</h2> + </div> + <div class="p-5"> + {% url "dashboard.users.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">Username</label> + <input type="text" name="username" required autocomplete="off" class="input-field"> + </div> + <div> + <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Display Name</label> + <input type="text" name="display_name" required autocomplete="off" class="input-field"> + </div> + <div class="flex items-center gap-3 pt-2"> + <button type="submit" class="btn-primary">Create User</button> + {% url "dashboard.users" as users_path %} + <a href="{{ users_path }}" hx-get="{{ users_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> diff --git a/templates/dashboard/htmx/users.htmx.django b/templates/dashboard/htmx/users.htmx.django index 6578ba2..edcf72a 100644 --- a/templates/dashboard/htmx/users.htmx.django +++ b/templates/dashboard/htmx/users.htmx.django @@ -3,7 +3,11 @@ <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">All Users</h2> - <span class="text-xs text-zinc-600">{{ total }} total</span> + <div class="flex items-center gap-3"> + <span class="text-xs text-zinc-600">{{ total }} total</span> + {% url "dashboard.users.new" as new_user_path %} + <a href="{{ new_user_path }}" hx-get="{{ new_user_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="btn-small">New User</a> + </div> </div> {% if items %} <div class="divide-y divide-white/[0.04]"> diff --git a/templates/dashboard/newmailbox.django b/templates/dashboard/newmailbox.django new file mode 100644 index 0000000..179ca88 --- /dev/null +++ b/templates/dashboard/newmailbox.django @@ -0,0 +1,5 @@ +{% extends "layouts/dashboard.django" %} + +{% block dashboard %} +{% include "dashboard/htmx/newmailbox.htmx.django" %} +{% endblock %} diff --git a/templates/dashboard/newuser.django b/templates/dashboard/newuser.django new file mode 100644 index 0000000..efb3176 --- /dev/null +++ b/templates/dashboard/newuser.django @@ -0,0 +1,5 @@ +{% extends "layouts/dashboard.django" %} + +{% block dashboard %} +{% include "dashboard/htmx/newuser.htmx.django" %} +{% endblock %} diff --git a/templates/layouts/base.django b/templates/layouts/base.django index 0a884dd..bcfab20 100644 --- a/templates/layouts/base.django +++ b/templates/layouts/base.django @@ -16,6 +16,7 @@ {% block content %}{% endblock %} <script src="/static/js/htmx.min.js"></script> <script src="/static/js/navigation.js"></script> + <script src="/static/js/dropdown.js"></script> {% block scripts %}{% endblock %} </body> </html> diff --git a/types/mailbox.go b/types/mailbox.go index 5d213fd..adf1ce5 100644 --- a/types/mailbox.go +++ b/types/mailbox.go @@ -1,5 +1,16 @@ 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/user.go b/types/user.go new file mode 100644 index 0000000..6686567 --- /dev/null +++ b/types/user.go @@ -0,0 +1,6 @@ +package types + +type CreateUserRequest struct { + Username string `form:"username"` + DisplayName string `form:"display_name"` +} |
