Skip to content
Open
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
18 changes: 17 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ PUBLIC_PUBLISHER_API_ENV_LABEL="(Sandbox)"
PUBLIC_UI_API_BASEURL="/publisher"
ADAPTER_STATIC="false"

# OPTIONAL, DEV-ONLY: bypass Publisher login for local testing.
# When "true", the login screen shows a "Skip login (dev)" button that injects a
# fake publisher session so you can exercise badge-source workflows (Accredible,
# Canvas, etc.) without a working Publisher login. Publisher environments on
# Keycloak/OIDC (e.g. Sandbox) no longer support the email/password login this
# app uses, so this is the way to test locally against them.
# Leave "false"/unset in any deployed build. Saving to the Publisher still
# requires a real, environment-issued token, so the final save step will fail.
PUBLIC_DEV_BYPASS_PUBLISHER_LOGIN="false"
PUBLIC_DEV_PUBLISHER_ORG_CTID="" # optional: org CTID to attach drafts to; defaults to a placeholder
PUBLIC_DEV_PUBLISHER_ORG_NAME="" # optional: display name for the fake org

# OPTIONAL Canvas Login Settings per environment: only for use on production.
# It is OK that client secret is readable by the user, because the exchange is
# protected with PKCE and exact-match redirect URIs set by Canvas admins.
Expand Down Expand Up @@ -48,4 +60,8 @@ PUBLIC_PARCHMENT_EU_LOGIN_CLIENT_SECRET=""

PUBLIC_PARCHMENT_US_ENABLED="true"
PUBLIC_PARCHMENT_US_LOGIN_CLIENT_ID=""
PUBLIC_PARCHMENT_US_LOGIN_CLIENT_SECRET=""
PUBLIC_PARCHMENT_US_LOGIN_CLIENT_SECRET=""

PUBLIC_PARCHMENT_SG_ENABLED="true"
PUBLIC_PARCHMENT_SG_LOGIN_CLIENT_ID=""
PUBLIC_PARCHMENT_SG_LOGIN_CLIENT_SECRET=""
5 changes: 5 additions & 0 deletions local.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ PUBLIC_PUBLISHER_API_BASEURL="https://localhost:44330/"
PUBLIC_PUBLISHER_API_ENV_LABEL="(local)"
PUBLIC_UI_API_BASEURL="/"
ADAPTER_STATIC="false"

# DEV-ONLY: bypass Publisher login for local testing (see .env.example for details).
PUBLIC_DEV_BYPASS_PUBLISHER_LOGIN="false"
PUBLIC_DEV_PUBLISHER_ORG_CTID="" # optional: org CTID to attach drafts to; defaults to a placeholder
PUBLIC_DEV_PUBLISHER_ORG_NAME="" # optional: display name for the fake org
PUBLIC_CANVAS_TEST_LOGIN_CLIENT_ID=""
PUBLIC_CANVAS_TEST_LOGIN_CLIENT_SECRET=""
174 changes: 174 additions & 0 deletions src/lib/partials/AccredibleConfig.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import {
accredibleApiKey,
accredibleAgreeTerms,
accredibleSelectedRegion,
accredibleGroups,
fetchAccredibleGroups
} from '$lib/stores/badgeSourceStore.js';
import { accredibleRegions, type AccredibleEnvKey } from '$lib/utils/accredible.js';
import ConfigurationStep from '$lib/components/ConfigurationStep.svelte';
import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte';
import RadioCard from '$lib/components/RadioCard.svelte';
import OpenEye from '$lib/icons/eye.svelte';
import ClosedEye from '$lib/icons/closed-eye.svelte';
import LoadingSpinner from '$lib/components/LoadingSpinner.svelte';
import BodyText from '$lib/components/typography/BodyText.svelte';
import Heading from '$lib/components/typography/Heading.svelte';

let accredibleApiKeyHidden = true;
let debounceLoadGroups = false;
let loadGroupsMessage = '';
let loadGroupsPromise: Promise<boolean> = new Promise((resolve) => resolve(true));

const handleSelectRegion = (regionId: string) => {
$accredibleSelectedRegion = regionId as AccredibleEnvKey;
};

const handleLoadGroups = () => {
if (
debounceLoadGroups ||
!$accredibleSelectedRegion ||
!$accredibleAgreeTerms ||
!$accredibleApiKey
)
return;

loadGroupsMessage = '';
debounceLoadGroups = true;
loadGroupsPromise = fetchAccredibleGroups().catch((err) => {
loadGroupsMessage = err.message || 'Error loading badges from Accredible.';
throw err;
});
setTimeout(() => (debounceLoadGroups = false), 5000);
};
</script>

<Heading><h3>Configure Accredible connection</h3></Heading>

<BodyText>
You'll need an Accredible API key with issuer access, found in Accredible under Settings &gt;
API &amp; Integrations. Use a Sandbox key when testing against a Sandbox account.
</BodyText>

