Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
139 changes: 139 additions & 0 deletions src/app/clarin-wayf/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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 (`<clarin-wayf>`), 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 `<ds-clarin-wayf>` 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 `<ds-log-in>` 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=<selectedEntityID>&target=<returnUrl>
```
`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 `<clarin-wayf>` custom element (single `<script>` tag, Shadow DOM)

---

## DSpace-Specific Gotchas

- **Themed components**: DSpace uses a `src/themes/custom/` shadow that re-exports base components with their own `imports` array. Whenever you add a new component to a base component's template, you **must also add it to the themed wrapper's `imports`**. Forgetting this causes `Unknown element 'ds-...'` errors only in the themed variant.
- **TypeScript config**: `noImplicitAny: false` and `strictNullChecks: false` — code is permissive but `fullTemplateTypeCheck: true` means template errors are strict.
- **i18n**: After adding keys to `en.json5`, restart the dev server — the asset hash changes and the old bundle won't pick up new keys.
- **Auth method decorator map**: `AUTH_METHOD_FOR_DECORATOR_MAP` in `log-in.methods-decorator.ts` is the single source of truth for which component renders for each `AuthMethodType`. Update both the map and the `AuthMethodTypeComponent` union type together.
14 changes: 14 additions & 0 deletions src/app/clarin-wayf/clarin-wayf-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Route } from '@angular/router';

import { ClarinWayfComponent } from './clarin-wayf.component';
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';

export const ROUTES: Route[] = [
{
path: '',
pathMatch: 'full',
component: ClarinWayfComponent,
resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { breadcrumbKey: 'wayf', title: 'wayf.title' },
},
];
Loading
Loading