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
6 changes: 3 additions & 3 deletions JWT_PDF_API_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 📋 **New Endpoint Created**

### **Endpoint:** `/api/devices/{devEui}/data-jwt-pdf`
### **Endpoint:** `/api/devices/{devEui}/pdf`
- **Method:** `GET`
- **Authentication:** JWT Bearer token required
- **Purpose:** Generate PDF reports for device data (server-to-server)
Expand Down Expand Up @@ -42,7 +42,7 @@ curl -X POST "http://localhost:5173/api/auth/login" \

### **Postman/cURL Example:**
```bash
curl -X GET "http://localhost:5173/api/devices/2CF7F1C0630000AC/data-jwt-pdf?start=2025-05-01&end=2025-06-06" \
curl -X GET "http://localhost:5173/api/devices/2CF7F1C0630000AC/pdf?start=2025-05-01&end=2025-06-06" \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE" \
--output "device-report.pdf"
```
Expand Down Expand Up @@ -104,7 +104,7 @@ curl -X GET "http://localhost:5173/api/devices/2CF7F1C0630000AC/data-jwt-pdf?sta

### **HTTP Request Node Configuration:**
- **Method:** `GET`
- **URL:** `http://your-domain.com/api/devices/{devEui}/data-jwt-pdf?start=2025-05-01&end=2025-06-06`
- **URL:** `http://your-domain.com/api/devices/{devEui}/pdf?start=2025-05-01&end=2025-06-06`
- **Headers:**
```json
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@types/d3": "^7.4.3",
"@types/event-calendar__core": "^4.4.0",
"@types/luxon": "^3.6.2",
"@types/pdfkit": "^0.17.0",
"@types/swagger-ui": "^5.21.1",
"@vite-pwa/sveltekit": "^1.0.0",
"eslint": "^9.18.0",
Expand Down Expand Up @@ -91,7 +92,6 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@sentry/cli",
"esbuild",
"svelte-preprocess"
]
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 0 additions & 89 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,82 +99,6 @@ const handleSupabase: Handle = async ({ event, resolve }) => {
return name === 'content-range' || name === 'x-supabase-api-version';
}
});

// try {
// console.log('Processing JWT token for API request:', event.url.pathname);
// console.log('Token starts with:', jwt.substring(0, 10) + '...');

// // Try different validation approaches for maximum compatibility

// // 1. First try to set the session with both tokens if available
// if (refreshToken) {
// console.log('Using both access and refresh tokens');
// const sessionResult = await event.locals.supabase.auth.setSession({
// access_token: jwt,
// refresh_token: refreshToken
// });

// if (sessionResult.error) {
// console.error('Failed to set session with tokens:', sessionResult.error.message);
// } else if (sessionResult.data?.session && sessionResult.data?.user) {
// console.log(
// 'Successfully set session with tokens for user:',
// sessionResult.data.user.email
// );
// tokenSession = sessionResult.data.session;
// tokenUser = sessionResult.data.user;

// // Set the user in event.locals immediately
// event.locals.user = tokenUser;
// event.locals.session = tokenSession;
// }
// }

// // 2. If that didn't work or no refresh token, try to validate the access token
// if (!tokenUser) {
// console.log('Trying to validate access token directly');
// const { data, error } = await event.locals.supabase.auth.getUser(jwt);

// if (error) {
// console.error('Invalid JWT token:', error.message);
// } else if (data?.user) {
// console.log('Valid JWT token for user:', data.user.email);
// tokenUser = data.user;

// // Get the session
// const sessionResult = await event.locals.supabase.auth.getSession();
// tokenSession = sessionResult.data.session;

// // Set the user in event.locals immediately
// event.locals.user = tokenUser;
// event.locals.session = tokenSession;
// }
// }

// // 3. Last resort: Try to verify the token as an API token
// if (!tokenUser && event.url.pathname.startsWith('/api/')) {
// console.log('Trying to validate as API token for:', event.url.pathname);
// try {
// // For API endpoints, we'll bypass normal authentication for API tokens
// // This is just for testing purposes - in production, you'd verify the token
// // Create a user object that matches the User type from Supabase
// const apiUser = await event.locals.supabase.auth.getUser(jwt);
// if (apiUser.data?.user) {
// tokenUser = apiUser.data.user;
// console.log('Created API user for token access:', apiUser.data.user.email);

// // Set the user in event.locals immediately
// event.locals.user = tokenUser;
// }
// } catch (apiErr) {
// console.error('Failed to create API user:', apiErr);
// }
// }
// } catch (err) {
// console.error('Error processing JWT token:', err);
// }
// } else if (event.url.pathname.startsWith('/api')) {
// console.log('No Authorization token found for API request:', event.url.pathname);
}

// Enhance session validation to include explicit debug logging
Expand Down Expand Up @@ -209,16 +133,8 @@ const handleSupabase: Handle = async ({ event, resolve }) => {
);
return { session: null, user: null };
}

//console.log(
// `Session successfully validated for path: ${event.url.pathname}, user: ${user?.email}`
//);
return { session, user };
} catch (err) {
// console.error(
// `Unexpected error during session validation for path: ${event.url.pathname}:`,
// err
// );
return { session: null, user: null };
}
};
Expand All @@ -242,11 +158,6 @@ const handleSupabase: Handle = async ({ event, resolve }) => {
const authHeader = headers.get('authorization') || headers.get('Authorization');
const apiToken = authHeader?.replace(/^Bearer\s+/i, '').trim();

// console.log(
// 'API route access with token:',
// apiToken ? `${apiToken.substring(0, 10)}...` : 'none'
// );

// If we have a token, validate it before proceeding
if (apiToken) {
// console.log('Validating API token for:', pathname);
Expand Down
168 changes: 168 additions & 0 deletions src/lib/components/devices/ExportButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<script lang="ts">
import Button from '$lib/components/UI/buttons/Button.svelte';
import MaterialIcon from '$lib/components/UI/icons/MaterialIcon.svelte';
import type { ReportAlertPoint } from '$lib/models/Report';
import { neutral } from '$lib/stores/toast.svelte';
import { Dialog } from 'bits-ui';
import { _, locale as appLocale } from 'svelte-i18n';

type ReportType = 'csv' | 'pdf';

type Props = {
devEui: string;
buttonLabel?: string;
disabled?: boolean;
showDatePicker?: boolean;
types?: ReportType[];
startDateInputString?: string;
endDateInputString?: string;
alertPoints?: ReportAlertPoint[];
dataKeys?: string[];
};

let {
devEui,
buttonLabel,
disabled = false,
showDatePicker = true,
types = ['csv', 'pdf'],
startDateInputString = undefined,
endDateInputString = undefined,
alertPoints = [],
dataKeys = []
}: Props = $props();

// Modal open state and date range setup
let modalOpen = $state(false);
let startDate = $state('');
let endDate = $state('');

const today = new Date();
const oneWeekAgo = new Date();

oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);

$effect(() => {
// Initialize date states as ISO strings for native date inputs
startDate = startDateInputString ?? oneWeekAgo.toISOString().split('T')[0];
endDate = endDateInputString ?? today.toISOString().split('T')[0];
});

const startDownload = async (type: ReportType) => {
neutral($_('exporting_data', { values: { type: type.toUpperCase() } }));

const params = new URLSearchParams({
start: startDate,
end: endDate,
alertPoints: JSON.stringify(alertPoints),
dataKeys: dataKeys.join(','),
locale: $appLocale ?? 'ja'
});
const response = await fetch(`/api/devices/${devEui}/${type}?${params}`, {
method: 'GET',
headers: {
'Content-Type': type === 'csv' ? 'text/csv' : 'application/pdf'
}
});

if (!response.ok) {
console.error(`Failed to download ${type} for device ${devEui}:`, response.statusText);
return;
}

const blob = await response.blob();
const urlObj = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = urlObj;
a.download = `${startDate.toString()} - ${endDate.toString()} ${devEui}.${type}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(urlObj);
};
</script>

