diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index c967418daeb..f3916031aa3 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -183,6 +183,11 @@ export const APP_ROUTES: Route[] = [ .then((m) => m.ROUTES), canActivate: [notAuthenticatedGuard], }, + { + path: 'wayf', + loadChildren: () => import('./clarin-wayf/clarin-wayf-routes') + .then((m) => m.ROUTES), + }, { path: 'logout', loadChildren: () => import('./logout-page/logout-page-routes') diff --git a/src/app/clarin-wayf/AGENTS.md b/src/app/clarin-wayf/AGENTS.md new file mode 100644 index 00000000000..f1cb0c52197 --- /dev/null +++ b/src/app/clarin-wayf/AGENTS.md @@ -0,0 +1,139 @@ +# CLARIN WAYF — Agent Context + +This document captures all context needed to continue work on this feature. + +--- + +## What This Is + +A **CLARIN WAYF (Where Are You From)** Identity Provider (IdP) picker, implemented as a standalone Angular component inside DSpace Angular 9.2. + +It replaces the legacy external DiscoJuice/jQuery solution. Instead of redirecting to a separately deployed discovery service, the IdP selection UI is now embedded directly inside the DSpace frontend — on the `/login` page and in the header dropdown. + +The eventual goal is to extract this into a standalone Angular Elements Web Component (``), but for now it lives here for development and design iteration. + +--- + +## Component Location + +``` +src/app/clarin-wayf/ +├── AGENTS.md ← this file +├── clarin-wayf.component.ts ← main orchestrator component +├── clarin-wayf-routes.ts ← standalone route at /wayf +├── models/ +│ ├── idp-entry.model.ts ← IdpEntry, LocalizedValue, IdpLogo interfaces +│ └── wayf-config.model.ts ← WayfConfig, SamldsParams types +├── services/ +│ ├── search.service.ts ← fuzzy search engine (Sørensen–Dice) +│ ├── search.service.spec.ts ← 33 unit tests (all passing) +│ ├── feed.service.ts ← HTTP fetch + cache of IdP JSON feed +│ ├── persistence.service.ts ← localStorage (last IdP, up to 5 recent) +│ └── i18n.service.ts ← signal-based translation (en/cs/de) +└── components/ + ├── idp-card/ + │ └── wayf-idp-card.component.ts ← single IdP card (logo, name, tag badge) + ├── search-bar/ + │ └── wayf-search-bar.component.ts ← search input with ARIA combobox + ├── idp-list/ + │ └── wayf-idp-list.component.ts ← virtualized/filtered list of IdP cards + └── recent-idps/ + └── wayf-recent-idps.component.ts ← strip of recently used IdPs +``` + +--- + +## Integration Points (Files Modified Outside This Folder) + +### 1. Standalone Route +- **`src/app/app-routes.ts`** — added lazy route `/wayf` → `clarin-wayf-routes.ts` + +### 2. Login Page (`/login`) +- **`src/app/login-page/login-page.component.ts`** — added `wayfOpen` signal, `toggleWayf()`, `onIdpSelected()`, `HardRedirectService` injection +- **`src/app/login-page/login-page.component.html`** — divider + toggle button + collapsible `` panel below password form +- **`src/themes/custom/app/login-page/login-page.component.ts`** — added `ClarinWayfComponent` to `imports` (themed wrapper) + +### 3. Header Dropdown Login +- **`src/app/shared/auth-nav-menu/auth-nav-menu.component.ts`** — added `activeLoginTab` signal, `ClarinWayfComponent`, `HardRedirectService`, tab switching logic +- **`src/app/shared/auth-nav-menu/auth-nav-menu.component.html`** — replaced single `` with two-tab layout: "Local Login" + "Institution" +- **`src/app/shared/auth-nav-menu/auth-nav-menu.component.scss`** — widened dropdown to 400px, added `.wayf-login-tabs` styling +- **`src/themes/custom/app/shared/auth-nav-menu/auth-nav-menu.component.ts`** — added `ClarinWayfComponent` to `imports` + +### 4. Shibboleth Auth Method (for backends with Shibboleth configured) +- **`src/app/shared/log-in/methods/shibboleth-wayf/log-in-shibboleth-wayf.component.ts`** — new component; replaces hard-redirect button with inline WAYF +- **`src/app/shared/log-in/methods/log-in.methods-decorator.ts`** — `AuthMethodType.Shibboleth` now maps to `LogInShibbolethWayfComponent` +- **`src/app/shared/log-in/methods/auth-methods.type.ts`** — added `typeof LogInShibbolethWayfComponent` to union type + +### 5. i18n +- **`src/assets/i18n/en.json5`** — added keys: + - `wayf.title`, `wayf.breadcrumbs` + - `login.wayf.button`, `login.wayf.header`, `login.wayf.close` + - `nav.login.tab.local`, `nav.login.tab.institution` + +### 6. Mock Feed +- **`src/assets/mock/wayf-feed.json`** — 10 sample IdPs (MUNI, CESNET, Charles University, CVUT, LMU, KU Leuven, Perun, Café Brazil, UW, Example University) + +--- + +## Key Design Decisions + +### SAMLDS Protocol +On IdP selection, the component builds a Shibboleth SP redirect URL: +``` +/Shibboleth.sso/Login?entityID=&target= +``` +`onIdpSelected()` in both `login-page.component.ts` and `auth-nav-menu.component.ts` calls `hardRedirectService.redirect()` with this URL. + +### Feed Loading (`clarin-wayf.component.ts`) +Feed URL resolved in this priority order: +1. `feedUrl` input binding (parent passes it) +2. `?feedUrl=` query parameter (for standalone `/wayf` route) +3. Falls back to `assets/mock/wayf-feed.json` for local development + +### Fuzzy Search (`search.service.ts`) +- Diacritics normalized via `NFD` + strip combining marks +- Sørensen–Dice bigram similarity coefficient (no external deps) +- Scoring: exact match = 2, word boundary = 1 + ratio, fuzzy ≥ 0.4 threshold +- Language-aware: preferred `lang` gets small ranking bonus + +### Persistence (`persistence.service.ts`) +- `clarin-wayf-last-idp` key — entityID of last selected IdP +- `clarin-wayf-recent-idps` key — JSON array, max 5 entries, newest first + +### Angular Patterns Used +- **Standalone components** throughout (no NgModules) +- **`inject()`** exclusively (no constructor injection) +- **Signals** for all reactive state (`signal()`, `computed()`) +- **`input()`/`output()`** for component I/O (Angular 17+ API) +- **`@if`/`@for`** control flow (Angular 17+ template syntax) +- **OnPush** change detection + +--- + +## Running Tests + +```bash +npm test -- --include='src/app/clarin-wayf/**/*.spec.ts' +``` + +All 33 tests in `search.service.spec.ts` should pass. + +--- + +## TODO / Next Steps + +- [ ] **Production feed URL**: Replace `assets/mock/wayf-feed.json` default with the actual CLARIN feed (e.g. `https://ds.aai.cesnet.cz/feeds/CLARIN_SP_Feed.json`) +- [ ] **Shibboleth SP path**: Verify `/Shibboleth.sso/Login` matches the actual SP endpoint in the target deployment; make it configurable via `environment.ts` +- [ ] **Proxy/Hub IdPs**: Wire `proxyEntities` input with actual CLARIN hub entityIDs so they pin to the top of the list with a badge +- [ ] **Visual polish**: The component currently uses minimal Bootstrap 5 CSS variables; full UX design pass needed +- [ ] **Component tests**: Only `search.service.spec.ts` exists; add specs for `feed.service.ts`, `persistence.service.ts`, and `clarin-wayf.component.ts` +- [ ] **Angular Elements extraction**: Once stable, extract into a separate library and package as `` custom element (single `