From cf46745cadce4b88a640225544e7a49d48d3ff66 Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 18:17:25 +0200 Subject: [PATCH 1/7] Added Dashboard Pages --- bugs.md | 261 ++++++++++++++++++ .../admin/(protected)/applications/page.tsx | 10 + .../admin/(protected)/courses/page.tsx | 10 + .../admin/(protected)/settings/page.tsx | 10 + .../admin/(protected)/students/page.tsx | 10 + 5 files changed, 301 insertions(+) create mode 100644 bugs.md create mode 100644 next-ts/app/(dashboard)/admin/(protected)/applications/page.tsx create mode 100644 next-ts/app/(dashboard)/admin/(protected)/courses/page.tsx create mode 100644 next-ts/app/(dashboard)/admin/(protected)/settings/page.tsx create mode 100644 next-ts/app/(dashboard)/admin/(protected)/students/page.tsx diff --git a/bugs.md b/bugs.md new file mode 100644 index 0000000..e91666a --- /dev/null +++ b/bugs.md @@ -0,0 +1,261 @@ +# Moipone Admin Bug Report + +Repo: `github.com/blebelo/moipone` · Frontend: `next-ts/` · Date: 2026-06-04 + +--- + +## Critical + +### BUG-01 — Token read from wrong storage; all authenticated API calls have no auth header + +**File:** `src/lib/utils/axiosInstance.ts` + +The token is written to `localStorage` in the auth provider but `axiosInstance` reads from `sessionStorage`. `sessionStorage` never receives the `"token"` key, so `token` is always `null` and every API call goes out with no `Authorization` header. + +```ts +// auth-provider/index.tsx +localStorage.setItem("token", token); // ← stored here + +// axiosInstance.ts +sessionStorage.getItem("token") // ← reads the wrong store, always null +``` + +**Fix:** +```diff +- ? sessionStorage.getItem("token") ++ ? localStorage.getItem("token") +``` + +--- + +### BUG-02 — Dashboard pages don't exist; login redirect always hits a 404 + +**File:** `app/(dashboard)/admin/page.tsx` + missing dashboard routes + +`authenticate()` is called with no `redirectPath`, so the auth provider defaults to `router.push('/admin/dashboard')`. The page at that path (`app/admin/dashboard/page.tsx` or equivalent) does not exist in the repository. After a successful login the user lands on a blank 404 page every time. + +Pages missing that `constants.tsx` and the auth provider already reference: + +| Route | File needed | +|---|---| +| `/admin/dashboard` | `app/admin/(workspace)/dashboard/page.tsx` | +| `/admin/courses` | `app/admin/(workspace)/courses/page.tsx` | +| `/admin/students` | `app/admin/(workspace)/students/page.tsx` | +| `/admin/applications` | `app/admin/(workspace)/applications/page.tsx` | +| `/admin/settings` | `app/admin/(workspace)/settings/page.tsx` | + +--- + +## High + +### BUG-03 — No auth guard on workspace routes; dashboard is publicly accessible once built + +**File:** `app/(dashboard)/admin/layout.tsx` + +There is no `AuthGuard` component, no middleware route check, and no HOC protecting authenticated routes. Once the dashboard pages exist (BUG-02), any unauthenticated user who navigates directly to `/admin/dashboard` will see them without logging in. `AuthProvider` is present but nothing consumes `currentUser` to enforce a redirect. + +**Fix:** Create `src/components/AuthGuard/index.tsx`: + +```tsx +'use client'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthState } from '@/src/providers/auth-provider'; + +export default function AuthGuard({ children }: { children: React.ReactNode }) { + const { currentUser, isPending } = useAuthState(); + const router = useRouter(); + + useEffect(() => { + if (!isPending && !currentUser) router.replace('/admin'); + }, [currentUser, isPending, router]); + + if (!currentUser) return null; + return <>{children}; +} +``` + +Wrap workspace layout with it (see BUG-05 for the layout refactor). + +--- + +### BUG-04 — No redirect-if-authenticated on login page; logged-in users see the login form + +**File:** `app/(dashboard)/admin/page.tsx` + +When a user is already authenticated and visits `/admin`, the login form renders normally. There is no check for an existing session that would send them to `/admin/dashboard` instead. Combined with BUG-02, this also means a user who refreshes after a successful login lands back on the login page. + +**Fix:** Add a client-side redirect in the login page or its layout: + +```tsx +'use client'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthState } from '@/src/providers/auth-provider'; + +export function useRedirectIfAuthenticated() { + const { currentUser } = useAuthState(); + const router = useRouter(); + useEffect(() => { + if (currentUser) router.replace('/admin/dashboard'); + }, [currentUser, router]); +} +``` + +--- + +## Medium + +### BUG-05 — `(dashboard)` route group wraps one folder and provides no benefit + +**Files:** `app/(dashboard)/` directory + +The `(dashboard)` route group contains only `admin/` and has no `layout.tsx` at the group level. Route groups exist to share layouts across sibling URL segments or to organise code; here it does neither. The name is also misleading — it sounds like it scopes the dashboard pages, but the actual dashboard doesn't exist yet. This creates the confusing path `app/(dashboard)/admin/` where both the group and its child need to be navigated to find any file. + +**Fix:** Remove `(dashboard)/` and move `admin/` directly under `app/`. Split into `(auth)/` and `(workspace)/` route groups inside `admin/` to give login and dashboard separate layouts: + +``` +app/ + admin/ + (auth)/ ← login page only — bare layout + layout.tsx + page.tsx ← /admin + style.ts + (workspace)/ ← authenticated pages — shell + auth guard + layout.tsx + dashboard/page.tsx + courses/page.tsx + ... + (public)/ + (student)/ +``` + +--- + +### BUG-06 — Login page loads all five data providers unnecessarily + +**File:** `app/(dashboard)/admin/layout.tsx` + +`AdminRootLayout` wraps every page — including the login form — with `AuthProvider`, `StudentProvider`, `CourseProvider`, `ApplicationProvider`, and `ContactProvider`. The login page needs only `AuthProvider`. The four data providers each initialise their own state and may trigger API calls or setup work on a page that has no use for them. + +**Fix:** After the BUG-05 restructure, `(auth)/layout.tsx` should contain only `AuthProvider`. Data providers belong exclusively in `(workspace)/layout.tsx`. + +--- + +### BUG-07 — Middleware rewrites cross-host violations to `/404`, a path with no page + +**File:** `src/middleware.ts` line ~37 + +```ts +url.pathname = "/404"; +return NextResponse.rewrite(url); +``` + +When a scoped path is accessed from the wrong host (e.g. hitting a `/student` URL from `admin.moiponeacademy.org`), the middleware rewrites to `/404`. But `app/404/page.tsx` does not exist. Next.js's custom 404 lives at `app/not-found.tsx` and is triggered by `notFound()` or an unmatched route — not by a URL path. The rewrite silently falls through to Next.js's built-in 404, bypassing the custom `NotFoundPage` component entirely. + +**Fix:** Replace the pathname rewrite with Next.js's built-in mechanism: + +```ts +import { notFound } from 'next/navigation'; + +// instead of: +url.pathname = "/404"; +return NextResponse.rewrite(url); + +// use: +return NextResponse.rewrite(new URL('/not-found', request.url)); +``` + +Or add an actual `app/404/page.tsx` that renders the same `NotFoundPage` component. + +--- + +### BUG-08 — Auth cookie is set on login but never read anywhere + +**File:** `src/providers/auth-provider/index.tsx` + +```ts +document.cookie = `token=${token}; path=/; secure; samesite=strict`; +``` + +This cookie is set on every successful login, but nothing in the codebase reads it. `axiosInstance` reads from `sessionStorage` (currently broken) or `localStorage` (after the BUG-01 fix). The middleware does not check for it. No server component or route handler references it. The line is dead code today, and the `secure` flag means it won't even be set in a non-HTTPS local development environment. + +**Fix:** Either remove the line entirely, or replace it with a server-side `httpOnly` cookie set via an API route if cookie-based auth is a future goal. + +--- + +### BUG-09 — `logout()` uses an implicit root redirect that depends on middleware behaviour + +**File:** `src/providers/auth-provider/index.tsx` + +```ts +router.push('/'); +``` + +On `admin.moiponeacademy.org`, pushing `/` works only because the middleware rewrites it to `/admin/`, which then resolves to the login page. This is an implicit dependency on middleware internals. If the middleware rewrite rules change, logout silently stops redirecting to the login page with no error — it would land on the public site's root instead. + +**Fix:** +```diff +- router.push('/') ++ router.push('/admin') +``` + +--- + +## Low + +### BUG-10 — `sessionStorage` cleared on tab close causes stale UI state on re-open + +**File:** `src/providers/auth-provider/index.tsx` + +`role`, `Id`, and `userName` are written to `sessionStorage`, which is cleared when the tab closes. On re-open, the `localStorage` token is still valid and the `useEffect` recovery path decodes the JWT to rebuild `currentUser`. However, any code that reads `sessionStorage.getItem("role")` or `sessionStorage.getItem("Id")` directly (outside the React state) will return `null` after a tab re-open even though the user is authenticated. This creates a window between mount and the `useEffect` running where these values are inconsistently unavailable. + +**Fix:** Derive role/Id/userName exclusively from the decoded JWT inside the auth provider state rather than ever reading `sessionStorage` keys outside of it. + +--- + +### BUG-11 — No axios interceptor for 401; expired tokens never trigger logout + +**File:** `src/lib/utils/axiosInstance.ts` + +There is no response interceptor on the axios instance. When the JWT expires, API calls return `401 Unauthorized` silently. The UI stays in an "authenticated" state (token still in `localStorage`, `currentUser` still in context) while every data request fails. The user is never logged out and sees no meaningful error. + +**Fix:** Add a response interceptor: + +```ts +instance.interceptors.response.use( + (res) => res, + (err) => { + if (err.response?.status === 401) { + localStorage.removeItem('token'); + sessionStorage.clear(); + window.location.replace('/admin'); + } + return Promise.reject(err); + } +); +``` + +--- + +### BUG-12 — `axiosInstance` captures token at call time, not at request time + +**File:** `src/lib/utils/axiosInstance.ts` + +`axiosInstance()` reads the token once when the function is called and bakes it into the `axios.create()` defaults. If a provider calls `axiosInstance()` during initialisation (before the auth `useEffect` runs and sets the token), all requests from that instance will have no auth header even after the user is authenticated in state. + +**Fix:** Read the token inside a request interceptor instead, so it is resolved freshly on every request: + +```ts +const instance = axios.create({ baseURL }); + +instance.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; +}); +``` + +--- + +*12 bugs total — 2 critical, 2 high, 5 medium, 3 low.* \ No newline at end of file diff --git a/next-ts/app/(dashboard)/admin/(protected)/applications/page.tsx b/next-ts/app/(dashboard)/admin/(protected)/applications/page.tsx new file mode 100644 index 0000000..c692839 --- /dev/null +++ b/next-ts/app/(dashboard)/admin/(protected)/applications/page.tsx @@ -0,0 +1,10 @@ +'use client' + +export default function ApplicationsPage() { + return ( +
+

