Skip to content

Commit efba302

Browse files
authored
Merge pull request #83 from cloudadoption/issue-login
Issue login
2 parents fa11f3f + e480970 commit efba302

File tree

7 files changed

+411
-0
lines changed

7 files changed

+411
-0
lines changed

blocks/header/header.css

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,39 @@ header nav .nav-tools a.button:any-link:not(.nav-tool) {
519519
transition: all 0.3s;
520520
}
521521

522+
header nav .nav-tools a.button.nav-auth-link:any-link {
523+
padding: 10px 14px;
524+
min-height: 36px;
525+
line-height: 1;
526+
white-space: nowrap;
527+
}
528+
529+
header nav .nav-auth-link .nav-auth-info {
530+
display: inline-flex;
531+
align-items: center;
532+
justify-content: center;
533+
width: 14px;
534+
height: 14px;
535+
margin-left: 8px;
536+
border: 1px solid currentcolor;
537+
border-radius: 50%;
538+
font-size: 10px;
539+
font-weight: 700;
540+
line-height: 1;
541+
text-transform: none;
542+
}
543+
544+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p > a {
545+
display: block;
546+
color: inherit;
547+
text-decoration: none;
548+
}
549+
550+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p > a:hover,
551+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p > a:focus-visible {
552+
color: var(--nav-hover-color);
553+
}
554+
522555
header nav .nav-tools a.button:any-link:not(.nav-tool):hover {
523556
background: var(--color-rust);
524557
color: var(--color-light);
@@ -735,6 +768,12 @@ header nav .nav-tools .nav-language-menu a:focus-visible {
735768
width: 16px;
736769
height: 16px;
737770
}
771+
772+
header nav .nav-tools a.button.nav-auth-link:any-link {
773+
padding: 8px 10px;
774+
font-size: var(--body-font-size-xs);
775+
letter-spacing: 1px;
776+
}
738777
}
739778

740779
/* very small phones */
@@ -1168,6 +1207,10 @@ header nav .nav-tools .nav-language-menu a:focus-visible {
11681207
width: 190px;
11691208
}
11701209

1210+
header nav .nav-tools a.button.nav-auth-link:any-link {
1211+
padding: 12px 18px;
1212+
}
1213+
11711214
header nav .nav-tools .nav-search-input:hover {
11721215
border-color: light-dark(rgb(0 0 0 / 24%), rgb(255 255 255 / 30%));
11731216
}
@@ -1177,6 +1220,57 @@ header nav .nav-tools .nav-language-menu a:focus-visible {
11771220
}
11781221
}
11791222

