Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 10/11 #256

Merged
merged 5 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ VITE_AUTH_PROVIDER_MODULE_AUTH0_HELPTEXT='Login with your <a href="https://googl

VITE_AUTH_PROVIDER_MODULE_RAS=true
VITE_AUTH_PROVIDER_MODULE_RAS_TYPE=RAS
VITE_AUTH_PROVIDER_MODULE_RAS_IDP=ras
VITE_AUTH_PROVIDER_MODULE_RAS_CLIENTID=12345ABCD
VITE_AUTH_PROVIDER_MODULE_RAS_URI=http://pic-sure.org/ras
VITE_AUTH_PROVIDER_MODULE_RAS_DESCRIPTION="Login with RAS"
VITE_AUTH_PROVIDER_MODULE_RAS_OKTAIDPID=12345ABCDE
VITE_AUTH_PROVIDER_MODULE_RAS_CONNECTION=okta-ras
VITE_AUTH_PROVIDER_MODULE_RAS_DESCRIPTION='Login with Researcher Auth Service (RAS)'
VITE_AUTH_PROVIDER_MODULE_RAS_ALT=false
VITE_AUTH_PROVIDER_MODULE_RAS_IMAGESRC=NIH_2013_logo_vertical_text_removed.svg
VITE_AUTH_PROVIDER_MODULE_RAS_IMAGEALT='NIH 2013 Logo'
VITE_AUTH_PROVIDER_MODULE_RAS_LOGOUTURL=https://authtest.nih.gov/siteminderagent/smlogoutredirector.asp?TARGET=
VITE_AUTH_PROVIDER_MODULE_RAS_OKTALOGOUTURL=https://hms-srce.oktapreview.com/oauth2/default/v1/logout

VITE_AUTH_PROVIDER_MODULE_FENCE=true
VITE_AUTH_PROVIDER_MODULE_FENCE_TYPE=FENCE
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Tests
on:
push:
branches: [main, dev]
branches: [main, release]
pull_request:
branches: [main, dev]
branches: [main, release]
jobs:
test:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ input.required:invalid,
}

&:disabled {
@apply text-surface-500-400-token;
@apply text-surface-400;
}
}

Expand Down
48 changes: 48 additions & 0 deletions src/lib/assets/login/NIH_2013_logo_vertical_text_removed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 64 additions & 15 deletions src/lib/auth/RAS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,46 @@ import { browser } from '$app/environment';
import type { AuthData } from '$lib/models/AuthProvider';
import AuthProvider from '$lib/models/AuthProvider';
import * as api from '$lib/api';
import type { User } from '$lib/models/User';
import type { OktaUser } from '$lib/models/User';
import { login as UserLogin } from '$lib/stores/User';

interface RasData extends AuthData {
uri: string;
clientid: string;
state?: string;
idp: string;
oktaidpid: string;
oktalogouturl: string;
}