<div class="mt-8 mb-2">
<ConfigurationStep
stepNumber="5a"
stepName="Choose environment"
isActive={!$accredibleSelectedRegion}
/>
</div>
<ul class="mt-4 md:grid gap-6 w-full grid-cols-2 xl:grid-cols-3">
{#each [...accredibleRegions.values()] as region}
<RadioCard
label={region.name}
name="accredibleregionradio"
groupValue={$accredibleSelectedRegion}
value={region.id}
on:select={(e) => handleSelectRegion(e.detail.value)}
description={region.apiDomain}
/>
{/each}
</ul>

{#if $accredibleSelectedRegion}
<div class="mt-8 mb-2" transition:fade>
<ConfigurationStep stepNumber="5b" stepName="Enter API key" isActive={!$accredibleApiKey} />
</div>
<div class="flex items-end flex-col md:flex-row" transition:fade>
<div class="flex mb-3 w-full">
<button
type="button"
class="inline-flex items-center p-2 text-sm text-gray-900 bg-gray-200 rounded-l border border-r-0 border-gray-300"
on:click={() => (accredibleApiKeyHidden = !accredibleApiKeyHidden)}
aria-label="Toggle API key visibility"
>
{#if accredibleApiKeyHidden}<ClosedEye />{:else}<OpenEye />{/if}
</button>
{#if accredibleApiKeyHidden}
<input
bind:value={$accredibleApiKey}
autocomplete="off"
type="password"
name="accredible_api_key"
id="input_accredibleapikey"
class="rounded-none rounded-r bg-gray-50 p-2 border border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 block flex-1 min-w-0 w-full text-sm"
placeholder="Accredible API key"
/>
{:else}
<input
bind:value={$accredibleApiKey}
autocomplete="off"
type="text"
name="accredible_api_key"
id="input_accredibleapikey"
class="rounded-none rounded-r bg-gray-50 p-2 border border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 block flex-1 min-w-0 w-full text-sm"
placeholder="Accredible API key"
/>
{/if}
</div>
</div>
{/if}

{#if $accredibleSelectedRegion && $accredibleApiKey}
<div class="mt-8 mb-2" transition:fade>
<ConfigurationStep
stepNumber="5c"
stepName="Agree to terms"
isActive={!$accredibleAgreeTerms}
/>
</div>
<div class="mt-2" transition:fade>
<div class="py-4 flex items-center">
<input
bind:checked={$accredibleAgreeTerms}
id="accredibleAgreeTerms"
type="checkbox"
class="w-4 h-4 text-tahiti bg-gray-100 rounded border-gray-300 focus:ring-tahiti focus:ring-2"
/>
<label for="accredibleAgreeTerms" class="ml-2 max-w-prose text-sm font-medium text-gray-900">
I certify that I am a representative of the listed organization and authorized to publish
this data to the Credential Registry.
</label>
</div>
</div>
{/if}

{#if $accredibleSelectedRegion && $accredibleApiKey && $accredibleAgreeTerms}
<div class="mt-8 mb-2" transition:fade>
<ConfigurationStep stepNumber="5d" stepName="Load badges" isActive={!$accredibleGroups.length} />
</div>
{#await loadGroupsPromise}
<div role="status" class="max-w-sm animate-pulse mt-8" transition:fade>
<div class="h-2.5 bg-gray-200 rounded-full w-48 mb-4" />
<div class="h-2 bg-gray-200 rounded-full max-w-[360px] mb-2.5" />
<span class="sr-only">Loading...</span>
</div>
{:then}
{#if $accredibleGroups.length}
<div class="max-w-sm mt-8" transition:fade>
<BodyText>
Found {$accredibleGroups.length}
{$accredibleGroups.length == 1 ? 'badge' : 'badges'} on Accredible.
</BodyText>
<BodyText gray={true}>
Skill/outcome alignments that don't resolve to a URL (framework code) will be omitted
from the imported data, since Badge Publisher requires a URL on every alignment entry.
</BodyText>
</div>
{:else}
<div
class="my-4 flex flex-col items-center justify-center w-full h-32 rounded-lg border-2 border-gray-300 border-dashed"
transition:fade
>
<Button buttonType="primary" disabled={debounceLoadGroups} on:click={handleLoadGroups}>
Load badges from Accredible
</Button>
</div>
{/if}
{:catch}
<Alert message={loadGroupsMessage} level="error" heading="Fetching badges failed." />
{/await}
{/if}
11 changes: 11 additions & 0 deletions src/lib/partials/BadgeSourceConfig.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import NextPrevButton from '$lib/components/NextPrevButton.svelte';
import CanvasConfig from '$lib/partials/CanvasConfig.svelte';
import CredlyConfig from '$lib/partials/CredlyConfig.svelte';
import AccredibleConfig from '$lib/partials/AccredibleConfig.svelte';
import AdvancedBadgeInput from './AdvancedBadgeInput.svelte';
import BadgeSelection from '$lib/partials/BadgeSelection.svelte';
import {
Expand Down Expand Up @@ -110,6 +111,14 @@
on:select={(e) => ($badgeSourceType = e.detail.value)}
description="A leading badge platform focused on resume-ready achievements in education, workforce, and professional development."
/>
<RadioCard
label="Accredible"
name="sourcetyperadio"
groupValue={$badgeSourceType}
value="accredible"
on:select={(e) => ($badgeSourceType = e.detail.value)}
description="A digital credentialing platform for badges, certificates, and diplomas."
/>
<RadioCard
label="JSON"
name="sourcetyperadio"
Expand All @@ -134,6 +143,8 @@
<ParchmentConfig />
{:else if $badgeSourceType == 'credly'}
<CredlyConfig />
{:else if $badgeSourceType == 'accredible'}
<AccredibleConfig />
{:else if $badgeSourceType == 'json'}
<AdvancedBadgeInput />
{/if}
Expand Down
Loading