1223+
@media (width <= 899px) {
1224+
header nav .nav-tools .nav-auth-desktop {
1225+
display: none;
1226+
}
1227+
1228+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item {
1229+
border-bottom: 0;
1230+
padding: var(--space-m) 0;
1231+
text-align: center;
1232+
}
1233+
1234+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p {
1235+
margin: 0;
1236+
padding: 0;
1237+
justify-content: center;
1238+
}
1239+
1240+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p > a.button.nav-auth-mobile:any-link {
1241+
display: inline-flex;
1242+
align-items: center;
1243+
justify-content: center;
1244+
border: 1.5px solid var(--color-rust);
1245+
border-radius: 4px;
1246+
background: transparent;
1247+
color: var(--color-rust);
1248+
padding: 12px 24px;
1249+
font-size: 0.75rem;
1250+
font-weight: 700;
1251+
text-transform: uppercase;
1252+
letter-spacing: 1.5px;
1253+
line-height: 1;
1254+
min-width: 120px;
1255+
text-decoration: none;
1256+
}
1257+
1258+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p > a.button.nav-auth-mobile:any-link:hover,
1259+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item > p > a.button.nav-auth-mobile:any-link:focus-visible {
1260+
background: var(--color-rust);
1261+
color: var(--color-light);
1262+
transform: none;
1263+
box-shadow: none;
1264+
text-decoration: none;
1265+
}
1266+
}
1267+
1268+
@media (width >= 900px) {
1269+
header nav .nav-sections .default-content-wrapper > ul > li.nav-auth-mobile-item {
1270+
display: none;
1271+
}
1272+
}
1273+
11801274
/* large screens: 1200px */
11811275
@media (width >= 1200px) {
11821276
header nav {

blocks/header/header.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
import { getMetadata } from '../../scripts/aem.js';
88
import { loadFragment } from '../fragment/fragment.js';
99
import { getBlockContext } from '../../scripts/shared.js';
10+
import {
11+
getLoginUrl,
12+
getLogoutUrl,
13+
getDefaultAuthLabel,
14+
getSessionState,
15+
} from '../../scripts/shared/auth-api.js';
1016

1117
const DESKTOP = window.matchMedia('(min-width: 900px)');
1218
const THEME_KEY = 'diyfire-theme';
@@ -231,6 +237,99 @@ function getCookie(name) {
231237
return decodeURIComponent(cookie.split('=').slice(1).join('='));
232238
}
233239

240+
function hasCookieStartingWith(prefix) {
241+
return document.cookie
242+
.split(';')
243+
.map((entry) => decodeURIComponent(entry.split('=')[0] || '').trim())
244+
.some((cookieName) => cookieName.startsWith(prefix));
245+
}
246+
247+
function isLoggedIn() {
248+
return hasCookieStartingWith('CF_Authorization');
249+
}
250+
251+
async function resolveAuthState() {
252+
try {
253+
const session = await getSessionState();
254+
return {
255+
authenticated: Boolean(session?.authenticated),
256+
email: session?.email || '',
257+
};
258+
} catch (e) {
259+
return {
260+
authenticated: isLoggedIn(),
261+
email: '',
262+
};
263+
}
264+
}
265+
266+
function setAuthUserInfo(link, email) {
267+
link.querySelector('.nav-auth-info')?.remove();
268+
link.removeAttribute('title');
269+
link.removeAttribute('data-auth-email');
270+
271+
if (!email) return;
272+
273+
link.dataset.authEmail = email;
274+
link.setAttribute('title', email);
275+
const info = document.createElement('span');
276+
info.className = 'nav-auth-info';
277+
info.setAttribute('aria-hidden', 'true');
278+
info.setAttribute('title', email);
279+
info.textContent = 'i';
280+
link.append(info);
281+
}
282+
283+
async function initAuth(nav, tools) {
284+
const loginLabel = getDefaultAuthLabel('login');
285+
const logoutLabel = getDefaultAuthLabel('logout');
286+
287+
const loginCandidate = tools.querySelector('a[href*="login" i], a[data-auth-link]');
288+
const shouldCreateLink = !loginCandidate;
289+
290+
const desktopLink = loginCandidate || document.createElement('a');
291+
if (shouldCreateLink) {
292+
desktopLink.href = getLoginUrl();
293+
desktopLink.className = 'button nav-auth-link nav-auth-desktop';
294+
tools.append(desktopLink);
295+
}
296+
297+
desktopLink.dataset.authLink = 'true';
298+
if (!desktopLink.classList.contains('button')) desktopLink.classList.add('button');
299+
desktopLink.classList.add('nav-auth-link', 'nav-auth-desktop');
300+
301+
let mobileLink = nav.querySelector('.nav-auth-mobile-item a');
302+
if (!mobileLink) {
303+
const mobileList = nav.querySelector('.nav-sections .default-content-wrapper > ul');
304+
if (mobileList) {
305+
const li = document.createElement('li');
306+
li.className = 'nav-auth-mobile-item';
307+
const p = document.createElement('p');
308+
mobileLink = document.createElement('a');
309+
mobileLink.className = 'button nav-auth-link nav-auth-mobile';
310+
mobileLink.dataset.authLink = 'true';
311+
p.append(mobileLink);
312+
li.append(p);
313+
mobileList.append(li);
314+
}
315+
}
316+
if (mobileLink && !mobileLink.classList.contains('button')) mobileLink.classList.add('button');
317+
318+
const authState = await resolveAuthState();
319+
const loggedIn = authState.authenticated;
320+
const loginHref = getLoginUrl();
321+
const logoutHref = getLogoutUrl();
322+
const targetHref = loggedIn ? logoutHref : loginHref;
323+
const label = loggedIn ? logoutLabel : loginLabel;
324+
325+
[desktopLink, mobileLink].filter(Boolean).forEach((link) => {
326+
link.setAttribute('href', targetHref);
327+
link.textContent = label;
328+
link.setAttribute('aria-label', label);
329+
setAuthUserInfo(link, loggedIn ? authState.email : '');
330+
});
331+
}
332+
234333
function ensureGoogleTranslateScript() {
235334
if (window.__googleTranslateScriptLoaded) return Promise.resolve();
236335
if (window.__googleTranslateScriptPromise) return window.__googleTranslateScriptPromise;
@@ -437,6 +536,7 @@ export default async function decorate(block) {
437536
if (tools) {
438537
initTheme(tools);
439538
initSearch(tools);
539+
await initAuth(nav, tools);
440540
initLanguage(tools, eventRoot);
441541
hydrateTranslateFromCookie();
442542
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"build:json:definitions": "merge-json-cli -i \"ue/models/component-definition.json\" -o \"component-definition.json\"",
1414
"build:json:filters": "merge-json-cli -i \"ue/models/component-filters.json\" -o \"component-filters.json\"",
1515
"prepare": "husky",
16+
"dev:auth": "wrangler dev --config ./workers/auth/wrangler.toml",
17+
"deploy:auth": "wrangler deploy --config ./workers/auth/wrangler.toml",
18+
"tail:auth": "wrangler tail --config ./workers/auth/wrangler.toml",
1619
"dev:contact-us": "wrangler dev --config ./workers/contact_us/wrangler.toml",
1720
"deploy:contact-us": "wrangler deploy --config ./workers/contact_us/wrangler.toml",
1821
"tail:contact-us": "wrangler tail --config ./workers/contact_us/wrangler.toml"

scripts/shared/auth-api.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const AUTH_ORIGIN = 'https://demo-bbird-auth.aem-poc-lab.workers.dev';
2+
const AUTH_PATHS = {
3+
login: '/auth/login',
4+
logout: '/auth/logout',
5+
session: '/auth/session',
6+
};
7+
8+
const AUTH_LABELS = {
9+
login: 'Login',
10+
logout: 'Logout',
11+
};
12+
13+
function authUrl(path) {
14+
return new URL(path, AUTH_ORIGIN).toString();
15+
}
16+
17+
export function getDefaultAuthLabel(type) {
18+
return AUTH_LABELS[type] || '';
19+
}
20+
21+
export function getLoginUrl(returnTo = window.location.href) {
22+
const target = new URL(AUTH_PATHS.login, AUTH_ORIGIN);
23+
target.searchParams.set('returnTo', returnTo);
24+
return target.toString();
25+
}
26+
27+
export function getLogoutUrl() {
28+
return authUrl(AUTH_PATHS.logout);
29+
}
30+
31+
export async function getSessionState() {
32+
const response = await fetch(authUrl(AUTH_PATHS.session), {
33+
method: 'GET',
34+
credentials: 'include',
35+
headers: { Accept: 'application/json' },
36+
});
37+
if (!response.ok) {
38+
throw new Error(`Auth session request failed: ${response.status}`);
39+
}
40+
return response.json();
41+
}

workers/auth/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Auth Test Worker
2+
3+
Lightweight Cloudflare Worker to drive login/logout/session state for the header auth button.
4+
5+
## Endpoints
6+
7+
- `GET /auth/login?returnTo=<url-or-path>` -> triggers Access and redirects to `returnTo`
8+
- `GET /auth/logout` -> logs out via app domain `/cdn-cgi/access/logout` (logout success page)
9+
- `GET /auth/session` -> returns `{ authenticated, email, hasJwtAssertion }`
10+
11+
## Setup
12+
13+
1. Make sure your Cloudflare Access application protects the same hostname and `/auth/*` path.
14+
2. Use the configured worker name in `wrangler.toml` (`demo-bbird-auth`) or change it.
15+
3. Deploy from the project root:
16+
17+
```bash
18+
npm install
19+
npm run deploy:auth
20+
```
21+
22+
## Local dev
23+
24+
```bash
25+
npm run dev:auth
26+
```
27+
28+
## Test flow
29+
30+
1. Open an incognito window.
31+
2. Visit the login endpoint, for example:
32+
33+
```text
34+
https://demo-bbird-auth.aem-poc-lab.workers.dev/auth/login?returnTo=http://localhost:3000/
35+
```
36+
37+
3. Enter an allowed email address on the Access screen.
38+
4. Enter the one-time PIN.
39+
5. Confirm your site loads and `/auth/session` shows `authenticated: true`.
40+
41+
## Logout
42+
43+
Visit:
44+
45+
```text
46+
https://demo-bbird-auth.aem-poc-lab.workers.dev/auth/logout
47+
```
48+
49+
## Site integration
50+
51+
Header auth URLs are centralized in:
52+
53+
`scripts/shared/auth-api.js`
54+
55+
Update `AUTH_ORIGIN` there if your worker hostname changes.
56+
57+
Logout uses app-domain endpoint: `https://demo-bbird-auth.aem-poc-lab.workers.dev/cdn-cgi/access/logout`
58+
without additional redirect parameters for demo simplicity.

0 commit comments

Comments
 (0)