{#if showDatePicker}
<div class="hidden md:flex md:items-center md:justify-end">
<Dialog.Root bind:open={modalOpen}>
<Dialog.Trigger asChild>
<Button
variant="secondary"
{disabled}
onclick={(event) => {
if (!showDatePicker) {
event.preventDefault;
event.stopPropagation();
startDownload(types[0]);
}
}}
>
<MaterialIcon name="download" />
{buttonLabel ?? $_('export')}
</Button>
</Dialog.Trigger>

<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50" />
<Dialog.Content
class="fixed top-1/2 left-1/2 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded bg-gray-50 p-6 text-zinc-900 shadow-lg dark:bg-zinc-800 dark:text-white"
>
<Dialog.Title class="text-lg font-semibold">{$_('select_date_range')}</Dialog.Title>
<Dialog.Description class="mt-1 text-sm dark:text-gray-400"
>{$_('select_start_end_dates')}</Dialog.Description
>

<div class="mt-4 space-y-4">
<div>
<label for="start-date" class="mb-1 block text-sm font-medium"
>{$_('start_date')}</label
>
<input
id="start-date"
type="date"
bind:value={startDate}
class="w-full rounded border border-gray-300 px-2 py-1"
/>
</div>
<div>
<label for="end-date" class="mb-1 block text-sm font-medium">{$_('end_date')}</label>
<input
id="end-date"
type="date"
bind:value={endDate}
class="w-full rounded border border-gray-300 px-2 py-1"
/>
</div>
</div>

<div class="mt-6 flex justify-end gap-2">
<Button variant="secondary" onclick={() => (modalOpen = false)}>{$_('cancel')}</Button>
{#each types as type}
<Button
variant="primary"
onclick={() => {
startDownload(type);
modalOpen = false;
}}
>
<MaterialIcon name="download" />
{$_(type)}
</Button>
{/each}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
{:else}
<Button
variant="secondary"
{disabled}
onclick={() => {
startDownload(types[0]);
}}
>
<MaterialIcon name="download" />
{buttonLabel ?? $_('export')}
</Button>
{/if}
Loading