package mail import ( "encoding/json" "net/http" "strings" "time" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" "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/apitokens" ) type createApiTokenRequest struct { Name string `json:"name"` Permissions []apitokens.PermissionGrant `json:"permissions"` MailScope apitokens.MailScope `json:"mail_scope"` DriveScope apitokens.DriveScope `json:"drive_scope"` ExpiresAt *time.Time `json:"expires_at,omitempty"` } func (h *Handler) ListApiTokens(w http.ResponseWriter, r *http.Request) { claims := middleware.ClaimsFromContext(r.Context()) db := h.db() if db == nil { apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "database unavailable", nil) return } tokens, err := apitokens.List(r.Context(), db, claims.Sub) if err != nil { apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "failed to list api tokens", nil) return } apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"tokens": tokens}) } func (h *Handler) CreateApiToken(w http.ResponseWriter, r *http.Request) { claims := middleware.ClaimsFromContext(r.Context()) var req createApiTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid json", nil) return } name := strings.TrimSpace(req.Name) if name == "" { apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{ Field: "name", Message: "name is required", })) return } if !hasAnyPermission(req.Permissions) { apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{ Field: "permissions", Message: "at least one permission is required", })) return } db := h.db() if db == nil { apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "database unavailable", nil) return } created, err := apitokens.Create( r.Context(), db, claims.Sub, name, req.Permissions, normalizeMailScope(req.MailScope), normalizeDriveScope(req.DriveScope), req.ExpiresAt, ) if err != nil { apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "failed to create api token", nil) return } apiresponse.WriteJSON(w, http.StatusCreated, created) } func (h *Handler) RevokeApiToken(w http.ResponseWriter, r *http.Request) { claims := middleware.ClaimsFromContext(r.Context()) tokenID := chi.URLParam(r, "tokenID") db := h.db() if db == nil { apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "database unavailable", nil) return } if err := apitokens.Revoke(r.Context(), db, claims.Sub, tokenID); err != nil { if err == apitokens.ErrNotFound { apiresponse.WriteError(w, r, http.StatusNotFound, apiresponse.CodeNotFound, "api token not found", nil) return } apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "failed to revoke api token", nil) return } w.WriteHeader(http.StatusNoContent) } func hasAnyPermission(grants []apitokens.PermissionGrant) bool { for _, g := range grants { if g.Read || g.Write { return true } } return false } func normalizeMailScope(scope apitokens.MailScope) apitokens.MailScope { if scope.AllAccounts || len(scope.AccountIDs) == 0 { return apitokens.MailScope{AllAccounts: true, AccountIDs: nil} } return scope } func normalizeDriveScope(scope apitokens.DriveScope) apitokens.DriveScope { if scope.AllFolders || len(scope.FolderPaths) == 0 { return apitokens.DriveScope{AllFolders: true, FolderPaths: nil} } return scope } func (h *Handler) db() *pgxpool.Pool { if s, ok := h.svc.(*Service); ok { return s.DB() } return nil }