Skip to content

Commit d38d744

Browse files
Merge branch 'main' into translations
2 parents 735a6d8 + abfc034 commit d38d744

29 files changed

+860
-82
lines changed

package-lock.json

Lines changed: 13 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@fortawesome/free-regular-svg-icons": "^6.7.2",
3737
"@fortawesome/free-solid-svg-icons": "^6.7.2",
3838
"@fortawesome/react-fontawesome": "^0.2.2",
39+
"@natlibfi/ekirjasto-opds-feed-parser": "2.0.1",
3940
"@nypl/design-system-react-components": "^0.7.2",
4041
"@nypl/dgx-svg-icons": "^0.3.12",
4142
"@theme-ui/color": "^0.16.2",
@@ -58,7 +59,6 @@
5859
"next-i18next": "^15.4.3",
5960
"next-transpile-modules": "^10.0.1",
6061
"node-fetch": "^2.6.13",
61-
"opds-feed-parser": "^0.1.0",
6262
"react": "^18.3.1",
6363
"react-dom": "^18.2.0",
6464
"react-hook-form": "^7.53.1",

src/auth/AuthenticationHandler.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,37 @@ import React from "react";
22
import BasicAuthHandler from "./BasicAuthHandler";
33
import BasicTokenAuthHandler from "./BasicTokenAuthHandler";
44
import CleverAuthHandler from "./CleverAuthHandler";
5+
import EkirjastoAuthHandler from "./EkirjastoAuthHandler";
56
import SamlAuthHandler from "./SamlAuthHandler";
67
import {
78
ClientBasicMethod,
89
ClientBasicTokenMethod,
10+
ClientEkirjastoMethod,
911
ClientSamlMethod
1012
} from "interfaces";
1113
import {
1214
BasicAuthType,
1315
CleverAuthType,
1416
CleverAuthMethod,
1517
SamlAuthType,
16-
BasicTokenAuthType
18+
BasicTokenAuthType,
19+
EkirjastoAuthType,
20+
EkirjastoMethod
1721
} from "../types/opds1";
1822
import track from "../analytics/track";
1923
import ApplicationError from "../errors";
2024

2125
type SupportedAuthTypes =
26+
| typeof EkirjastoAuthType
2227
| typeof BasicAuthType
2328
| typeof BasicTokenAuthType
2429
| typeof SamlAuthType
2530
| typeof CleverAuthType;
2631

