Web and mobile clients

Platform

Web and mobile clients

Two Vite multi-page apps and one Flutter app. All three call the same REST API with the same token. Section changes on the web are real page loads; React Router only handles sub-views inside a section.

Web routing

Each top-level section (courses, assessments, resources, profile) is its own HTML entry and its own bundle. Links between sections are plain anchors, so the browser reloads. Inside a section React Router switches views such as list and detail without reloading.

Web routing

refresh on /courses/123

User clicks a sidebar link
plain anchor

Full document reload

Nginx location block
try_files per section

/courses/index.html

pages/courses/main.tsx

Sidebar
fetchNav from nav_items and nav_item_roles

CoursesApp
BrowserRouter basename /courses

course list

course detail :id

localStorage
sidebar:openSection

Refreshing on a sub-path such as /courses/123 has to land back on the section entry, which is what the per-section try_files rule does.
apps/portal/vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  build: { rollupOptions: { input: {
    main:        resolve(__dirname, 'index.html'),
    courses:     resolve(__dirname, 'courses/index.html'),
    assessments: resolve(__dirname, 'assessments/index.html'),
    resources:   resolve(__dirname, 'resources/index.html'),
    profile:     resolve(__dirname, 'profile/index.html'),
  }}},
});
infra/nginx/portal.conf
location /courses/     { try_files $uri $uri/ /courses/index.html; }
location /assessments/ { try_files $uri $uri/ /assessments/index.html; }
location /resources/   { try_files $uri $uri/ /resources/index.html; }
location /profile/     { try_files $uri $uri/ /profile/index.html; }
location /             { try_files $uri $uri/ /index.html; }
Function or fileDoes
pages/courses/main.tsxEntry for the courses section. Renders Sidebar and CoursesApp into the root.
CoursesAppBrowserRouter with basename /courses and the nested routes for list and detail.
fetchNav()GET nav_items with their roles on every page load, so admin edits never show stale.
NavRendererRenders anchors, not Link, for top-level items. Persists the open group in localStorage under sidebar:openSection.
api.tsfetch wrapper that adds the token and redirects to login on 401.
RoleGate role="trainer"Renders a branch of a page for one role. Convenience only; the API is the real check.
Chrome is not a guard

A logged-out visitor may see empty page chrome before the API answers 401. That leaks nothing. Do not mistake it for access control.

Section-scoped basename

Each section sets its own basename so sub-view URLs stay real and shareable without colliding.

One origin

A single API origin is enough. If the admin console moves to its own subdomain, a cross-subdomain cookie carries the session.

Screens by role

RoleWebMobile
traineeprofile, courses, resources, assessments, feedback, certificate downloadsame screens
trainerprofile, courses, resource upload, questionnaire builder and review, trainee monitoringprofile, questionnaire builder and review, trainee monitoring, resource upload
adminadmin console: approvals, courses, assessments overview, competency, dropdowns, noticesnot planned

Mobile routing

Flutter uses go_router. Its redirect callback plays the same role as authorizeRoute on the server: it decides whether a screen may be built. The API check stays authoritative.

Flutter startup and redirect

noyesyesfailsoknonoyes

App start

read tokens
flutter_secure_storage

token present?

go /login

GET /api/me via dio

401?

refresh token interceptor

appRedirect(context, state)

approved and role allowed for this route?

go /pending or /login

build screen

apps/mobile/lib/router.dart
final router = GoRouter(
  refreshListenable: authState,                    // re-run redirect when auth changes
  redirect: (context, state) => appRedirect(authState, state.matchedLocation),
  routes: [ /* login, pending, courses, assessments, ... */ ],
);

String? appRedirect(AuthState a, String loc) {
  if (!a.signedIn)  return loc == '/login'   ? null : '/login';
  if (!a.approved)  return loc == '/pending' ? null : '/pending';
  if (!a.roleAllows(loc)) return '/courses';       // role table mirrors nav_item_roles
  return null;
}
PieceDoes
dioREST client. An interceptor adds the token and, on 401, calls refresh once and replays the request.
flutter_secure_storageKeeps the access and refresh tokens.
AuthRepository.refresh()Exchanges the refresh token. Failure signs the user out and routes to /login.
appRedirect(auth, location)The redirect rule above.
Capacity Connect · Team Syntax Squad · SIH 2026 · PS 26075Code samples are implementation sketches.