Skip to content

Commit aaafa64

Browse files
committed
ajout header authentification et correction filtre
1 parent 5a1d14a commit aaafa64

10 files changed

Lines changed: 322 additions & 30 deletions

File tree

apps/datahub/src/app/app.component.html

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -98,30 +98,15 @@
9898
</dsfr-tool-link-menu>
9999
</li>
100100
<li>
101-
<dsfr-tool-link-menu
102-
[outline]="false"
103-
[labelMenu]="'Mon espace'"
104-
[iconMenu]="'fr-icon-account-circle-fill'"
105-
>
106-
<ng-template #linksTemplate>
107-
<ul>
108-
<dsfr-link
109-
[link]="'https://cartes.gouv.fr/tableau-de-bord'"
110-
[label]="'Tableau de bord'"
111-
[icon]="'fr-icon-dashboard-3-line'"
112-
[customClass]="'fr-nav__link'"
113-
[iconPosition]="'left'"
114-
></dsfr-link>
115-
<dsfr-link
116-
[link]="'https://cartes.gouv.fr/mon-compte'"
117-
[label]="'Mon compte'"
118-
[icon]="'fr-icon-user-line'"
119-
[customClass]="'fr-nav__link'"
120-
[iconPosition]="'left'"
121-
></dsfr-link>
122-
</ul>
123-
</ng-template>
124-
</dsfr-tool-link-menu>
101+
<div id="header-auth">
102+
<button
103+
class="login-btn fr-icon-account-circle-fill fr-btn"
104+
type="button"
105+
title="Se connecter"
106+
>
107+
Se connecter
108+
</button>
109+
</div>
125110
</li>
126111
</ng-template>
127112
</dsfr-header>

apps/datahub/src/app/app.component.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component, OnInit, AfterViewInit, Renderer2 } from '@angular/core'
22
import { getThemeConfig } from '@geonetwork-ui/util/app-config'
33
import { ThemeService } from '@geonetwork-ui/util/shared'
4+
import Keycloak from 'keycloak-js'
45