2732
type SupportedAuthHandlerProps = {
28-
[key in SupportedAuthTypes]: key extends typeof BasicAuthType
33+
[key in SupportedAuthTypes]: key extends typeof EkirjastoAuthType
34+
? EkirjastoMethod
35+
: key extends typeof BasicAuthType
2936
? ClientBasicMethod
3037
: key extends typeof BasicTokenAuthType
3138
? ClientBasicTokenMethod
@@ -44,6 +51,7 @@ export const authHandlers: {
4451
method: SupportedAuthHandlerProps[key];
4552
}>;
4653
} = {
54+
[EkirjastoAuthType]: EkirjastoAuthHandler,
4755
[BasicAuthType]: BasicAuthHandler,
4856
[BasicTokenAuthType]: BasicTokenAuthHandler,
4957
[SamlAuthType]: SamlAuthHandler,
@@ -62,6 +70,9 @@ const AuthenticationHandler: React.ComponentType<AuthHandlerWrapperProps> = ({
6270
}) => {
6371
const _AuthHandler = authHandlers[method.type];
6472

73+
if (method.type === EkirjastoAuthType && typeof method !== "string") {
74+
return <_AuthHandler method={method as ClientEkirjastoMethod} />;
75+
}
6576
if (method.type === BasicTokenAuthType && typeof method !== "string") {
6677
return <_AuthHandler method={method as ClientBasicTokenMethod} />;
6778
} else if (method.type === BasicAuthType && typeof method !== "string") {

src/auth/EkirjastoAuthHandler.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/** @jsxRuntime classic */
2+
/** @jsx jsx */
3+
import { jsx } from "theme-ui";
4+
import * as React from "react";
5+
import { ClientEkirjastoMethod } from "interfaces";
6+
import LoadingIndicator from "components/LoadingIndicator";
7+
import Stack from "components/Stack";
8+
import useUser from "components/context/UserContext";
9+
import useLoginRedirectUrl from "auth/useLoginRedirect";
10+
import { clientOnly } from "components/ClientOnly";
11+
12+
/**
13+
* The Ekirjasto Auth handler sends you off to an external website to complete
14+
* auth.
15+
*/
16+
const EkirjastoAuthHandler: React.FC<{ method: ClientEkirjastoMethod }> = ({
17+
method
18+
}) => {
19+
const { token, signOut } = useUser();
20+
const { authSuccessUrl } = useLoginRedirectUrl();
21+
22+
// Get link for strong authentication
23+
const authenticationStartHref = method.links?.find(
24+
link => link.rel === "tunnistus_start"
25+
)?.href;
26+
27+
//Create link with redirect
28+
const urlWithRedirect = `${authenticationStartHref}&redirect_uri=${encodeURIComponent(
29+
authSuccessUrl
30+
)}`;
31+
32+
// Start login
33+
React.useEffect(() => {
34+
if (!token && urlWithRedirect) {
35+
window.location.href = urlWithRedirect;
36+
console.log(urlWithRedirect);
37+
}
38+
}, [token, signOut, urlWithRedirect]);
39+
40+
return (
41+
<Stack direction="column" sx={{ alignItems: "center" }}>
42+
<LoadingIndicator />
43+
Logging in with EkirjastoAuthentication...
44+
</Stack>
45+
);
46+
};
47+
48+
export default clientOnly(EkirjastoAuthHandler);

src/auth/Login.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Text } from "components/Text";
88
import LoadingIndicator from "components/LoadingIndicator";
99
import useLogin from "auth/useLogin";
1010
import { isSupportedAuthType } from "./AuthenticationHandler";
11+
import { EKIRJASTO_AUTH_TYPE } from "utils/constants";
1112

1213
export default function Login(): React.ReactElement {
1314
const { initLogin } = useLogin();
@@ -20,9 +21,17 @@ export default function Login(): React.ReactElement {
2021
);
2122

2223
// Automatically redirect user to first supported auth method
24+
// Unless there is ekirjasto authentication, then redirect to that
2325
React.useEffect(() => {
2426
if (supportedAuthMethods.length > 0) {
25-
initLogin(supportedAuthMethods[0].id);
27+
const ekirjastoAuth = supportedAuthMethods.find(
28+
method => method.type === EKIRJASTO_AUTH_TYPE
29+
);
30+
if (ekirjastoAuth) {
31+
initLogin(ekirjastoAuth.id);
32+
} else {
33+
initLogin(supportedAuthMethods[0].id);
34+
}
2635
}
2736
}, [supportedAuthMethods, initLogin]);
2837

src/auth/ekirjastoFetch.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import ApplicationError, { ServerError } from "errors";
2+
import fetchWithHeaders from "../dataflow/fetch";
3+
4+
/**
5+
* Fetch bearer (circulation) token for authenticating user
6+
*/
7+
export async function fetchEAuthToken(
8+
url: string | undefined,
9+
token: string | undefined
10+
) {
11+
if (!url) {
12+
throw new ApplicationError({
13+
title: "Incomplete Authentication Info",
14+
detail: "No URL or Token was provided for authentication"
15+
});
16+
}
17+
18+
const response = await fetchWithHeaders(url, `Bearer ${token}`, {}, "POST");
19+
const json = await response.json();
20+
21+
if (!response.ok) {
22+
throw new ServerError(url, response.status, json);
23+
}
24+
25+
return json;
26+
}

src/auth/useCredentials.ts

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as React from "react";
22
import Cookie from "js-cookie";
3-
import { AuthCredentials, OPDS1 } from "interfaces";
3+
import { AppAuthMethod, AuthCredentials, OPDS1 } from "interfaces";
44
import { IS_SERVER } from "utils/env";
55
import { NextRouter, useRouter } from "next/router";
66
import { generateCredentials } from "utils/auth";
7-
import { SAML_LOGIN_QUERY_PARAM } from "utils/constants";
7+
import {
8+
EKIRJASTO_AUTH_TYPE,
9+
EKIRJASTO_TOKEN_PARAM,
10+
SAML_LOGIN_QUERY_PARAM
11+
} from "utils/constants";
812

913
/**
1014
* This hook:
@@ -16,16 +20,31 @@ import { SAML_LOGIN_QUERY_PARAM } from "utils/constants";
1620
* if finds a token, it extracts it and sets it as the current
1721
* credentials.
1822
*/
19-
export default function useCredentials(slug: string | null) {
23+
export default function useCredentials(
24+
slug: string | null,
25+
authMethods: AppAuthMethod[] | null
26+
) {
2027
const router = useRouter();
28+
29+
// Since we don't actually call the login function anywhere, we need to put the authentication url
30+
// in somehow, so we fetch it here and take it to getCredentialsState function
31+
const ekirjastoMethod = authMethods?.find(
32+
method => method.type === EKIRJASTO_AUTH_TYPE
33+
);
34+
let authenticationUrl;
35+
if (ekirjastoMethod) {
36+
authenticationUrl = ekirjastoMethod.links?.find(
37+
link => link.rel === "authenticate"
38+
)?.href;
39+
}
2140
const [credentialsState, setCredentialsState] = React.useState<
2241
AuthCredentials | undefined
23-
>(getCredentialsCookie(slug));
42+
>(getCredentialsCookie(slug, authenticationUrl));
2443
// sync up cookie state with react state
2544
React.useEffect(() => {
26-
const cookie = getCredentialsCookie(slug);
45+
const cookie = getCredentialsCookie(slug, authenticationUrl);
2746
if (cookie) setCredentialsState(cookie);
28-
}, [slug]);
47+
}, [authenticationUrl, slug]);
2948

3049
// set both cookie and state credentials
3150
const setCredentials = React.useCallback(
@@ -60,7 +79,7 @@ export default function useCredentials(slug: string | null) {
6079
}
6180

6281
/**
63-
* COOKIE CREDENDIALS
82+
* COOKIE CREDENTIALS
6483
*/
6584
/**
6685
* If you pass a librarySlug, the cookie will be scoped to the
@@ -70,31 +89,69 @@ function cookieName(librarySlug: string | null): string {
7089
const AUTH_COOKIE_NAME = "CPW_AUTH_COOKIE";
7190
return `${AUTH_COOKIE_NAME}/${librarySlug}`;
7291
}
73-
92+
/**
93+
* When using ekirjasto authentication, we don't use scoping to a particular library
94+
* @returns ekirjasto cookie name
95+
*/
96+
function cookieNameEkirjasto(): string {
97+
const AUTH_COOKIE_NAME = EKIRJASTO_TOKEN_PARAM;
98+
return AUTH_COOKIE_NAME;
99+
}
100+
/**
101+
* Get credentials from cookies.
102+
* We assume the token is in access_token cookie, and that it is
103+
* of the Ekirjasto Authentication type
104+
*
105+
* @param librarySlug Library slug, that is useful if we have multiple libraries
106+
* @param authenticationUrl AuthenticationUrl where we make refresh requests
107+
* @returns Ekirjasto credentials if access_token is available, otherwise undefined
108+
*/
74109
function getCredentialsCookie(
75-
librarySlug: string | null
110+
librarySlug: string | null,
111+
authenticationUrl: string | null
76112
): AuthCredentials | undefined {
77-
const credentials = Cookie.get(cookieName(librarySlug));
78-
return credentials ? JSON.parse(credentials) : undefined;
113+
if (librarySlug === "ekirjasto") {
114+
// Get access token, for ekirjasto login credentials
115+
const accessToken = Cookie.get(cookieNameEkirjasto());
116+
// Create ekirjasto authentication credentials
117+
const authCredentials: AuthCredentials = {
118+
token: `Bearer ${accessToken}`,
119+
methodType: OPDS1.EkirjastoAuthType,
120+
authenticationUrl: authenticationUrl ? authenticationUrl : undefined
121+
};
122+
// Return the credentials
123+
return authCredentials ? authCredentials : undefined;
124+
} else {
125+
const credentials = Cookie.get(cookieName(librarySlug));
126+
return credentials ? JSON.parse(credentials) : undefined;
127+
}
79128
}
80129

81130
function setCredentialsCookie(
82131
librarySlug: string | null,
83132
credentials: AuthCredentials
84133
) {
85-
Cookie.set(cookieName(librarySlug), JSON.stringify(credentials));
134+
if (librarySlug === "ekirjasto") {
135+
Cookie.set(cookieNameEkirjasto(), JSON.stringify(credentials));
136+
} else {
137+
Cookie.set(cookieName(librarySlug), JSON.stringify(credentials));
138+
}
86139
}
87140

88141
function clearCredentialsCookie(librarySlug: string | null) {
89-
Cookie.remove(cookieName(librarySlug));
142+
if (librarySlug === "ekirjasto") {
143+
Cookie.remove(cookieNameEkirjasto());
144+
} else {
145+
Cookie.remove(cookieName(librarySlug));
146+
}
90147
}
91148

92149
export function generateToken(username: string, password?: string) {
93150
return generateCredentials(username, password);
94151
}
95152

96153
/**
97-
* URL CREDENTIALS
154+
* URL CREDENTIALS, NOT USED WITH EKIRJASTO
98155
*/
99156
function getUrlCredentials(router: NextRouter) {
100157
/* TODO: throw error if samlAccessToken and cleverAccessToken exist at the same time as this is an invalid state that shouldn't be reached */

src/auth/useLoginRedirect.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IS_SERVER } from "utils/env";
77
/**
88
* Extracts the redirect url stored in the browser url bar and returns
99
* a version including the origin (fullSuccessUrl) or a version that is
10-
* just a path (successPath).
10+
* just a path (successPath), or a path for special authentication handling (authSuccessUrl)
1111
*/
1212
export default function useLoginRedirectUrl() {
1313
const { buildMultiLibraryLink } = useLinkUtils();
@@ -33,8 +33,14 @@ export default function useLoginRedirectUrl() {
3333
? ""
3434
: `${window.location.origin}${successPath}`;
3535

36+
// get the url where we redirect after authentication
37+
const authSuccessUrl = IS_SERVER
38+
? ""
39+
: `${window.location.origin}/api/authsuccess`;
40+
3641
return {
3742
fullSuccessUrl,
38-
successPath
43+
successPath,
44+
authSuccessUrl
3945
};
4046
}

0 commit comments

Comments
 (0)