diff --git a/admin/next.config.ts b/admin/next.config.ts index 97ad710..f731faa 100644 --- a/admin/next.config.ts +++ b/admin/next.config.ts @@ -2,7 +2,6 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", - basePath: "/admin", trailingSlash: true, images: { unoptimized: true, diff --git a/admin/src/app/(dashboard)/apple-social-auth/page.tsx b/admin/src/app/(dashboard)/apple-social-auth/page.tsx index ed7cfbd..a022965 100644 --- a/admin/src/app/(dashboard)/apple-social-auth/page.tsx +++ b/admin/src/app/(dashboard)/apple-social-auth/page.tsx @@ -206,7 +206,7 @@ export default function AppleSocialAuthPage() { {entry.id} {entry.username} diff --git a/admin/src/app/(dashboard)/auth-tokens/page.tsx b/admin/src/app/(dashboard)/auth-tokens/page.tsx index c2b8820..c141780 100644 --- a/admin/src/app/(dashboard)/auth-tokens/page.tsx +++ b/admin/src/app/(dashboard)/auth-tokens/page.tsx @@ -205,7 +205,7 @@ export default function AuthTokensPage() { {token.user_id} {token.username} diff --git a/admin/src/app/(dashboard)/completion-images/page.tsx b/admin/src/app/(dashboard)/completion-images/page.tsx index 1b9c9e0..8d04a8f 100644 --- a/admin/src/app/(dashboard)/completion-images/page.tsx +++ b/admin/src/app/(dashboard)/completion-images/page.tsx @@ -221,7 +221,7 @@ export default function CompletionImagesPage() { {img.task_title || `Task #${img.task_id}`} @@ -234,7 +234,7 @@ export default function CompletionImagesPage() { #{img.completion_id} diff --git a/admin/src/app/(dashboard)/confirmation-codes/page.tsx b/admin/src/app/(dashboard)/confirmation-codes/page.tsx index 0ff0117..2315a16 100644 --- a/admin/src/app/(dashboard)/confirmation-codes/page.tsx +++ b/admin/src/app/(dashboard)/confirmation-codes/page.tsx @@ -212,7 +212,7 @@ export default function ConfirmationCodesPage() { {code.id} {code.username} diff --git a/admin/src/app/(dashboard)/devices/page.tsx b/admin/src/app/(dashboard)/devices/page.tsx index d8b2c04..94fd786 100644 --- a/admin/src/app/(dashboard)/devices/page.tsx +++ b/admin/src/app/(dashboard)/devices/page.tsx @@ -312,7 +312,7 @@ export default function DevicesPage() { {device.user_id ? ( {device.username || `User #${device.user_id}`} diff --git a/admin/src/app/(dashboard)/document-images/page.tsx b/admin/src/app/(dashboard)/document-images/page.tsx index 63e2f07..e5d6369 100644 --- a/admin/src/app/(dashboard)/document-images/page.tsx +++ b/admin/src/app/(dashboard)/document-images/page.tsx @@ -221,7 +221,7 @@ export default function DocumentImagesPage() { {img.document_title || `Document #${img.document_id}`} @@ -229,7 +229,7 @@ export default function DocumentImagesPage() { {img.residence_name || `#${img.residence_id}`} diff --git a/admin/src/app/(dashboard)/notification-prefs/page.tsx b/admin/src/app/(dashboard)/notification-prefs/page.tsx index 92fb22b..5714f11 100644 --- a/admin/src/app/(dashboard)/notification-prefs/page.tsx +++ b/admin/src/app/(dashboard)/notification-prefs/page.tsx @@ -102,7 +102,7 @@ export default function NotificationPrefsPage() { header: 'User', cell: ({ row }) => ( {row.original.username} @@ -265,7 +265,7 @@ export default function NotificationPrefsPage() { Actions - View User + View User [] = [ Actions - View user + View user diff --git a/admin/src/app/(dashboard)/password-reset-codes/page.tsx b/admin/src/app/(dashboard)/password-reset-codes/page.tsx index 5c683c0..2363d6d 100644 --- a/admin/src/app/(dashboard)/password-reset-codes/page.tsx +++ b/admin/src/app/(dashboard)/password-reset-codes/page.tsx @@ -245,7 +245,7 @@ export default function PasswordResetCodesPage() { {code.id} {code.username} diff --git a/admin/src/app/(dashboard)/share-codes/page.tsx b/admin/src/app/(dashboard)/share-codes/page.tsx index 3f742d9..b98c1df 100644 --- a/admin/src/app/(dashboard)/share-codes/page.tsx +++ b/admin/src/app/(dashboard)/share-codes/page.tsx @@ -232,7 +232,7 @@ export default function ShareCodesPage() { {code.residence_name} @@ -240,7 +240,7 @@ export default function ShareCodesPage() { {code.created_by} diff --git a/admin/src/app/(dashboard)/user-profiles/page.tsx b/admin/src/app/(dashboard)/user-profiles/page.tsx index 051720f..1cb3881 100644 --- a/admin/src/app/(dashboard)/user-profiles/page.tsx +++ b/admin/src/app/(dashboard)/user-profiles/page.tsx @@ -206,7 +206,7 @@ export default function UserProfilesPage() { {profile.id} {profile.username} diff --git a/admin/src/components/app-sidebar.tsx b/admin/src/components/app-sidebar.tsx index b3cadd8..1c144e2 100644 --- a/admin/src/components/app-sidebar.tsx +++ b/admin/src/components/app-sidebar.tsx @@ -49,40 +49,40 @@ import { import { Button } from '@/components/ui/button'; const menuItems = [ - { title: 'Dashboard', url: '/admin/', icon: Home }, - { title: 'Users', url: '/admin/users', icon: Users }, - { title: 'User Profiles', url: '/admin/user-profiles', icon: UserCircle }, - { title: 'Apple Sign In', url: '/admin/apple-social-auth', icon: Apple }, - { title: 'Auth Tokens', url: '/admin/auth-tokens', icon: Key }, - { title: 'Confirmation Codes', url: '/admin/confirmation-codes', icon: Mail }, - { title: 'Password Resets', url: '/admin/password-reset-codes', icon: KeyRound }, - { title: 'Residences', url: '/admin/residences', icon: Building2 }, - { title: 'Share Codes', url: '/admin/share-codes', icon: Share2 }, - { title: 'Tasks', url: '/admin/tasks', icon: ClipboardList }, - { title: 'Completions', url: '/admin/completions', icon: CheckCircle }, - { title: 'Completion Images', url: '/admin/completion-images', icon: Image }, - { title: 'Contractors', url: '/admin/contractors', icon: Wrench }, - { title: 'Documents', url: '/admin/documents', icon: FileText }, - { title: 'Document Images', url: '/admin/document-images', icon: ImagePlus }, - { title: 'Notifications', url: '/admin/notifications', icon: Bell }, - { title: 'Notification Prefs', url: '/admin/notification-prefs', icon: BellRing }, - { title: 'Onboarding Emails', url: '/admin/onboarding-emails', icon: MailCheck }, - { title: 'Devices', url: '/admin/devices', icon: Smartphone }, - { title: 'Subscriptions', url: '/admin/subscriptions', icon: CreditCard }, + { title: 'Dashboard', url: '/', icon: Home }, + { title: 'Users', url: '/users', icon: Users }, + { title: 'User Profiles', url: '/user-profiles', icon: UserCircle }, + { title: 'Apple Sign In', url: '/apple-social-auth', icon: Apple }, + { title: 'Auth Tokens', url: '/auth-tokens', icon: Key }, + { title: 'Confirmation Codes', url: '/confirmation-codes', icon: Mail }, + { title: 'Password Resets', url: '/password-reset-codes', icon: KeyRound }, + { title: 'Residences', url: '/residences', icon: Building2 }, + { title: 'Share Codes', url: '/share-codes', icon: Share2 }, + { title: 'Tasks', url: '/tasks', icon: ClipboardList }, + { title: 'Completions', url: '/completions', icon: CheckCircle }, + { title: 'Completion Images', url: '/completion-images', icon: Image }, + { title: 'Contractors', url: '/contractors', icon: Wrench }, + { title: 'Documents', url: '/documents', icon: FileText }, + { title: 'Document Images', url: '/document-images', icon: ImagePlus }, + { title: 'Notifications', url: '/notifications', icon: Bell }, + { title: 'Notification Prefs', url: '/notification-prefs', icon: BellRing }, + { title: 'Onboarding Emails', url: '/onboarding-emails', icon: MailCheck }, + { title: 'Devices', url: '/devices', icon: Smartphone }, + { title: 'Subscriptions', url: '/subscriptions', icon: CreditCard }, ]; const limitationsItems = [ - { title: 'Tier Limits', url: '/admin/limitations', icon: Layers }, - { title: 'Upgrade Triggers', url: '/admin/limitations/triggers', icon: Sparkles }, + { title: 'Tier Limits', url: '/limitations', icon: Layers }, + { title: 'Upgrade Triggers', url: '/limitations/triggers', icon: Sparkles }, ]; const settingsItems = [ - { title: 'Monitoring', url: '/admin/monitoring', icon: Activity }, - { title: 'Automation Reference', url: '/admin/automation-reference', icon: Cog }, - { title: 'Lookup Tables', url: '/admin/lookups', icon: BookOpen }, - { title: 'Task Templates', url: '/admin/task-templates', icon: LayoutTemplate }, - { title: 'Admin Users', url: '/admin/admin-users', icon: UserCog }, - { title: 'Settings', url: '/admin/settings', icon: Settings }, + { title: 'Monitoring', url: '/monitoring', icon: Activity }, + { title: 'Automation Reference', url: '/automation-reference', icon: Cog }, + { title: 'Lookup Tables', url: '/lookups', icon: BookOpen }, + { title: 'Task Templates', url: '/task-templates', icon: LayoutTemplate }, + { title: 'Admin Users', url: '/admin-users', icon: UserCog }, + { title: 'Settings', url: '/settings', icon: Settings }, ]; export function AppSidebar() { @@ -134,7 +134,7 @@ export function AppSidebar() { diff --git a/admin/src/lib/api.ts b/admin/src/lib/api.ts index b272371..434d9f6 100644 --- a/admin/src/lib/api.ts +++ b/admin/src/lib/api.ts @@ -41,7 +41,7 @@ api.interceptors.response.use( if (error.response?.status === 401) { if (typeof window !== 'undefined') { localStorage.removeItem('admin_token'); - window.location.href = '/admin/login/'; + window.location.href = '/login/'; } } return Promise.reject(error); diff --git a/internal/admin/routes.go b/internal/admin/routes.go index b7f5c06..86baa5f 100644 --- a/internal/admin/routes.go +++ b/internal/admin/routes.go @@ -5,6 +5,7 @@ import ( "net/http/httputil" "net/url" "os" + "strings" "github.com/labstack/echo/v4" "gorm.io/gorm" @@ -447,7 +448,10 @@ func SetupRoutes(router *echo.Echo, db *gorm.DB, cfg *config.Config, deps *Depen setupAdminProxy(router) } -// setupAdminProxy configures reverse proxy to the Next.js admin panel +// setupAdminProxy configures reverse proxy to the Next.js admin panel. +// When ADMIN_HOST is set (e.g. admin.myhoneydue.com), requests to that +// subdomain are proxied to Next.js at the root path. Requests to /admin/* +// on the admin subdomain redirect to the main web app. func setupAdminProxy(router *echo.Echo) { // Get admin panel URL from env, default to localhost:3001 // Note: In production (Dokku), Next.js runs on internal port 3001 @@ -456,19 +460,6 @@ func setupAdminProxy(router *echo.Echo) { adminURL = "http://127.0.0.1:3001" } - // Admin subdomain (e.g. admin.myhoneydue.com) — redirect root to /admin/ - adminHost := os.Getenv("ADMIN_HOST") - if adminHost != "" { - router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if c.Request().Host == adminHost && c.Request().URL.Path == "/" { - return c.Redirect(http.StatusMovedPermanently, "/admin/") - } - return next(c) - } - }) - } - target, err := url.Parse(adminURL) if err != nil { return @@ -476,18 +467,40 @@ func setupAdminProxy(router *echo.Echo) { proxy := httputil.NewSingleHostReverseProxy(target) - // Handle all /admin/* requests - router.Any("/admin/*", func(c echo.Context) error { - proxy.ServeHTTP(c.Response(), c.Request()) - return nil - }) + // Admin subdomain: proxy all non-API requests to Next.js + adminHost := os.Getenv("ADMIN_HOST") + webAppURL := os.Getenv("WEB_APP_URL") + if webAppURL == "" { + webAppURL = "https://myhoneydue.com" + } - // Also handle /admin without trailing path - router.Any("/admin", func(c echo.Context) error { - return c.Redirect(http.StatusMovedPermanently, "/admin/") - }) + if adminHost != "" { + router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Request().Host != adminHost { + return next(c) + } - // Proxy Next.js static assets + path := c.Request().URL.Path + + // Redirect /admin/* to the main web app + if strings.HasPrefix(path, "/admin") { + return c.Redirect(http.StatusMovedPermanently, webAppURL) + } + + // Let /api/* routes pass through to the Go API + if strings.HasPrefix(path, "/api/") { + return next(c) + } + + // Proxy everything else to Next.js admin + proxy.ServeHTTP(c.Response(), c.Request()) + return nil + } + }) + } + + // Proxy Next.js static assets (served from /_next/ regardless of host) router.Any("/_next/*", func(c echo.Context) error { proxy.ServeHTTP(c.Response(), c.Request()) return nil diff --git a/internal/router/router.go b/internal/router/router.go index 98093ac..822925c 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "os" "strings" "time" @@ -73,7 +74,9 @@ func SetupRouter(deps *Dependencies) *echo.Echo { // timeout middleware wraps the response writer in *http.timeoutWriter // which does not implement http.Flusher, causing a panic when // httputil.ReverseProxy or WebSocket upgraders try to flush. - return strings.HasPrefix(path, "/admin") || + // Also skip for admin subdomain (all requests proxied to Next.js). + adminHost := os.Getenv("ADMIN_HOST") + return (adminHost != "" && c.Request().Host == adminHost) || strings.HasPrefix(path, "/_next") || strings.HasSuffix(path, "/ws") },