Skip to content
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
28 changes: 15 additions & 13 deletions src/frontend/api/__tests__/auth-client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

const mockApiClient: any = {
const mockApiClient = {
post: vi.fn(),
get: vi.fn(),
setToken: vi.fn(),
clearToken: vi.fn(),
baseUrl: 'http://test.local'

}

globalThis.localStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn()
} as any
removeItem: vi.fn(),
length: 0,
clear: vi.fn(),
key: vi.fn()
} as Storage

describe('authClient', () => {
let AuthClient: any
let authClient: any
let originalProcessClient: any
let AuthClient: typeof import('../auth-client').AuthClient
let authClient: InstanceType<typeof import('../auth-client').AuthClient>
let originalProcessClient: boolean | undefined

beforeEach(async () => {
vi.resetAllMocks()
// Mock process.client to true
originalProcessClient = globalThis.process?.client
originalProcessClient = import.meta.client
globalThis.process = { ...(globalThis.process || {}), client: true }
vi.doMock('../api-client', () => ({ apiClient: mockApiClient }))
vi.doMock('./api-client', () => ({ apiClient: mockApiClient }))
Expand All @@ -37,11 +39,11 @@ describe('authClient', () => {
vi.resetModules()
// Restore process.client
if (originalProcessClient === undefined) {
// @ts-expect-error
delete globalThis.process.client
// @ts-expect-error We expect an error here
delete import.meta.client
}
else {
globalThis.process.client = originalProcessClient
globalThis.process = { ...(globalThis.process || {}), client: originalProcessClient }
}
})

Expand Down Expand Up @@ -73,12 +75,12 @@ describe('authClient', () => {
})

it('isAuthenticated returns true if token and user exist', () => {
;(localStorage.getItem as any) = vi.fn(key => (key === 'token' ? 'token' : key === 'user' ? '{}' : null))
;(localStorage.getItem as ReturnType<typeof vi.fn>) = vi.fn(key => (key === 'token' ? 'token' : key === 'user' ? '{}' : null))
expect(authClient.isAuthenticated()).toBe(true)
})

it('isAuthenticated returns false if token or user missing', () => {
;(localStorage.getItem as any) = vi.fn(() => null)
;(localStorage.getItem as ReturnType<typeof vi.fn>) = vi.fn(() => null)
expect(authClient.isAuthenticated()).toBe(false)
})
})
38 changes: 19 additions & 19 deletions src/frontend/api/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,6 @@ import type {
VerificationResult,
} from '../types/openbadges'

let API_URL = '' // fallback, will be set by Nuxt plugin

// This will be updated when the module is initialized in the browser
export function updateApiUrl(url: string) {
if (url) { API_URL = url }
apiClient.setBaseUrl(url)
console.log('API URL set to:', API_URL)
}

/**
* API client for interacting with the Strapi backend
*/
Expand All @@ -26,7 +17,7 @@ export class ApiClient {
this.token = null

// Initialize token from localStorage if available
if (process.client) {
if (import.meta.client) {
const storedToken = localStorage.getItem('token')
if (storedToken) {
this.token = storedToken
Expand All @@ -41,7 +32,7 @@ export class ApiClient {
this.token = token

// Also store in localStorage for persistence
if (process.client) {
if (import.meta.client) {
localStorage.setItem('token', token)
}
}
Expand All @@ -52,7 +43,7 @@ export class ApiClient {
clearToken() {
this.token = null

if (process.client) {
if (import.meta.client) {
localStorage.removeItem('token')
}
}
Expand All @@ -69,7 +60,7 @@ export class ApiClient {
// Get token from instance, localStorage, or cookie
let token = this.token

if (!token && process.client) {
if (!token && import.meta.client) {
// Try localStorage
const localToken = localStorage.getItem('token')
if (localToken) {
Expand Down Expand Up @@ -145,9 +136,9 @@ export class ApiClient {
}

const error = new Error(errorMessage)
// @ts-ignore - Add response data to error for debugging
// @ts-expect-error Add response data to error for debugging
error.response = response
// @ts-ignore - Add parsed error data to error for debugging
// @ts-expect-error Add parsed error data to error for debugging
error.data = errorData
throw error
}
Expand Down Expand Up @@ -300,7 +291,7 @@ export class ApiClient {

try {
// Ensure we have a valid token
const token = this.token || (process.client ? localStorage.getItem('token') : null)
const token = this.token || (import.meta.client ? localStorage.getItem('token') : null)
if (!token) {
throw new Error('Authentication required. Please log in to issue badges.')
}
Expand Down Expand Up @@ -566,7 +557,9 @@ export class ApiClient {
* This helps handle different data formats from Strapi
*/
formatCredential(credential: any) {
if (!credential) { return null }
if (!credential) {
return null
}

const formatted: any = {
id: credential.id,
Expand Down Expand Up @@ -653,7 +646,9 @@ export class ApiClient {
* Format an array of credentials
*/
formatCredentials(credentials: any[]) {
if (!credentials || !Array.isArray(credentials)) { return [] }
if (!credentials || !Array.isArray(credentials)) {
return []
}
return credentials.map(credential => this.formatCredential(credential))
}

Expand All @@ -672,7 +667,7 @@ export class ApiClient {
throw new Error('At least one recipient is required')
}
try {
const token = this.token || (process.client ? localStorage.getItem('token') : null)
const token = this.token || (import.meta.client ? localStorage.getItem('token') : null)
if (!token) {
throw new Error('Authentication required. Please log in to issue badges.')
}
Expand Down Expand Up @@ -730,4 +725,9 @@ export class ApiClient {
// Export a singleton instance
export const apiClient = new ApiClient()

// This will be updated when the module is initialized in the browser
export function updateApiUrl(url: string) {
apiClient.setBaseUrl(url)
}

export default apiClient
22 changes: 15 additions & 7 deletions src/frontend/api/auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ export class AuthClient {
* Check if a user is logged in
*/
isAuthenticated(): boolean {
if (!process.client) { return false }
if (!import.meta.client) {
return false
}
const token = this.getToken()
const user = this.getCurrentUser()
return !!(token && user)
Expand All @@ -123,11 +125,15 @@ export class AuthClient {
* Get current authenticated user
*/
getCurrentUser() {
if (!process.client) { return null }
if (!import.meta.client) {
return null
}

try {
const userJson = localStorage.getItem('user')
if (!userJson) { return null }
if (!userJson) {
return null
}

return JSON.parse(userJson)
}
Expand All @@ -141,15 +147,17 @@ export class AuthClient {
* Get the stored authentication token
*/
getToken(): string | null {
if (!process.client) { return null }
if (!import.meta.client) {
return null
}
return localStorage.getItem('token')
}

/**
* Save token to localStorage
*/
private saveToken(token: string): void {
if (process.client) {
if (import.meta.client) {
localStorage.setItem('token', token)
}
}
Expand All @@ -158,7 +166,7 @@ export class AuthClient {
* Save user to localStorage
*/
private saveUser(user: any): void {
if (process.client) {
if (import.meta.client) {
localStorage.setItem('user', JSON.stringify(user))
}
}
Expand All @@ -167,7 +175,7 @@ export class AuthClient {
* Clear storage (token and user)
*/
private clearStorage(): void {
if (process.client) {
if (import.meta.client) {
localStorage.removeItem('token')
localStorage.removeItem('user')
}
Expand Down
17 changes: 10 additions & 7 deletions src/frontend/components/BadgeIssuanceForm.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<script setup lang="ts">
import { useRuntimeConfig } from '#app'
import { computed, onMounted, ref } from 'vue'
import { apiClient } from '~/api/api-client'
import type { Ref } from 'vue'

interface StrapiImage {
data?: {
Expand Down Expand Up @@ -118,7 +115,7 @@ const props = defineProps<{
initialBadge?: Badge
}>()

const emit = defineEmits<{
defineEmits<{
(e: 'submit', recipients: Recipient[]): void
(e: 'error', message: string): void
}>()
Expand Down Expand Up @@ -168,6 +165,7 @@ function getBadgeImageUrl(badge: Badge): string | undefined {
}

// Computed property to format badge data
/*
const formattedBadges = computed(() => {
return badges.value.map((badge) => {
const attrs = badge.attributes || {}
Expand All @@ -182,6 +180,7 @@ const formattedBadges = computed(() => {
}
})
})
*/

onMounted(async () => {
await loadBadges()
Expand Down Expand Up @@ -326,7 +325,9 @@ async function handleBatchSubmit(recipients: Recipient[]) {

function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement
if (!input.files?.length) { return }
if (!input.files?.length) {
return
}

const file = input.files[0]
csvFile.value = file
Expand All @@ -346,7 +347,9 @@ function handleFileUpload(event: Event) {
const expIdx = header.indexOf('expirationdate')

for (let i = 1; i < rows.length; i++) {
if (!rows[i].trim()) continue
if (!rows[i].trim()) {
continue
}
const row = rows[i].split(',')
const name = row[nameIdx]?.trim()
const email = row[emailIdx]?.trim()
Expand Down Expand Up @@ -713,7 +716,7 @@ function handleCsvButtonClick() {
<NButton size="xs" variant="outline" class="mt-2" @click="handleCsvButtonClick">
Upload CSV File
</NButton>
<input ref="csvInput" type="file" accept=".csv" class="hidden" @change="handleFileUpload" />
<input ref="csvInput" type="file" accept=".csv" class="hidden" @change="handleFileUpload">
</p>
<NAlert v-if="batchError" variant="error" class="mt-4">
{{ batchError }}
Expand Down
11 changes: 7 additions & 4 deletions src/frontend/components/BadgeVerifier.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup>
import { onMounted, ref, watch } from 'vue'
import { apiClient } from '~/api/api-client'

const props = defineProps({
Expand Down Expand Up @@ -52,7 +51,9 @@ function setVerifyMode(mode) {
async function handleFileUpload(event) {
fileError.value = null
const file = event.target.files && event.target.files[0]
if (!file) { return }
if (!file) {
return
}
uploadedFileName.value = file.name
if (file.type !== 'application/json') {
fileError.value = 'Please upload a valid JSON file.'
Expand All @@ -64,7 +65,7 @@ async function handleFileUpload(event) {
// Try to parse for preview
JSON.parse(text)
}
catch (e) {
catch {
fileError.value = 'Invalid JSON file.'
jsonInput.value = ''
}
Expand Down Expand Up @@ -132,7 +133,7 @@ async function verifyByJson() {
try {
credentialData = JSON.parse(jsonInput.value)
}
catch (e) {
catch {
throw new Error('Invalid JSON format. Please check your input.')
}

Expand All @@ -157,6 +158,7 @@ async function verifyByJson() {
}
}

/*
function handleShare() {
if (!badge.value) { return }

Expand All @@ -181,6 +183,7 @@ function handleShare() {
console.error('Error sharing:', err)
}
}
*/
</script>

<template>
Expand Down
Loading