56
@Component({
67
selector: 'datahub-root',
@@ -9,7 +10,7 @@ import { ThemeService } from '@geonetwork-ui/util/shared'
910
standalone: false,
1011
})
1112
export class AppComponent implements OnInit, AfterViewInit {
12-
constructor(public renderer: Renderer2) { }
13+
constructor(public renderer: Renderer2) {}
1314

1415
ngOnInit(): void {
1516
const favicon = getThemeConfig().FAVICON
@@ -27,5 +28,116 @@ export class AppComponent implements OnInit, AfterViewInit {
2728
this.renderer.removeClass(title, 'fr-badge--green-emeraude')
2829
this.renderer.insertBefore(title, spanBadge, title.firstChild)
2930
this.renderer.addClass(title, 'fr-badge--blue-cumulus')
31+
this.keycloakCheckAuth()
32+
}
33+
34+
keycloakCheckAuth(): void {
35+
const authContainer = document.getElementById('header-auth')
36+
if (!authContainer) return
37+
38+
const renderLoggedOut = () => {
39+
document.querySelectorAll('.login-btn').forEach((btn) => {
40+
btn.addEventListener('click', () => {
41+
keycloak.login({ redirectUri: window.location.href })
42+
})
43+
})
44+
}
45+
46+
const renderLoggedIn = async () => {
47+
const claims = keycloak.idTokenParsed || keycloak.tokenParsed || {}
48+
49+
const displayName =
50+
(typeof claims.name === 'string' && claims.name) ||
51+
[claims.given_name, claims.family_name].filter(Boolean).join(' ') ||
52+
(typeof claims.preferred_username === 'string' &&
53+
claims.preferred_username) ||
54+
(typeof claims.email === 'string' && claims.email) ||
55+
'Compte'
56+
57+
const generateUserMenuHTML = (collapseId) => {
58+
const currentUrl = encodeURIComponent(window.location.href)
59+
return `
60+
<li>
61+
<div class="fr-translate fr-nav">
62+
<div class="fr-nav__item">
63+
<button aria-controls="${collapseId}" aria-expanded="false" title="Mon espace" class="fr-nav__btn fr-btn fr-px-2w">
64+
<span class="fr-icon-account-circle-fill fr-icon--sm fr-mr-1w" aria-hidden="true"></span>Mon espace</button>
65+
<div class="fr-collapse fr-translate__menu fr-menu" id="${collapseId}">
66+
<ul class="fr-menu__list">
67+
<li style="pointer-events: none;">
68+
<div class="fr-text--sm">
69+
<p class="custom-center-btn fr-text--bold fr-mx-2w fr-text--sm fr-mt-3v fr-mb-2v">${displayName}</p>
70+
<p class="fr-text--xs fr-mb-3v fr-mx-2w fr-text-mention--grey" style="text-align: left;">${claims.email}</p>
71+
</div>
72+
</li>
73+
<li>
74+
<a class="fr-nav__link fr-mr-3w" href="https://cartes.gouv.fr/tableau-de-bord">
75+
<span class="fr-icon-dashboard-3-line fr-icon--sm">&emsp;Tableau de bord</span></a>
76+
</li>
77+
<li>
78+
<a class="fr-nav__link fr-mr-3w" href="https://cartes.gouv.fr/mon-compte">
79+
<span class="fr-icon-user-line fr-icon--sm">&emsp;Mon compte</span></a>
80+
</li>
81+
<li>
82+
<div>
83+
<a href="https://sso.geopf.fr/realms/geoplateforme/protocol/openid-connect/logout?post_logout_redirect_uri=${currentUrl}&client_id=cartes-gouv-public"
84+
class="fr-icon-logout-box-r-line fr-icon--sm custom-center-btn fr-btn fr-btn--tertiary fr-btn--sm fr-mt-3v fr-mx-2w">
85+
Se déconnecter
86+
</a>
87+
</div>
88+
</li>
89+
</ul>
90+
</div>
91+
</div>
92+
</div>
93+
</li>
94+
`
95+
}
96+
97+
authContainer.innerHTML = generateUserMenuHTML('espace-collapse')
98+
99+
const authContainerMobile = document.getElementById('header-auth-mobile')
100+
if (authContainerMobile) {
101+
authContainerMobile.innerHTML = generateUserMenuHTML(
102+
'espace-collapse-mobile'
103+
)
104+
}
105+
}
106+
107+
const keycloak = new Keycloak({
108+
url: 'https://sso.geopf.fr',
109+
realm: 'geoplateforme',
110+
clientId: 'cartes-gouv-public',
111+
})
112+
113+
// "Authorization Code" flow avec PKCE (type de client Keycloak : Public).
114+
keycloak
115+
.init({
116+
onLoad: 'check-sso',
117+
flow: 'standard',
118+
pkceMethod: 'S256',
119+
checkLoginIframe: false,
120+
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
121+
})
122+
.then(async (authenticated) => {
123+
if (!authenticated) {
124+
renderLoggedOut()
125+
return
126+
}
127+
128+
await renderLoggedIn()
129+
130+
// Si on veut garder le token à jour pour d'éventuelles futures appels API :
131+
// window.setInterval(() => {
132+
// keycloak.updateToken(60).catch(() => {
133+
// // Si le rafraîchissement échoue, on affiche simplement l'interface déconnectée.
134+
// renderLoggedOut();
135+
// });
136+
// }, 30_000);
137+
})
138+
.catch((error) => {
139+
console.error('Failed to initialize Keycloak', error)
140+
renderLoggedOut()
141+
})
30142
}
31143
}

apps/datahub/src/app/home/search/search-filters/search-filters.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class SearchFiltersComponent implements OnInit {
106106
}
107107

108108
ngOnInit(): void {
109+
this.isQualitySortable = false
109110
if (this.platformService.supportsAuthentication()) {
110111
this.platformService.getMe().subscribe((user) => (this.userId = user?.id))
111112
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import Keycloak from './keycloak.js'
2+
3+
;(() => {
4+
const authContainer = document.getElementById('header-auth')
5+
if (!authContainer) return
6+
7+
const renderLoggedOut = () => {
8+
document.querySelectorAll('.login-btn').forEach((btn) => {
9+
btn.addEventListener('click', () => {
10+
keycloak.login({ redirectUri: window.location.href })
11+
console.log(window.location.href)
12+
})
13+
})
14+
}
15+
16+
const renderLoggedIn = async () => {
17+
const claims = keycloak.idTokenParsed || keycloak.tokenParsed || {}
18+
19+
const displayName =
20+
(typeof claims.name === 'string' && claims.name) ||
21+
[claims.given_name, claims.family_name].filter(Boolean).join(' ') ||
22+
(typeof claims.preferred_username === 'string' &&
23+
claims.preferred_username) ||
24+
(typeof claims.email === 'string' && claims.email) ||
25+
'Compte'
26+
27+
const generateUserMenuHTML = (collapseId) => {
28+
const currentUrl = encodeURIComponent(window.location.href)
29+
return `
30+
<li>
31+
<div class="fr-translate fr-nav">
32+
<div class="fr-nav__item">
33+
<button aria-controls="${collapseId}" aria-expanded="false" title="Mon espace" class="fr-translate__btn fr-btn fr-px-2w">
34+
<span class="fr-icon-account-circle-fill fr-icon--sm fr-mr-1w" aria-hidden="true"></span>Mon espace</button>
35+
<div class="fr-collapse fr-translate__menu fr-menu" id="${collapseId}">
36+
<ul class="fr-menu__list">
37+
<li style="pointer-events: none;">
38+
<div class="fr-text--sm">
39+
<p class="custom-center-btn fr-text--bold fr-mx-2w fr-text--sm fr-mt-3v fr-mb-2v">${displayName}</p>
40+
<p class="fr-text--xs fr-mb-3v fr-mx-2w fr-text-mention--grey" style="text-align: left;">${claims.email}</p>
41+
</div>
42+
</li>
43+
<li>
44+
<a class="fr-nav__link fr-mr-3w" href="https://cartes.gouv.fr/tableau-de-bord">
45+
<span class="fr-icon-dashboard-3-line fr-icon--sm">&emsp;Tableau de bord</span></a>
46+
</li>
47+
<li>
48+
<a class="fr-nav__link fr-mr-3w" href="https://cartes.gouv.fr/mon-compte">
49+
<span class="fr-icon-user-line fr-icon--sm">&emsp;Mon compte</span></a>
50+
</li>
51+
<li>
52+
<div>
53+
<a href="https://sso.geopf.fr/realms/geoplateforme/protocol/openid-connect/logout?post_logout_redirect_uri=${currentUrl}&client_id=cartes-gouv-public"
54+
class="fr-icon-logout-box-r-line fr-icon--sm custom-center-btn fr-btn fr-btn--tertiary fr-btn--sm fr-mt-3v fr-mx-2w">
55+
Se déconnecter
56+
</a>
57+
</div>
58+
</li>
59+
</ul>
60+
</div>
61+
</div>
62+
</div>
63+
</li>
64+
`
65+
}
66+
67+
authContainer.innerHTML = generateUserMenuHTML('espace-collapse')
68+
69+
const authContainerMobile = document.getElementById('header-auth-mobile')
70+
if (authContainerMobile) {
71+
authContainerMobile.innerHTML = generateUserMenuHTML(
72+
'espace-collapse-mobile'
73+
)
74+
}
75+
}
76+
77+
const keycloak = new Keycloak({
78+
url: 'https://sso.geopf.fr',
79+
realm: 'geoplateforme',
80+
clientId: 'cartes-gouv-public',
81+
})
82+
83+
// "Authorization Code" flow avec PKCE (type de client Keycloak : Public).
84+
keycloak
85+
.init({
86+
onLoad: 'check-sso',
87+
flow: 'standard',
88+
pkceMethod: 'S256',
89+
checkLoginIframe: false,
90+
silentCheckSsoRedirectUri: `${window.location.origin}/assets/silent-check-sso.html`,
91+
})
92+
.then(async (authenticated) => {
93+
if (!authenticated) {
94+
renderLoggedOut()
95+
return
96+
}
97+
98+
await renderLoggedIn()
99+
100+
// Si on veut garder le token à jour pour d'éventuelles futures appels API :
101+
// window.setInterval(() => {
102+
// keycloak.updateToken(60).catch(() => {
103+
// // Si le rafraîchissement échoue, on affiche simplement l'interface déconnectée.
104+
// renderLoggedOut();
105+
// });
106+
// }, 30_000);
107+
})
108+
.catch((error) => {
109+
console.error('Failed to initialize Keycloak', error)
110+
renderLoggedOut()
111+
})
112+
})()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="fr">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="robots" content="noindex,nofollow" />
7+
<title>silent-check-sso</title>
8+
</head>
9+
10+
<body>
11+
<script>
12+
// keycloak-js : utilisé pour le check-sso sans rediriger l'utilisateur.
13+
parent.postMessage(location.href, location.origin)
14+
</script>
15+
</body>
16+
</html>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
declare module 'keycloak-js' {
2+
export interface KeycloakConfig {
3+
url?: string
4+
realm?: string
5+
clientId?: string
6+
[key: string]: any
7+
}
8+
9+
export interface KeycloakInitOptions {
10+
onLoad?: string
11+
flow?: string
12+
pkceMethod?: string
13+
checkLoginIframe?: boolean
14+
silentCheckSsoRedirectUri?: string
15+
[key: string]: any
16+
}
17+
18+
export interface KeycloakTokenParsed {
19+
exp?: number
20+
iat?: number
21+
nonce?: string
22+
sub?: string
23+
session_state?: string
24+
realm_access?: { roles: string[] }
25+
resource_access?: { [key: string]: { roles: string[] } }
26+
name?: string
27+
given_name?: string
28+
family_name?: string
29+
preferred_username?: string
30+
email?: string
31+
[key: string]: any
32+
}
33+
34+
export default class Keycloak {
35+
constructor(config?: KeycloakConfig | string)
36+
init(options?: KeycloakInitOptions): Promise<boolean>
37+
login(options?: { redirectUri?: string; [key: string]: any }): Promise<void>
38+
logout(options?: {
39+
redirectUri?: string
40+
[key: string]: any
41+
}): Promise<void>
42+
updateToken(minValidity?: number): Promise<boolean>
43+
44+
token?: string
45+
tokenParsed?: KeycloakTokenParsed
46+
idToken?: string
47+
idTokenParsed?: KeycloakTokenParsed
48+
authenticated?: boolean
49+
}
50+
}

conf/default.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,5 @@ record.metadata.quality.contacts.success="Le contact est renseigné"
240240
record.metadata.quality.abstract.success="La description est renseignée"
241241
record.metadata.quality.abstract.failed="La description n'est pas renseignée"
242242
search.filters.organization="Producteur"
243-
results.records.hits.found="{hits, plural, =0{Aucun résultat.} one{1 résultat trouvé.} other{{hits} résultats.}}"
243+
results.records.hits.found="{hits, plural, =0{Aucun résultat.} one{1 résultat trouvé.} other{{hits} résultats.}}"
244+
results.sortBy.changeDate="Dernière modification"

libs/feature/search/src/lib/sort-by/sort-by.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export class SortByComponent implements OnInit {
4040
label: marker('results.sortBy.popularity'),
4141
value: SortByEnum.POPULARITY,
4242
},
43+
{
44+
label: marker('results.sortBy.changeDate'),
45+
value: SortByEnum.CHANGE_DATE,
46+
},
4347
]
4448
currentSortBy$ = this.facade.sortBy$.pipe(
4549
filter((sortBy) => !!sortBy),

0 commit comments

Comments
 (0)