Commit 169b79ae authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: enhance authentication flow with loading state handling and error display

parent 05fea1a8
Loading
Loading
Loading
Loading
+17 −1
Changes for README.md: 17 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -10,12 +10,28 @@ Developer portal UI and deployment for the OOP platform.

## Run locally

The backend needs PostgreSQL. Start one matching `POSTGRESQL_SETTINGS__URL` in
`.env.example`:

```bash
docker run -d --name portal-db -p 5432:5432 \
  -e POSTGRES_USER=portal -e POSTGRES_PASSWORD=portal -e POSTGRES_DB=portal \
  postgres:16
```

Then:

```bash
cp .env.example .env   # then fill in the Keycloak client
make dev               # backend on :8000, GUI on :3000
```

`make run` and `make gui-run` start either one alone.
`make run` and `make gui-run` start either one alone. To work without Keycloak, set
`AUTH__SECURITY_MODE="none"` in `.env`: there is no login page, and every request acts
as a dev user with every role.

If the GUI says it can't reach the portal, the backend is not running: look for its
startup error in the `make dev` output.

Open [http://localhost:3000](http://localhost:3000). The GUI dev server proxies `/api`
to the backend, so both share one address and the login cookies work. That is why
+13 −0
Changes for portal-gui/eslint.config.js: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -18,6 +18,19 @@ export default defineConfig([
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      // Type information, which the promise rules below need.
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      '@typescript-eslint/no-floating-promises': 'error',
      // Async handlers in JSX (onClick, onSubmit) are normal React.
      '@typescript-eslint/no-misused-promises': [
        'error',
        { checksVoidReturn: { attributes: false } },
      ],
    },
  },
]);
+22 −0
Changes for portal-gui/src/app/guards/auth-pending.tsx: 22 added lines, 0 removed lines.
Original line number Diff line number Diff line
import { ErrorState } from '@/components/common/error-state';
import { LoadingState } from '@/components/common/loading-state';
import type { AuthStatus } from '@/types/authentication';

// Shown while we cannot tell yet whether anyone is logged in.
export function AuthPending({
  status,
}: {
  status: Extract<AuthStatus, 'loading' | 'unavailable'>;
}) {
  if (status === 'loading') {
    return <LoadingState message="Checking your session…" />;
  }

  return (
    <ErrorState
      title="Can't reach the portal"
      description="The portal backend or the login service is not responding."
      onRetry={() => window.location.reload()}
    />
  );
}
+5 −1
Changes for portal-gui/src/app/guards/protected-route.tsx: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '@/features/auth/auth-context';
import { ROUTES } from '../config/route';
import { AuthPending } from './auth-pending';

export function ProtectedRoute() {
  const { isAuthenticated } = useAuth();
  const { status, isAuthenticated } = useAuth();
  if (status === 'loading' || status === 'unavailable') {
    return <AuthPending status={status} />;
  }
  if (!isAuthenticated) {
    return <Navigate to={ROUTES.LOGIN} replace />;
  }
+5 −1
Changes for portal-gui/src/app/guards/public-route.tsx: 5 added lines, 1 removed line.
Original line number Diff line number Diff line
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '@/features/auth/auth-context';
import { ROUTES } from '../config/route';
import { AuthPending } from './auth-pending';

export function PublicRoute() {
  const { isAuthenticated } = useAuth();
  const { status, isAuthenticated } = useAuth();
  if (status === 'loading' || status === 'unavailable') {
    return <AuthPending status={status} />;
  }
  if (isAuthenticated) {
    return <Navigate to={ROUTES.HOME} replace />;
  }
Loading