Applications

+

This section will display the admin application management dashboard.

+
+ ); +} diff --git a/next-ts/app/(dashboard)/admin/(protected)/courses/page.tsx b/next-ts/app/(dashboard)/admin/(protected)/courses/page.tsx new file mode 100644 index 0000000..35e0a1b --- /dev/null +++ b/next-ts/app/(dashboard)/admin/(protected)/courses/page.tsx @@ -0,0 +1,10 @@ +'use client' + +export default function CoursesPage() { + return ( +
+

Courses

+

This section will display the admin course management dashboard.

+
+ ); +} diff --git a/next-ts/app/(dashboard)/admin/(protected)/settings/page.tsx b/next-ts/app/(dashboard)/admin/(protected)/settings/page.tsx new file mode 100644 index 0000000..254a0fb --- /dev/null +++ b/next-ts/app/(dashboard)/admin/(protected)/settings/page.tsx @@ -0,0 +1,10 @@ +'use client' + +export default function SettingsPage() { + return ( +
+

Settings

+

This section will display the admin settings dashboard.

+
+ ); +} diff --git a/next-ts/app/(dashboard)/admin/(protected)/students/page.tsx b/next-ts/app/(dashboard)/admin/(protected)/students/page.tsx new file mode 100644 index 0000000..760458e --- /dev/null +++ b/next-ts/app/(dashboard)/admin/(protected)/students/page.tsx @@ -0,0 +1,10 @@ +'use client' + +export default function StudentsPage() { + return ( +
+

Students

+

This section will display the admin student management dashboard.

+
+ ); +} From 30d8ffc5123f97269400d4596a1ae88eb0543b4a Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 18:23:54 +0200 Subject: [PATCH 2/7] Refactored Token Storage --- next-ts/src/lib/AuthGuard/index.tsx | 4 ++-- next-ts/src/lib/utils/axiosInstance.ts | 2 +- next-ts/src/providers/auth-provider/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/next-ts/src/lib/AuthGuard/index.tsx b/next-ts/src/lib/AuthGuard/index.tsx index 0560f06..b2101ac 100644 --- a/next-ts/src/lib/AuthGuard/index.tsx +++ b/next-ts/src/lib/AuthGuard/index.tsx @@ -28,7 +28,7 @@ const WithAuth = ({ children, redirectTo }: WithAuthProps) => { useEffect(() => { const unauthorizedRedirect = redirectTo || getDefaultRedirectPath(); - const token = sessionStorage.getItem("token"); + const token = localStorage.getItem("token"); if (!token) { router.replace(unauthorizedRedirect); @@ -46,7 +46,7 @@ const WithAuth = ({ children, redirectTo }: WithAuthProps) => { setAuthorized(true); } catch { - sessionStorage.removeItem("token"); + localStorage.removeItem("token"); router.replace(unauthorizedRedirect); setAuthorized(false); } diff --git a/next-ts/src/lib/utils/axiosInstance.ts b/next-ts/src/lib/utils/axiosInstance.ts index c928d76..b08964c 100644 --- a/next-ts/src/lib/utils/axiosInstance.ts +++ b/next-ts/src/lib/utils/axiosInstance.ts @@ -3,7 +3,7 @@ import axios from "axios"; export const axiosInstance = (isAppService: boolean = true) => { const token = typeof window !== "undefined" - ? sessionStorage.getItem("token") + ? localStorage.getItem("token") : null; const rawBaseUrl = process.env.NEXT_PUBLIC_API_LINK ?? ""; diff --git a/next-ts/src/providers/auth-provider/index.tsx b/next-ts/src/providers/auth-provider/index.tsx index 776ad7d..55af936 100644 --- a/next-ts/src/providers/auth-provider/index.tsx +++ b/next-ts/src/providers/auth-provider/index.tsx @@ -26,7 +26,7 @@ export const AuthProvider = ({children}: {children: React.ReactNode}) => { if (!exp || Date.now() >= exp * 1000) { localStorage.removeItem("token"); - sessionStorage.clear(); + localStorage.clear(); return; } From 727173617d8fb0d8fedcee2e508f0054ba65799c Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 18:30:32 +0200 Subject: [PATCH 3/7] #2 Refactored Admin Folder Structure --- .../admin/(protected)/applications/page.tsx | 0 .../{(dashboard) => (admin)}/admin/(protected)/courses/page.tsx | 0 .../{(dashboard) => (admin)}/admin/(protected)/dashboard/page.tsx | 0 .../{(dashboard) => (admin)}/admin/(protected)/dashboard/style.ts | 0 next-ts/app/{(dashboard) => (admin)}/admin/(protected)/layout.tsx | 0 .../{(dashboard) => (admin)}/admin/(protected)/settings/page.tsx | 0 .../{(dashboard) => (admin)}/admin/(protected)/students/page.tsx | 0 next-ts/app/{(dashboard) => (admin)}/admin/layout.tsx | 0 next-ts/app/{(dashboard) => (admin)}/admin/page.tsx | 0 next-ts/app/{(dashboard) => (admin)}/admin/style.ts | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/applications/page.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/courses/page.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/dashboard/page.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/dashboard/style.ts (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/layout.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/settings/page.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/(protected)/students/page.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/layout.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/page.tsx (100%) rename next-ts/app/{(dashboard) => (admin)}/admin/style.ts (100%) diff --git a/next-ts/app/(dashboard)/admin/(protected)/applications/page.tsx b/next-ts/app/(admin)/admin/(protected)/applications/page.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/applications/page.tsx rename to next-ts/app/(admin)/admin/(protected)/applications/page.tsx diff --git a/next-ts/app/(dashboard)/admin/(protected)/courses/page.tsx b/next-ts/app/(admin)/admin/(protected)/courses/page.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/courses/page.tsx rename to next-ts/app/(admin)/admin/(protected)/courses/page.tsx diff --git a/next-ts/app/(dashboard)/admin/(protected)/dashboard/page.tsx b/next-ts/app/(admin)/admin/(protected)/dashboard/page.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/dashboard/page.tsx rename to next-ts/app/(admin)/admin/(protected)/dashboard/page.tsx diff --git a/next-ts/app/(dashboard)/admin/(protected)/dashboard/style.ts b/next-ts/app/(admin)/admin/(protected)/dashboard/style.ts similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/dashboard/style.ts rename to next-ts/app/(admin)/admin/(protected)/dashboard/style.ts diff --git a/next-ts/app/(dashboard)/admin/(protected)/layout.tsx b/next-ts/app/(admin)/admin/(protected)/layout.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/layout.tsx rename to next-ts/app/(admin)/admin/(protected)/layout.tsx diff --git a/next-ts/app/(dashboard)/admin/(protected)/settings/page.tsx b/next-ts/app/(admin)/admin/(protected)/settings/page.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/settings/page.tsx rename to next-ts/app/(admin)/admin/(protected)/settings/page.tsx diff --git a/next-ts/app/(dashboard)/admin/(protected)/students/page.tsx b/next-ts/app/(admin)/admin/(protected)/students/page.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/(protected)/students/page.tsx rename to next-ts/app/(admin)/admin/(protected)/students/page.tsx diff --git a/next-ts/app/(dashboard)/admin/layout.tsx b/next-ts/app/(admin)/admin/layout.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/layout.tsx rename to next-ts/app/(admin)/admin/layout.tsx diff --git a/next-ts/app/(dashboard)/admin/page.tsx b/next-ts/app/(admin)/admin/page.tsx similarity index 100% rename from next-ts/app/(dashboard)/admin/page.tsx rename to next-ts/app/(admin)/admin/page.tsx diff --git a/next-ts/app/(dashboard)/admin/style.ts b/next-ts/app/(admin)/admin/style.ts similarity index 100% rename from next-ts/app/(dashboard)/admin/style.ts rename to next-ts/app/(admin)/admin/style.ts From b93fb5aa1e8f45c7679a9ecc67e8fc7b0a3f11be Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 18:31:45 +0200 Subject: [PATCH 4/7] Refactored Token Storage --- .../admin/{(protected) => (workspace)}/applications/page.tsx | 0 .../(admin)/admin/{(protected) => (workspace)}/courses/page.tsx | 0 .../(admin)/admin/{(protected) => (workspace)}/dashboard/page.tsx | 0 .../(admin)/admin/{(protected) => (workspace)}/dashboard/style.ts | 0 next-ts/app/(admin)/admin/{(protected) => (workspace)}/layout.tsx | 0 .../(admin)/admin/{(protected) => (workspace)}/settings/page.tsx | 0 .../(admin)/admin/{(protected) => (workspace)}/students/page.tsx | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/applications/page.tsx (100%) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/courses/page.tsx (100%) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/dashboard/page.tsx (100%) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/dashboard/style.ts (100%) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/layout.tsx (100%) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/settings/page.tsx (100%) rename next-ts/app/(admin)/admin/{(protected) => (workspace)}/students/page.tsx (100%) diff --git a/next-ts/app/(admin)/admin/(protected)/applications/page.tsx b/next-ts/app/(admin)/admin/(workspace)/applications/page.tsx similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/applications/page.tsx rename to next-ts/app/(admin)/admin/(workspace)/applications/page.tsx diff --git a/next-ts/app/(admin)/admin/(protected)/courses/page.tsx b/next-ts/app/(admin)/admin/(workspace)/courses/page.tsx similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/courses/page.tsx rename to next-ts/app/(admin)/admin/(workspace)/courses/page.tsx diff --git a/next-ts/app/(admin)/admin/(protected)/dashboard/page.tsx b/next-ts/app/(admin)/admin/(workspace)/dashboard/page.tsx similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/dashboard/page.tsx rename to next-ts/app/(admin)/admin/(workspace)/dashboard/page.tsx diff --git a/next-ts/app/(admin)/admin/(protected)/dashboard/style.ts b/next-ts/app/(admin)/admin/(workspace)/dashboard/style.ts similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/dashboard/style.ts rename to next-ts/app/(admin)/admin/(workspace)/dashboard/style.ts diff --git a/next-ts/app/(admin)/admin/(protected)/layout.tsx b/next-ts/app/(admin)/admin/(workspace)/layout.tsx similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/layout.tsx rename to next-ts/app/(admin)/admin/(workspace)/layout.tsx diff --git a/next-ts/app/(admin)/admin/(protected)/settings/page.tsx b/next-ts/app/(admin)/admin/(workspace)/settings/page.tsx similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/settings/page.tsx rename to next-ts/app/(admin)/admin/(workspace)/settings/page.tsx diff --git a/next-ts/app/(admin)/admin/(protected)/students/page.tsx b/next-ts/app/(admin)/admin/(workspace)/students/page.tsx similarity index 100% rename from next-ts/app/(admin)/admin/(protected)/students/page.tsx rename to next-ts/app/(admin)/admin/(workspace)/students/page.tsx From 051cdc3ef7f62639622fd2ce4fb2a01fb46399ec Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 18:41:10 +0200 Subject: [PATCH 5/7] Implemented Auth Guard --- next-ts/app/(admin)/admin/(workspace)/layout.tsx | 2 +- next-ts/app/(admin)/admin/layout.tsx | 5 ++++- next-ts/app/(public)/layout.tsx | 4 +++- next-ts/app/(student)/layout.tsx | 5 ++++- next-ts/app/layout.tsx | 4 +++- next-ts/src/{lib => components}/AuthGuard/index.tsx | 2 +- 6 files changed, 16 insertions(+), 6 deletions(-) rename next-ts/src/{lib => components}/AuthGuard/index.tsx (96%) diff --git a/next-ts/app/(admin)/admin/(workspace)/layout.tsx b/next-ts/app/(admin)/admin/(workspace)/layout.tsx index 269522d..5b18341 100644 --- a/next-ts/app/(admin)/admin/(workspace)/layout.tsx +++ b/next-ts/app/(admin)/admin/(workspace)/layout.tsx @@ -1,4 +1,4 @@ -import WithAuth from '@/src/lib/AuthGuard'; +import WithAuth from '@/src/components/AuthGuard'; import AdminLayout from '@/src/components/Admin/AdminLayout'; export default function AdminProtectedLayout({ diff --git a/next-ts/app/(admin)/admin/layout.tsx b/next-ts/app/(admin)/admin/layout.tsx index 5d39157..3736ab3 100644 --- a/next-ts/app/(admin)/admin/layout.tsx +++ b/next-ts/app/(admin)/admin/layout.tsx @@ -3,6 +3,7 @@ import { CourseProvider } from "@/src/providers/course-provider"; import { ApplicationProvider } from "@/src/providers/application-provider"; import { ContactProvider } from "@/src/providers/contact-provider"; import { AuthProvider } from "@/src/providers/auth-provider"; +import AuthGuard from "@/src/components/AuthGuard"; export default function AdminRootLayout({ children, @@ -15,7 +16,9 @@ export default function AdminRootLayout({ - {children} + + {children} + diff --git a/next-ts/app/(public)/layout.tsx b/next-ts/app/(public)/layout.tsx index a0fe38b..b115cc4 100644 --- a/next-ts/app/(public)/layout.tsx +++ b/next-ts/app/(public)/layout.tsx @@ -14,7 +14,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/next-ts/app/(student)/layout.tsx b/next-ts/app/(student)/layout.tsx index a29a8a6..ffd56e7 100644 --- a/next-ts/app/(student)/layout.tsx +++ b/next-ts/app/(student)/layout.tsx @@ -1,3 +1,4 @@ +import AuthGuard from '@/src/components/AuthGuard'; import { ApplicationProvider } from '@/src/providers/application-provider'; import { AuthProvider } from '@/src/providers/auth-provider'; @@ -8,7 +9,9 @@ export default function StudentLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/next-ts/app/layout.tsx b/next-ts/app/layout.tsx index 31ef5bc..acf7337 100644 --- a/next-ts/app/layout.tsx +++ b/next-ts/app/layout.tsx @@ -22,7 +22,9 @@ export default function AppLayout({ return ( - {children} + + {children} + diff --git a/next-ts/src/lib/AuthGuard/index.tsx b/next-ts/src/components/AuthGuard/index.tsx similarity index 96% rename from next-ts/src/lib/AuthGuard/index.tsx rename to next-ts/src/components/AuthGuard/index.tsx index b2101ac..950b196 100644 --- a/next-ts/src/lib/AuthGuard/index.tsx +++ b/next-ts/src/components/AuthGuard/index.tsx @@ -2,7 +2,7 @@ import { useRouter } from "next/navigation"; import { ReactNode, useEffect, useState } from "react"; -import { decodeToken } from "../utils/decoder"; +import { decodeToken } from "../../lib/utils/decoder"; interface WithAuthProps { children: ReactNode; From cda422dee66682294574a714a38856d6dc541b25 Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 19:04:09 +0200 Subject: [PATCH 6/7] Implemented Interceptor --- next-ts/src/components/AuthGuard/index.tsx | 4 +- next-ts/src/lib/utils/axiosInstance.ts | 70 +++++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/next-ts/src/components/AuthGuard/index.tsx b/next-ts/src/components/AuthGuard/index.tsx index 950b196..921b8f1 100644 --- a/next-ts/src/components/AuthGuard/index.tsx +++ b/next-ts/src/components/AuthGuard/index.tsx @@ -10,11 +10,11 @@ interface WithAuthProps { } const getDefaultRedirectPath = () => { - if (typeof window === "undefined") { + if (globalThis.window === undefined) { return "/admin"; } - const host = window.location.host.toLowerCase(); + const host = globalThis.window.location.host.toLowerCase(); if (host.startsWith("student.")) { return "/student"; } diff --git a/next-ts/src/lib/utils/axiosInstance.ts b/next-ts/src/lib/utils/axiosInstance.ts index b08964c..3f91179 100644 --- a/next-ts/src/lib/utils/axiosInstance.ts +++ b/next-ts/src/lib/utils/axiosInstance.ts @@ -1,22 +1,76 @@ import axios from "axios"; -export const axiosInstance = (isAppService: boolean = true) => { - const token = - typeof window !== "undefined" - ? localStorage.getItem("token") - : null; +const LOGIN_PATH_BY_SUBDOMAIN: Record = { + admin: "/admin", + student: "/student", +}; + +const getUnauthorizedRedirectPath = () => { + if (globalThis.window === undefined) return null; + + const { hostname, pathname } = new URL(globalThis.window.location.href); + const subdomainRedirect = LOGIN_PATH_BY_SUBDOMAIN[hostname.split(".")[0]]; + + if (subdomainRedirect) return subdomainRedirect; + + if (/^\/(student|withdraw)(\/|$)/.test(pathname)) { + return "/student"; + } + if (/^\/admin(\/|$)/.test(pathname)) { + return "/admin"; + } + + return null; +}; + +export const axiosInstance = (isAppService: boolean = true) => { const rawBaseUrl = process.env.NEXT_PUBLIC_API_LINK ?? ""; const baseURL = isAppService ? rawBaseUrl : rawBaseUrl.replace(/\/services\/app\/?$/, ""); - return axios.create({ + const instance = axios.create({ baseURL, headers: { "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), }, }); -}; \ No newline at end of file + + instance.interceptors.request.use((config) => { + const token = globalThis.window === undefined ? null : localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }); + + instance.interceptors.response.use( + (response) => response, + (error) => { + if (globalThis.window !== undefined && error.response?.status === 401) { + const hadToken = Boolean(localStorage.getItem("token")); + const redirectPath = getUnauthorizedRedirectPath(); + + localStorage.removeItem("token"); + sessionStorage.clear(); + + const currentPath = globalThis.window.location.pathname.replace(/\/$/, "") || "/"; + + if ( + hadToken && + redirectPath && + currentPath !== redirectPath + ) { + globalThis.location.replace(redirectPath); + } + } + + return Promise.reject(error); + }, + ); + + return instance; +}; From 44c2646027599db7b8fc481f0ac40a8f521f596f Mon Sep 17 00:00:00 2001 From: =Benny= Date: Fri, 5 Jun 2026 19:19:12 +0200 Subject: [PATCH 7/7] Implemented Admin Portal Fixes --- bugs.md | 79 ++++++++++++++++--- .../app/(admin)/admin/(workspace)/layout.tsx | 20 +++-- next-ts/app/(admin)/admin/layout.tsx | 21 +---- next-ts/app/(admin)/admin/page.tsx | 11 ++- next-ts/src/providers/auth-provider/index.tsx | 9 +-- 5 files changed, 94 insertions(+), 46 deletions(-) diff --git a/bugs.md b/bugs.md index e91666a..09edfda 100644 --- a/bugs.md +++ b/bugs.md @@ -9,6 +9,10 @@ Repo: `github.com/blebelo/moipone` · Frontend: `next-ts/` · Date: 2026-06-04 ### BUG-01 — Token read from wrong storage; all authenticated API calls have no auth header **File:** `src/lib/utils/axiosInstance.ts` + +**Status:** Fixed on 2026-06-05. + +**Verified:** `axiosInstance` now reads the token from `localStorage` inside the request interceptor before each request. The token is written to `localStorage` in the auth provider but `axiosInstance` reads from `sessionStorage`. `sessionStorage` never receives the `"token"` key, so `token` is always `null` and every API call goes out with no `Authorization` header. @@ -31,6 +35,10 @@ sessionStorage.getItem("token") // ← reads the wrong store, alway ### BUG-02 — Dashboard pages don't exist; login redirect always hits a 404 **File:** `app/(dashboard)/admin/page.tsx` + missing dashboard routes + +**Status:** Fixed on 2026-06-05. + +**Verified:** The dashboard, courses, students, applications, and settings pages now exist under `app/(admin)/admin/(workspace)/`. `authenticate()` is called with no `redirectPath`, so the auth provider defaults to `router.push('/admin/dashboard')`. The page at that path (`app/admin/dashboard/page.tsx` or equivalent) does not exist in the repository. After a successful login the user lands on a blank 404 page every time. @@ -51,6 +59,10 @@ Pages missing that `constants.tsx` and the auth provider already reference: ### BUG-03 — No auth guard on workspace routes; dashboard is publicly accessible once built **File:** `app/(dashboard)/admin/layout.tsx` + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** `AuthGuard` exists and `app/(admin)/admin/(workspace)/layout.tsx` wraps workspace routes with `WithAuth redirectTo="/admin"`. There is no `AuthGuard` component, no middleware route check, and no HOC protecting authenticated routes. Once the dashboard pages exist (BUG-02), any unauthenticated user who navigates directly to `/admin/dashboard` will see them without logging in. `AuthProvider` is present but nothing consumes `currentUser` to enforce a redirect. @@ -82,6 +94,10 @@ Wrap workspace layout with it (see BUG-05 for the layout refactor). ### BUG-04 — No redirect-if-authenticated on login page; logged-in users see the login form **File:** `app/(dashboard)/admin/page.tsx` + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** The admin login page now redirects an authenticated `currentUser` to `/admin/dashboard` and does not render the login form while that redirect is active. When a user is already authenticated and visits `/admin`, the login form renders normally. There is no check for an existing session that would send them to `/admin/dashboard` instead. Combined with BUG-02, this also means a user who refreshes after a successful login lands back on the login page. @@ -109,6 +125,10 @@ export function useRedirectIfAuthenticated() { ### BUG-05 — `(dashboard)` route group wraps one folder and provides no benefit **Files:** `app/(dashboard)/` directory + +**Status:** Fixed on 2026-06-05. + +**Verified:** The old `(dashboard)` route group is no longer present. Admin workspace routes now live under `app/(admin)/admin/(workspace)/`. The `(dashboard)` route group contains only `admin/` and has no `layout.tsx` at the group level. Route groups exist to share layouts across sibling URL segments or to organise code; here it does neither. The name is also misleading — it sounds like it scopes the dashboard pages, but the actual dashboard doesn't exist yet. This creates the confusing path `app/(dashboard)/admin/` where both the group and its child need to be navigated to find any file. @@ -135,6 +155,10 @@ app/ ### BUG-06 — Login page loads all five data providers unnecessarily **File:** `app/(dashboard)/admin/layout.tsx` + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** `app/(admin)/admin/layout.tsx` now wraps admin routes with `AuthProvider` only. Student, course, application, and contact providers are scoped to `app/(admin)/admin/(workspace)/layout.tsx`. `AdminRootLayout` wraps every page — including the login form — with `AuthProvider`, `StudentProvider`, `CourseProvider`, `ApplicationProvider`, and `ContactProvider`. The login page needs only `AuthProvider`. The four data providers each initialise their own state and may trigger API calls or setup work on a page that has no use for them. @@ -145,6 +169,10 @@ app/ ### BUG-07 — Middleware rewrites cross-host violations to `/404`, a path with no page **File:** `src/middleware.ts` line ~37 + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** Added `app/404/page.tsx`, which renders the same component as `app/not-found.tsx`, so the existing middleware rewrite target now resolves to the custom not-found UI. ```ts url.pathname = "/404"; @@ -173,6 +201,10 @@ Or add an actual `app/404/page.tsx` that renders the same `NotFoundPage` compone ### BUG-08 — Auth cookie is set on login but never read anywhere **File:** `src/providers/auth-provider/index.tsx` + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** Removed the unused `document.cookie` token write from the authentication success path. ```ts document.cookie = `token=${token}; path=/; secure; samesite=strict`; @@ -187,6 +219,10 @@ This cookie is set on every successful login, but nothing in the codebase reads ### BUG-09 — `logout()` uses an implicit root redirect that depends on middleware behaviour **File:** `src/providers/auth-provider/index.tsx` + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** `logout()` now redirects explicitly to `/admin` after clearing auth state. ```ts router.push('/'); @@ -207,6 +243,10 @@ On `admin.moiponeacademy.org`, pushing `/` works only because the middleware rew ### BUG-10 — `sessionStorage` cleared on tab close causes stale UI state on re-open **File:** `src/providers/auth-provider/index.tsx` + +**Status:** Fixed on 2026-06-05. + +**Fix implemented:** Auth identity is now derived from the decoded JWT. The auth provider no longer reads or writes `role`, `Id`, or `userName` from `sessionStorage`; remaining `sessionStorage.clear()` calls are cleanup only. `role`, `Id`, and `userName` are written to `sessionStorage`, which is cleared when the tab closes. On re-open, the `localStorage` token is still valid and the `useEffect` recovery path decodes the JWT to rebuild `currentUser`. However, any code that reads `sessionStorage.getItem("role")` or `sessionStorage.getItem("Id")` directly (outside the React state) will return `null` after a tab re-open even though the user is authenticated. This creates a window between mount and the `useEffect` running where these values are inconsistently unavailable. @@ -217,22 +257,33 @@ On `admin.moiponeacademy.org`, pushing `/` works only because the middleware rew ### BUG-11 — No axios interceptor for 401; expired tokens never trigger logout **File:** `src/lib/utils/axiosInstance.ts` + +**Status:** Fixed on 2026-06-05. There is no response interceptor on the axios instance. When the JWT expires, API calls return `401 Unauthorized` silently. The UI stays in an "authenticated" state (token still in `localStorage`, `currentUser` still in context) while every data request fails. The user is never logged out and sees no meaningful error. -**Fix:** Add a response interceptor: - +**Fix implemented:** Added a response interceptor that clears the stored token and session state on `401`. Redirects resolve from the current URL subdomain: + +- `admin.*` redirects to `/admin`. +- `student.*` redirects to `/student`. +- Other hosts clear stale auth state without forcing a login redirect. + ```ts instance.interceptors.response.use( - (res) => res, - (err) => { - if (err.response?.status === 401) { - localStorage.removeItem('token'); + (response) => response, + (error) => { + if (error.response?.status === 401) { + const redirectPath = getUnauthorizedRedirectPath(); + + localStorage.removeItem("token"); sessionStorage.clear(); - window.location.replace('/admin'); + + if (redirectPath) { + window.location.replace(redirectPath); + } } - return Promise.reject(err); - } + return Promise.reject(error); + }, ); ``` @@ -241,16 +292,18 @@ instance.interceptors.response.use( ### BUG-12 — `axiosInstance` captures token at call time, not at request time **File:** `src/lib/utils/axiosInstance.ts` + +**Status:** Fixed on 2026-06-05. `axiosInstance()` reads the token once when the function is called and bakes it into the `axios.create()` defaults. If a provider calls `axiosInstance()` during initialisation (before the auth `useEffect` runs and sets the token), all requests from that instance will have no auth header even after the user is authenticated in state. -**Fix:** Read the token inside a request interceptor instead, so it is resolved freshly on every request: - +**Fix implemented:** Moved token lookup into a request interceptor so every admin and student API call resolves the latest `localStorage` token immediately before the request is sent. If the token is absent, the request is sent without adding an `Authorization` header. + ```ts const instance = axios.create({ baseURL }); instance.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); + const token = localStorage.getItem("token"); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); @@ -258,4 +311,4 @@ instance.interceptors.request.use((config) => { --- -*12 bugs total — 2 critical, 2 high, 5 medium, 3 low.* \ No newline at end of file +*12 bugs total — 2 critical, 2 high, 5 medium, 3 low.* diff --git a/next-ts/app/(admin)/admin/(workspace)/layout.tsx b/next-ts/app/(admin)/admin/(workspace)/layout.tsx index 5b18341..3fd80d9 100644 --- a/next-ts/app/(admin)/admin/(workspace)/layout.tsx +++ b/next-ts/app/(admin)/admin/(workspace)/layout.tsx @@ -1,5 +1,9 @@ import WithAuth from '@/src/components/AuthGuard'; import AdminLayout from '@/src/components/Admin/AdminLayout'; +import { ApplicationProvider } from '@/src/providers/application-provider'; +import { ContactProvider } from '@/src/providers/contact-provider'; +import { CourseProvider } from '@/src/providers/course-provider'; +import { StudentProvider } from '@/src/providers/student-provider'; export default function AdminProtectedLayout({ children, @@ -7,10 +11,16 @@ export default function AdminProtectedLayout({ children: React.ReactNode; }>) { return ( - - - {children} - - + + + + + + {children} + + + + + ); } diff --git a/next-ts/app/(admin)/admin/layout.tsx b/next-ts/app/(admin)/admin/layout.tsx index 3736ab3..168fa29 100644 --- a/next-ts/app/(admin)/admin/layout.tsx +++ b/next-ts/app/(admin)/admin/layout.tsx @@ -1,28 +1,9 @@ -import { StudentProvider } from "@/src/providers/student-provider"; -import { CourseProvider } from "@/src/providers/course-provider"; -import { ApplicationProvider } from "@/src/providers/application-provider"; -import { ContactProvider } from "@/src/providers/contact-provider"; import { AuthProvider } from "@/src/providers/auth-provider"; -import AuthGuard from "@/src/components/AuthGuard"; export default function AdminRootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - return ( - - - - - - - {children} - - - - - - - ); + return {children}; } diff --git a/next-ts/app/(admin)/admin/page.tsx b/next-ts/app/(admin)/admin/page.tsx index c313653..b166f4c 100644 --- a/next-ts/app/(admin)/admin/page.tsx +++ b/next-ts/app/(admin)/admin/page.tsx @@ -1,4 +1,5 @@ 'use client' +import { useEffect } from 'react'; import { Form, Input, Button, Checkbox, Image } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { useAuthPageStyles } from './style'; @@ -11,7 +12,13 @@ const AdminLogin : React.FC = () => { const router = useRouter(); const [form] = Form.useForm(); const authActions = useAuthActions(); - const { isPending, isError } = useAuthState(); + const { currentUser, isPending, isError } = useAuthState(); + + useEffect(() => { + if (currentUser) { + router.replace('/admin/dashboard'); + } + }, [currentUser, router]); const handleSubmit = async (values: ILogin) => { const { userNameOrEmailAddress, password } = values; @@ -21,6 +28,8 @@ const AdminLogin : React.FC = () => { } }; + if (currentUser) return null; + return (
diff --git a/next-ts/src/providers/auth-provider/index.tsx b/next-ts/src/providers/auth-provider/index.tsx index 55af936..1240b03 100644 --- a/next-ts/src/providers/auth-provider/index.tsx +++ b/next-ts/src/providers/auth-provider/index.tsx @@ -33,7 +33,7 @@ export const AuthProvider = ({children}: {children: React.ReactNode}) => { const authenticatedUser: ICurrentUser = { userRole: decoded[AbpTokenProperies.role], userId: decoded[AbpTokenProperies.nameidentifier], - userName:sessionStorage.getItem("userName") ?? decoded[AbpTokenProperies.name], + userName: decoded[AbpTokenProperies.name], } dispatch( @@ -63,12 +63,7 @@ const authenticate = async (user: ILogin, redirectPath?: string) => { const userId = decoded[AbpTokenProperies.nameidentifier]; const userName = decoded[AbpTokenProperies.name]; - document.cookie = `token=${token}; path=/; secure; samesite=strict`; - localStorage.setItem("token", token); - sessionStorage.setItem("role", userRole); - sessionStorage.setItem("Id", userId); - sessionStorage.setItem("userName", userName); dispatch(authenticateSuccess({ userRole, userId, userName })); router.push(redirectPath || '/admin/dashboard'); @@ -87,7 +82,7 @@ const authenticate = async (user: ILogin, redirectPath?: string) => { localStorage.removeItem("token"); sessionStorage.clear(); dispatch(logoutSuccess()); - router.push('/') + router.push('/admin') } catch { dispatch(logoutError());