class RAS extends AuthProvider implements RasData {
uri: string;
clientid: string;
state: string;
oktaidpid: string;
oktalogouturl: string;
idp: string;

constructor(data: RasData) {
super(data);
this.state = localStorage.getItem('state') || 'ras-' + this.generateRandomState();
localStorage.setItem('state', this.state);

if (
data.uri === undefined ||
data.clientid === undefined ||
data.oktaidpid === undefined ||
data.logouturl === undefined ||
data.oktalogouturl === undefined
) {
throw new Error('Missing required RAS parameter(s).');
}

this.uri = data.uri;
this.clientid = data.clientid;
this.state = data.state ?? this.generateRandomState();
this.oktaidpid = data.oktaidpid;
this.idp = data.idp;
this.oktalogouturl = data.oktalogouturl;
}

private generateRandomState() {
Expand All @@ -29,22 +50,30 @@ class RAS extends AuthProvider implements RasData {
return randomPart + timePart;
}

//TODO: create real return types
// eslint-disable-next-line @typescript-eslint/no-unused-vars
authenticate = async (hashParts: string[]): Promise<boolean> => {
const responseMap = this.getResponseMap(hashParts);
const code = responseMap.get('code');
let state = '';
if (browser) {
state = sessionStorage.getItem('state') || '';
}
if (!code || state !== this.state) {
const responseState = responseMap.get('state') || '';
const localState = this.state;

if (!code || localState !== responseState) {
console.debug(
'RAS authentication failed code: ',
code,
' state: ',
responseState,
' localState: ',
localState,
);
return true;
}

try {
const newUser: User = await api.post('psama/okta/authentication', { code });
const newUser: OktaUser = await api.post('psama/authentication/ras', { code });
if (newUser?.token) {
UserLogin(newUser.token);
await UserLogin(newUser.token);
newUser.oktaIdToken && localStorage.setItem('oktaIdToken', newUser.oktaIdToken);
return false;
} else {
return true;
Expand All @@ -59,13 +88,33 @@ class RAS extends AuthProvider implements RasData {
let redirectUrl = '/';
if (browser) {
redirectUrl = this.getRedirectURI();
window.location.href = encodeURI(
`${this.uri}/oauth2/default/v1/authorize?response_type=code&scope=openid&client_id=${this.clientid}&provider=${type}&redirect_uri=${redirectUrl}&state=${this.state}`,
);
redirectUrl = redirectUrl.replace(/\/$/, '');
this.saveState(redirectTo, type, this.idp);
const rasClientID = encodeURIComponent(this.clientid);
const rasIdpId = encodeURIComponent(this.oktaidpid);
window.location.href = `${this.uri}?response_type=code&scope=openid&client_id=${rasClientID}&idp=${rasIdpId}&redirect_uri=${redirectUrl}&state=${this.state}`;
}
};
logout = (): Promise<void> => {
throw new Error('Method not implemented.');
logout = (): Promise<string> => {
localStorage.removeItem('state');

const redirect = encodeURI(
`${window.location.protocol}//${window.location.hostname}${
window.location.port ? ':' + window.location.port : ''
}/login`,
);

const oktaIdToken = localStorage.getItem('oktaIdToken');
const oktaRedirect =
this.oktalogouturl +
'?id_token_hint=' +
oktaIdToken +
'&post_logout_redirect_uri=' +
redirect;

const oktaEncodedRedirect = encodeURIComponent(oktaRedirect);
const logoutUrl = this.logouturl + oktaEncodedRedirect;
return Promise.resolve(logoutUrl);
};
}

Expand Down
8 changes: 8 additions & 0 deletions src/lib/components/LoginButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
resetSearch();
});
};

let imageSrc: string | undefined = undefined;
if (provider.imagesrc) {
imageSrc = './' + provider.imagesrc;
}
</script>

<button
Expand All @@ -23,6 +28,9 @@
class={$$props.class ?? 'btn variant-filled-primary m-1'}
on:click={() => login(redirectTo, provider.type)}
>
{#if imageSrc}
<img src={imageSrc} alt={provider.imageAlt} class="mr-2 h-8" />
{/if}
{buttonText}
</button>

Expand Down
20 changes: 18 additions & 2 deletions src/lib/components/Navigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
import { user, userRoutes, logout } from '$lib/stores/User';
import type { Route } from '$lib/models/Route';
import Logo from '$lib/components/Logo.svelte';

import type AuthData from '$lib/models/AuthProvider.ts';
import type AuthProvider from '$lib/models/AuthProvider.ts';
import { browser } from '$app/environment';
import { createInstance } from '$lib/AuthProviderRegistry.ts';
import { onMount } from 'svelte';
let providerData: AuthData;
let providerInstance: AuthProvider | undefined = undefined;
function setDropdown(path: string) {
dropdownPath = path;
}
Expand Down Expand Up @@ -36,6 +42,16 @@
return $page.url.pathname.includes(route.path) ? 'page' : undefined;
};

onMount(async () => {
if (browser && $page) {
const providerType = sessionStorage.getItem('type');
if (providerType) {
providerData = $page.data?.providers.find((p: AuthProvider) => p.type === providerType);
providerInstance = await createInstance(providerData);
}
}
});

$: dropdownPath = '';
</script>

Expand Down Expand Up @@ -116,7 +132,7 @@
id="user-logout-btn"
class="btn variant-ringed-primary"
title="Logout"
on:click={logout}>Logout</button
on:click={() => logout(providerInstance)}>Logout</button
>
</div>
{:else}
Expand Down
62 changes: 42 additions & 20 deletions src/lib/components/OptionsSelectionList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,64 @@
let searchInput: string = '';
export let unselectedOptions: string[] = [];
export let selectedOptions: string[] = [];
export let selectedOptionEndLocation = 20;
export let currentlyLoading: boolean = false;

export let showClearAll: boolean = true;
export let showSelectAll: boolean = true;
export let allOptionsLoaded: boolean = false;

export let allOptions: string[] = [];
export let allOptions: string[] | undefined = undefined;

let currentlyLoadingSelected: boolean = false;
let unselectedOptionsContainer: HTMLElement;
let selectedOptionsContainer: HTMLElement;
let selectedOptionEndLocation = 20;
let allSelectedOptionsLoaded: boolean = false;

const dispatch = createEventDispatcher<{ scroll: { search: string } }>();

function shouldLoadMore(element: HTMLElement) {
$: infiniteScroll = allOptions === undefined;

$: totalAvailableOptions = infiniteScroll
? Infinity
: (allOptions?.length || 0) - selectedOptions.length;

$: allUnselectedOptionsLoaded = infiniteScroll
? allOptionsLoaded
: unselectedOptions.length >= totalAvailableOptions;

$: allSelectedOptionsLoaded = infiniteScroll
? allSelectedOptionsLoaded
: displayedSelectedOptions.length >= selectedOptions.length;

$: displayedSelectedOptions = selectedOptions.slice(0, selectedOptionEndLocation);

function shouldLoadMore(element: HTMLElement, allLoaded: boolean) {
const scrollTop = element.scrollTop;
const containerHeight = element.clientHeight;
const contentHeight = element.scrollHeight;
const scrollBuffer = 30;
const hasLoadedAll = !unselectedOptions || unselectedOptions.length === 0;
return (
hasLoadedAll ||
(!hasLoadedAll && contentHeight - (scrollTop + containerHeight) <= scrollBuffer)
);
return !allLoaded && contentHeight - (scrollTop + containerHeight) <= scrollBuffer;
}

function handleScroll() {
if (!unselectedOptionsContainer) return;
if (!currentlyLoading && shouldLoadMore(unselectedOptionsContainer)) {
if (
!currentlyLoading &&
shouldLoadMore(unselectedOptionsContainer, allUnselectedOptionsLoaded)
) {
dispatch('scroll', { search: searchInput });
}
}

function loadMoreSelectedOptions() {
if (!selectedOptionsContainer) return;
currentlyLoadingSelected = true;
if (shouldLoadMore(selectedOptionsContainer, allSelectedOptionsLoaded)) {
selectedOptionEndLocation = selectedOptionEndLocation + 20;
}
currentlyLoadingSelected = false;
}

function onSearch() {
dispatch('scroll', { search: searchInput });
unselectedOptionsContainer.scrollTop = 0;
Expand All @@ -59,7 +85,7 @@
}

function selectAllOptions() {
if (allOptions.length !== 0) {
if (allOptions && allOptions?.length !== 0) {
selectedOptions = allOptions;
unselectedOptions = [];
selectedOptionEndLocation = 20;
Expand All @@ -70,18 +96,9 @@
}
}

function loadMoreSelectedOptions() {
if (!selectedOptionsContainer) return;
if (shouldLoadMore(selectedOptionsContainer)) {
selectedOptionEndLocation = selectedOptionEndLocation + 20;
}
}

function getID(option: string) {
return option.replaceAll(' ', '-').toLowerCase();
}

$: displayedSelectedOptions = selectedOptions.slice(0, selectedOptionEndLocation);
</script>

<div data-testid="optional-selection-list" class="flex w-full">
Expand Down Expand Up @@ -169,6 +186,11 @@
{option}
</label>
{/each}
{#if currentlyLoadingSelected}
<div class="flex justify-center">
<ProgressRadial width="w-5" meter="stroke-primary-500" track="stroke-primary-500/30" />
</div>
{/if}
</div>
</section>
</div>
Expand Down
Loading