ultisuite-backend/internal/api/mail/handlers_hosted.go
R3D347HR4Y bda75aeb0d
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(config): enhance AI gateway and model management features
- Updated .env.example to include new configuration options for AI gateway and WebUI secret key.
- Modified Nginx configuration to support additional API routes for model management and migration.
- Implemented new API endpoints for discovering organization-level LLM models and managing hosted mail services.
- Enhanced AI gateway logic to support organization-specific model access and permissions.
- Improved error handling and response structures in the AI and mail APIs.
- Added integration tests for new features and updated existing tests for model access control.
2026-06-13 20:38:26 +02:00

203 lines
6.2 KiB
Go

package mail
import (
"errors"
"net/http"
"strings"
"github.com/jackc/pgx/v5"
"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
"github.com/ultisuite/ulti-backend/internal/api/middleware"
"github.com/ultisuite/ulti-backend/internal/api/query"
"github.com/ultisuite/ulti-backend/internal/mail/hosted"
)
func (h *Handler) SetHostedService(svc *hosted.Service) {
if s, ok := h.svc.(*Service); ok {
s.SetHostedService(svc)
}
}
func (h *Handler) SetHostedPlatformDomain(domain string) {
h.hostedPlatformDomain = strings.ToLower(strings.TrimSpace(domain))
}
func (h *Handler) CheckAddressAvailability(w http.ResponseWriter, r *http.Request) {
svc := h.hostedService()
if svc == nil {
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"available": true, "reason": "hosted_mail_disabled"})
return
}
local := strings.TrimSpace(r.URL.Query().Get("local"))
domain := strings.TrimSpace(r.URL.Query().Get("domain"))
if domain == "" {
domain = strings.TrimSpace(r.URL.Query().Get("domain_name"))
}
if domain == "" {
domain = h.hostedPlatformDomain
}
if local == "" || domain == "" {
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "local", Message: "local and domain required",
}))
return
}
available, err := svc.IsAddressAvailable(r.Context(), domain, local)
if err != nil {
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"available": false, "reason": err.Error()})
return
}
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"available": available})
}
func (h *Handler) GetHostedMailStatus(w http.ResponseWriter, r *http.Request) {
svc := h.hostedService()
if svc == nil || !svc.Available() {
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"enabled": false})
return
}
ctx := r.Context()
domainName := h.hostedPlatformDomain
domainStatus := ""
if domain, err := svc.GetPlatformDomain(ctx); err == nil {
domainName = domain.Name
domainStatus = domain.Status
}
if domainName == "" {
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"enabled": false})
return
}
resp := map[string]any{
"enabled": true,
"platform_domain": domainName,
"domain_status": domainStatus,
"endpoints": svc.Endpoints(),
}
claims := middleware.ClaimsFromContext(ctx)
if claims != nil {
userID, err := h.svc.ResolveUserID(ctx, claims.Sub)
if err == nil {
if mailbox, err := svc.GetUserMailbox(ctx, userID); err == nil {
resp["mailbox"] = mailbox
} else if !errors.Is(err, pgx.ErrNoRows) {
h.logger.Error("hosted mailbox lookup", "error", err)
}
accounts, listErr := h.svc.ListAccounts(ctx, claims.Sub, query.ListParams{})
if listErr == nil {
for _, acct := range accounts.Accounts {
provider, _ := acct["provider"].(string)
if provider == "hosted" {
resp["hosted_mail_account_id"] = acct["id"]
resp["hosted_mail_account_email"] = acct["email"]
break
}
}
}
}
}
apiresponse.WriteJSON(w, http.StatusOK, resp)
}
type setupHostedMailboxRequest struct {
LocalPart string `json:"local_part"`
Password string `json:"password"`
DisplayName string `json:"display_name"`
}
func (h *Handler) SetupHostedMailbox(w http.ResponseWriter, r *http.Request) {
svc := h.hostedService()
if svc == nil || !svc.Available() {
apiresponse.WriteError(w, r, http.StatusServiceUnavailable, "hosted_mail_disabled", "hosted mail is not configured", nil)
return
}
var req setupHostedMailboxRequest
if err := apivalidate.DecodeJSON(w, r, 16*1024, &req); err != nil {
return
}
localPart := strings.TrimSpace(req.LocalPart)
password := req.Password
if localPart == "" {
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
apivalidate.FieldDetail{Field: "local_part", Message: "local part required"},
))
return
}
if len(password) < 8 {
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
apivalidate.FieldDetail{Field: "password", Message: "password must be at least 8 characters"},
))
return
}
domainName := h.hostedPlatformDomain
if domain, err := svc.GetPlatformDomain(r.Context()); err == nil {
domainName = domain.Name
}
if domainName == "" {
apiresponse.WriteError(w, r, http.StatusServiceUnavailable, "hosted_mail_disabled", "platform mail domain not configured", nil)
return
}
claims := middleware.ClaimsFromContext(r.Context())
userID, err := h.svc.ResolveUserID(r.Context(), claims.Sub)
if err != nil {
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "user not provisioned", nil)
return
}
displayName := strings.TrimSpace(req.DisplayName)
if displayName == "" {
displayName = localPart
}
email := strings.ToLower(localPart + "@" + domainName)
result, err := svc.EnsureMailboxProvisioned(r.Context(), hosted.ProvisionMailboxInput{
UserID: userID,
Email: email,
DisplayName: displayName,
Password: password,
})
if err != nil {
switch {
case errors.Is(err, hosted.ErrAddressTaken):
apiresponse.WriteError(w, r, http.StatusConflict, "address_taken", err.Error(), nil)
case errors.Is(err, hosted.ErrInvalidLocalPart):
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
apivalidate.FieldDetail{Field: "local_part", Message: err.Error()},
))
case errors.Is(err, hosted.ErrDomainNotActive):
apiresponse.WriteError(w, r, http.StatusConflict, "domain_not_active", err.Error(), nil)
default:
h.logger.Error("setup hosted mailbox", "error", err, "email", email)
apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "failed to provision mailbox", nil)
}
return
}
if h.accountSync != nil && result.MailAccountID != "" {
if syncErr := h.accountSync.SyncAccountForUser(r.Context(), claims.Sub, result.MailAccountID); syncErr != nil {
h.logger.Warn("hosted mailbox initial sync", "error", syncErr, "account_id", result.MailAccountID)
}
}
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
"mailbox": result.Mailbox,
"mail_account_id": result.MailAccountID,
"email": email,
})
}
func (h *Handler) hostedService() *hosted.Service {
if s, ok := h.svc.(*Service); ok {
return s.HostedService()
}
return nil
}