create-nuxt-base 0.3.16 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.github/workflows/publish.yml +4 -2
  2. package/.oxfmtrc.jsonc +7 -0
  3. package/CHANGELOG.md +22 -8
  4. package/nuxt-base-template/.dockerignore +44 -0
  5. package/nuxt-base-template/.env.example +1 -1
  6. package/nuxt-base-template/.nuxtrc +1 -0
  7. package/nuxt-base-template/.oxfmtrc.jsonc +8 -0
  8. package/nuxt-base-template/Dockerfile.dev +23 -0
  9. package/nuxt-base-template/README.md +127 -13
  10. package/nuxt-base-template/app/app.config.ts +67 -0
  11. package/nuxt-base-template/app/app.vue +10 -2
  12. package/nuxt-base-template/app/assets/css/tailwind.css +124 -84
  13. package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +117 -0
  14. package/nuxt-base-template/app/components/Modal/ModalBase.vue +65 -0
  15. package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -2
  16. package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -2
  17. package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -2
  18. package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +302 -0
  19. package/nuxt-base-template/app/composables/use-better-auth.ts +25 -0
  20. package/nuxt-base-template/app/composables/use-file.ts +57 -6
  21. package/nuxt-base-template/app/composables/use-share.ts +26 -10
  22. package/nuxt-base-template/app/composables/use-tus-upload.ts +278 -0
  23. package/nuxt-base-template/app/error.vue +7 -43
  24. package/nuxt-base-template/app/interfaces/upload.interface.ts +58 -0
  25. package/nuxt-base-template/app/interfaces/user.interface.ts +12 -0
  26. package/nuxt-base-template/app/layouts/default.vue +76 -4
  27. package/nuxt-base-template/app/layouts/slim.vue +5 -0
  28. package/nuxt-base-template/app/lib/auth-client.ts +135 -0
  29. package/nuxt-base-template/app/middleware/admin.global.ts +20 -6
  30. package/nuxt-base-template/app/middleware/auth.global.ts +15 -6
  31. package/nuxt-base-template/app/middleware/guest.global.ts +18 -0
  32. package/nuxt-base-template/app/pages/app/settings/security.vue +409 -0
  33. package/nuxt-base-template/app/pages/auth/2fa.vue +120 -0
  34. package/nuxt-base-template/app/pages/auth/forgot-password.vue +115 -0
  35. package/nuxt-base-template/app/pages/auth/login.vue +135 -0
  36. package/nuxt-base-template/app/pages/auth/register.vue +184 -0
  37. package/nuxt-base-template/app/pages/auth/reset-password.vue +153 -0
  38. package/nuxt-base-template/app/pages/index.vue +139 -2
  39. package/nuxt-base-template/app/utils/crypto.ts +13 -0
  40. package/nuxt-base-template/docker-entrypoint.sh +21 -0
  41. package/nuxt-base-template/docs/nuxt.config.ts +4 -0
  42. package/nuxt-base-template/docs/pages/docs.vue +663 -0
  43. package/nuxt-base-template/nuxt.config.ts +75 -30
  44. package/nuxt-base-template/openapi-ts.config.ts +18 -0
  45. package/nuxt-base-template/oxlint.json +14 -0
  46. package/nuxt-base-template/package-lock.json +11414 -15883
  47. package/nuxt-base-template/package.json +48 -50
  48. package/nuxt-base-template/tests/iam.spec.ts +247 -0
  49. package/nuxt-base-template/tsconfig.json +1 -1
  50. package/package.json +15 -12
  51. package/.eslintignore +0 -14
  52. package/.eslintrc +0 -3
  53. package/.prettierignore +0 -5
  54. package/.prettierrc +0 -6
  55. package/nuxt-base-template/app/composables/use-context-menu.ts +0 -19
  56. package/nuxt-base-template/app/composables/use-form-helper.ts +0 -41
  57. package/nuxt-base-template/app/composables/use-modal.ts +0 -84
  58. package/nuxt-base-template/app/composables/use-notification.ts +0 -29
  59. package/nuxt-base-template/app/middleware/logged-in.global.ts +0 -9
  60. package/nuxt-base-template/app/plugins/auth.server.ts +0 -72
  61. package/nuxt-base-template/app/plugins/form.plugin.ts +0 -21
  62. package/nuxt-base-template/app/plugins/pwa.plugin.ts +0 -114
  63. package/nuxt-base-template/eslint.config.mjs +0 -3
  64. package/nuxt-base-template/tailwind.config.js +0 -21
@@ -0,0 +1,117 @@
1
+ <script lang="ts" setup>
2
+ // ============================================================================
3
+ // Imports
4
+ // ============================================================================
5
+ import type { FormSubmitEvent } from '@nuxt/ui';
6
+ import type { InferOutput } from 'valibot';
7
+
8
+ import * as v from 'valibot';
9
+
10
+ import { authClient } from '~/lib/auth-client';
11
+
12
+ // ============================================================================
13
+ // Props & Emits
14
+ // ============================================================================
15
+ interface Props {
16
+ initialCodes?: string[];
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ initialCodes: () => [],
21
+ });
22
+
23
+ const emit = defineEmits<{
24
+ close: [result?: unknown];
25
+ }>();
26
+
27
+ // ============================================================================
28
+ // Composables
29
+ // ============================================================================
30
+ const toast = useToast();
31
+
32
+ // ============================================================================
33
+ // Variables
34
+ // ============================================================================
35
+ const loading = ref<boolean>(false);
36
+ const backupCodes = ref<string[]>(props.initialCodes);
37
+
38
+ const passwordSchema = v.object({
39
+ password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(1, 'Passwort ist erforderlich')),
40
+ });
41
+
42
+ type PasswordSchema = InferOutput<typeof passwordSchema>;
43
+
44
+ // ============================================================================
45
+ // Functions
46
+ // ============================================================================
47
+ function copyBackupCodes(): void {
48
+ navigator.clipboard.writeText(backupCodes.value.join('\n'));
49
+ toast.add({
50
+ color: 'success',
51
+ description: 'Backup-Codes wurden in die Zwischenablage kopiert',
52
+ title: 'Kopiert',
53
+ });
54
+ }
55
+
56
+ async function generateBackupCodes(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
57
+ loading.value = true;
58
+
59
+ try {
60
+ const { data, error } = await authClient.twoFactor.generateBackupCodes({
61
+ password: payload.data.password,
62
+ });
63
+
64
+ if (error) {
65
+ toast.add({
66
+ color: 'error',
67
+ description: error.message || 'Backup-Codes konnten nicht generiert werden',
68
+ title: 'Fehler',
69
+ });
70
+ return;
71
+ }
72
+
73
+ backupCodes.value = data?.backupCodes ?? [];
74
+ } finally {
75
+ loading.value = false;
76
+ }
77
+ }
78
+
79
+ function handleClose(): void {
80
+ emit('close');
81
+ }
82
+ </script>
83
+
84
+ <template>
85
+ <UModal title="Backup-Codes" :close="{ onClick: handleClose }">
86
+ <template #body>
87
+ <div class="space-y-4">
88
+ <div class="flex items-center gap-3">
89
+ <UIcon name="i-lucide-key" class="size-6 text-warning" />
90
+ <p class="text-sm text-muted">Bewahre diese Codes sicher auf. Jeder Code kann nur einmal verwendet werden.</p>
91
+ </div>
92
+
93
+ <template v-if="backupCodes.length > 0">
94
+ <div class="grid grid-cols-2 gap-2 rounded-lg bg-muted/50 p-4 font-mono text-sm">
95
+ <span v-for="code in backupCodes" :key="code">{{ code }}</span>
96
+ </div>
97
+ </template>
98
+
99
+ <template v-else>
100
+ <UForm :schema="passwordSchema" class="space-y-4" @submit="generateBackupCodes">
101
+ <UFormField label="Passwort" name="password">
102
+ <UInput name="password" type="password" placeholder="Dein Passwort" />
103
+ </UFormField>
104
+ <UButton type="submit" block :loading="loading"> Neue Backup-Codes generieren </UButton>
105
+ </UForm>
106
+ </template>
107
+ </div>
108
+ </template>
109
+
110
+ <template #footer>
111
+ <div class="flex justify-end gap-3">
112
+ <UButton v-if="backupCodes.length > 0" variant="outline" icon="i-lucide-copy" @click="copyBackupCodes"> Codes kopieren </UButton>
113
+ <UButton color="neutral" variant="outline" @click="handleClose"> Schließen </UButton>
114
+ </div>
115
+ </template>
116
+ </UModal>
117
+ </template>
@@ -0,0 +1,65 @@
1
+ <script lang="ts" setup>
2
+ // ============================================================================
3
+ // Props & Emits
4
+ // ============================================================================
5
+ interface Props {
6
+ description?: string;
7
+ title?: string;
8
+ }
9
+
10
+ withDefaults(defineProps<Props>(), {
11
+ description: 'This is a base modal example using Nuxt UI. You can customize this component to fit your needs.',
12
+ title: 'Modal Example',
13
+ });
14
+
15
+ const emit = defineEmits<{
16
+ close: [result?: unknown];
17
+ }>();
18
+
19
+ // ============================================================================
20
+ // Functions
21
+ // ============================================================================
22
+ function handleCancel(): void {
23
+ emit('close', false);
24
+ }
25
+
26
+ function handleConfirm(): void {
27
+ emit('close', true);
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <UModal :title="title" :description="description" :close="{ onClick: () => emit('close', false) }">
33
+ <template #body>
34
+ <div class="space-y-4">
35
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
36
+ This modal demonstrates how to use the <code class="px-1.5 py-0.5 bg-neutral-100 dark:bg-neutral-800 rounded text-xs">useOverlay</code> composable to open modals
37
+ programmatically.
38
+ </p>
39
+ <div class="rounded-lg bg-neutral-50 dark:bg-neutral-900 p-4">
40
+ <h4 class="text-sm font-medium text-neutral-900 dark:text-white mb-2">Key Features:</h4>
41
+ <ul class="space-y-2 text-sm text-neutral-600 dark:text-neutral-400">
42
+ <li class="flex items-start gap-2">
43
+ <UIcon name="i-lucide-check" class="size-4 mt-0.5 text-success shrink-0" />
44
+ <span>Programmatic control with useOverlay()</span>
45
+ </li>
46
+ <li class="flex items-start gap-2">
47
+ <UIcon name="i-lucide-check" class="size-4 mt-0.5 text-success shrink-0" />
48
+ <span>Return values via emit('close')</span>
49
+ </li>
50
+ <li class="flex items-start gap-2">
51
+ <UIcon name="i-lucide-check" class="size-4 mt-0.5 text-success shrink-0" />
52
+ <span>Fully typed props and emits</span>
53
+ </li>
54
+ </ul>
55
+ </div>
56
+ </div>
57
+ </template>
58
+ <template #footer>
59
+ <div class="flex justify-end gap-3">
60
+ <UButton color="neutral" variant="outline" @click="handleCancel"> Cancel </UButton>
61
+ <UButton color="primary" @click="handleConfirm"> Confirm </UButton>
62
+ </div>
63
+ </template>
64
+ </UModal>
65
+ </template>
@@ -1,5 +1,3 @@
1
- <script setup lang="ts"></script>
2
-
3
1
  <template>
4
2
  <Transition
5
3
  enter-active-class="transition ease-out duration-500"
@@ -1,5 +1,3 @@
1
- <script setup lang="ts"></script>
2
-
3
1
  <template>
4
2
  <Transition
5
3
  enter-active-class="transition ease-out duration-500"
@@ -1,5 +1,3 @@
1
- <script setup lang="ts"></script>
2
-
3
1
  <template>
4
2
  <Transition
5
3
  enter-active-class="transition ease-out duration-500"
@@ -0,0 +1,302 @@
1
+ <script setup lang="ts">
2
+ import type { UploadItem } from '~/interfaces/upload.interface';
3
+
4
+ // ============================================================================
5
+ // Props & Emits
6
+ // ============================================================================
7
+ interface Props {
8
+ /** Erlaubte Dateitypen (MIME types oder Extensions) */
9
+ accept?: string;
10
+ /** Upload automatisch starten */
11
+ autoStart?: boolean;
12
+ /** Chunk-Groesse fuer TUS */
13
+ chunkSize?: number;
14
+ /** Beschreibung fuer Dropzone */
15
+ description?: string;
16
+ /** Deaktiviert */
17
+ disabled?: boolean;
18
+ /** TUS Upload Endpoint */
19
+ endpoint?: string;
20
+ /** Label fuer Dropzone */
21
+ label?: string;
22
+ /** Layout der Dateiliste */
23
+ layout?: 'grid' | 'list';
24
+ /** Maximale Dateigroesse in Bytes */
25
+ maxSize?: number;
26
+ /** Zusaetzliche Metadaten */
27
+ metadata?: Record<string, string>;
28
+ /** Mehrere Dateien erlauben */
29
+ multiple?: boolean;
30
+ /** Parallele Uploads */
31
+ parallelUploads?: number;
32
+ }
33
+
34
+ const props = withDefaults(defineProps<Props>(), {
35
+ accept: '*/*',
36
+ autoStart: true,
37
+ chunkSize: 5 * 1024 * 1024,
38
+ description: 'Dateien werden automatisch hochgeladen',
39
+ label: 'Dateien hier ablegen',
40
+ layout: 'list',
41
+ maxSize: 100 * 1024 * 1024,
42
+ multiple: true,
43
+ parallelUploads: 3,
44
+ });
45
+
46
+ const emit = defineEmits<{
47
+ /** Alle Uploads abgeschlossen */
48
+ complete: [items: UploadItem[]];
49
+ /** Upload-Fehler */
50
+ error: [item: UploadItem, error: Error];
51
+ /** Ein Upload abgeschlossen */
52
+ success: [item: UploadItem];
53
+ /** Files geaendert */
54
+ 'update:modelValue': [files: File[]];
55
+ }>();
56
+
57
+ // ============================================================================
58
+ // Composables
59
+ // ============================================================================
60
+ const toast = useToast();
61
+ const { formatDuration, formatFileSize } = useFile();
62
+
63
+ // ============================================================================
64
+ // TUS Upload Setup
65
+ // ============================================================================
66
+ const { addFiles, cancelAll, cancelUpload, clearCompleted, isUploading, pauseAll, pauseUpload, resumeAll, resumeUpload, retryUpload, totalProgress, uploads } = useTusUpload({
67
+ autoStart: props.autoStart,
68
+ chunkSize: props.chunkSize,
69
+ endpoint: props.endpoint,
70
+ metadata: props.metadata,
71
+ onError: (item, error) => {
72
+ toast.add({
73
+ color: 'error',
74
+ description: item.file.name,
75
+ title: 'Upload fehlgeschlagen',
76
+ });
77
+ emit('error', item, error);
78
+ },
79
+ onSuccess: (item) => {
80
+ toast.add({
81
+ color: 'success',
82
+ description: item.file.name,
83
+ title: 'Upload abgeschlossen',
84
+ });
85
+ emit('success', item);
86
+ },
87
+ parallelUploads: props.parallelUploads,
88
+ });
89
+
90
+ // ============================================================================
91
+ // State
92
+ // ============================================================================
93
+ const files = ref<File[]>([]);
94
+
95
+ // ============================================================================
96
+ // Watchers
97
+ // ============================================================================
98
+ // Neue Dateien zu TUS hinzufuegen
99
+ watch(
100
+ files,
101
+ (newFiles, oldFiles) => {
102
+ const addedFiles = newFiles.filter((f) => !oldFiles?.includes(f));
103
+ if (addedFiles.length > 0) {
104
+ // Validierung
105
+ const validFiles = addedFiles.filter((file) => {
106
+ if (file.size > props.maxSize) {
107
+ toast.add({
108
+ color: 'error',
109
+ description: `${file.name} ist zu gross (max. ${formatFileSize(props.maxSize)})`,
110
+ title: 'Datei zu gross',
111
+ });
112
+ return false;
113
+ }
114
+ return true;
115
+ });
116
+
117
+ if (validFiles.length > 0) {
118
+ addFiles(validFiles);
119
+ }
120
+ }
121
+ emit('update:modelValue', newFiles);
122
+ },
123
+ { deep: true },
124
+ );
125
+
126
+ // Emit complete wenn alle fertig
127
+ const completedUploads = computed(() => uploads.value.filter((u) => u.status === 'completed'));
128
+ watch([completedUploads, isUploading], ([completed, uploading]) => {
129
+ if (completed.length > 0 && !uploading && uploads.value.every((u) => u.status === 'completed' || u.status === 'error')) {
130
+ emit('complete', completed);
131
+ }
132
+ });
133
+
134
+ function getStatusColor(status: string): 'error' | 'neutral' | 'success' | 'warning' {
135
+ switch (status) {
136
+ case 'completed':
137
+ return 'success';
138
+ case 'error':
139
+ return 'error';
140
+ case 'paused':
141
+ return 'warning';
142
+ default:
143
+ return 'neutral';
144
+ }
145
+ }
146
+
147
+ function getStatusLabel(status: string): string {
148
+ switch (status) {
149
+ case 'completed':
150
+ return 'Fertig';
151
+ case 'error':
152
+ return 'Fehler';
153
+ case 'paused':
154
+ return 'Pausiert';
155
+ case 'uploading':
156
+ return 'Hochladen';
157
+ default:
158
+ return 'Wartend';
159
+ }
160
+ }
161
+
162
+ // ============================================================================
163
+ // Helpers
164
+ // ============================================================================
165
+ function getUploadForFile(file: File, index: number): undefined | UploadItem {
166
+ // Versuche ueber Index zu matchen (funktioniert wenn Reihenfolge gleich)
167
+ const upload = uploads.value[index];
168
+ if (upload?.file.name === file.name && upload?.file.size === file.size) {
169
+ return upload;
170
+ }
171
+ // Fallback: Suche ueber Name und Groesse
172
+ return uploads.value.find((u) => u.file.name === file.name && u.file.size === file.size);
173
+ }
174
+
175
+ // ============================================================================
176
+ // Expose fuer Parent-Zugriff
177
+ // ============================================================================
178
+ defineExpose({
179
+ cancelAll,
180
+ clearCompleted,
181
+ isUploading,
182
+ pauseAll,
183
+ resumeAll,
184
+ totalProgress,
185
+ uploads,
186
+ });
187
+ </script>
188
+
189
+ <template>
190
+ <div class="space-y-4">
191
+ <!-- Nuxt UI FileUpload -->
192
+ <UFileUpload
193
+ v-model="files"
194
+ :accept="accept"
195
+ :description="description"
196
+ :disabled="disabled || isUploading"
197
+ :dropzone="!disabled"
198
+ :label="label"
199
+ :layout="layout"
200
+ :multiple="multiple"
201
+ class="w-full"
202
+ >
203
+ <!-- Custom File Slot mit TUS Progress -->
204
+ <template #file="{ file, index }">
205
+ <div class="flex w-full items-center gap-3 rounded-lg border border-default bg-default p-3">
206
+ <UIcon name="i-lucide-file" class="size-6 shrink-0 text-muted" />
207
+
208
+ <div class="min-w-0 flex-1">
209
+ <p class="truncate text-sm font-medium">{{ file.name }}</p>
210
+
211
+ <template v-if="getUploadForFile(file, index)">
212
+ <div class="mt-0.5 flex items-center gap-2 text-xs text-muted">
213
+ <span>{{ formatFileSize(file.size) }}</span>
214
+
215
+ <template v-if="getUploadForFile(file, index)?.status === 'uploading'">
216
+ <span>-</span>
217
+ <span>{{ getUploadForFile(file, index)?.progress.percentage }}%</span>
218
+ <span>-</span>
219
+ <span>{{ formatFileSize(getUploadForFile(file, index)?.progress.speed || 0) }}/s</span>
220
+ <span>-</span>
221
+ <span>{{ formatDuration(getUploadForFile(file, index)?.progress.remainingTime || 0) }}</span>
222
+ </template>
223
+
224
+ <template v-else>
225
+ <span>-</span>
226
+ <UBadge :color="getStatusColor(getUploadForFile(file, index)?.status || 'idle')" size="xs" variant="subtle">
227
+ {{ getStatusLabel(getUploadForFile(file, index)?.status || 'idle') }}
228
+ </UBadge>
229
+ </template>
230
+ </div>
231
+
232
+ <UProgress
233
+ v-if="getUploadForFile(file, index)?.status === 'uploading' || getUploadForFile(file, index)?.status === 'paused'"
234
+ :color="getUploadForFile(file, index)?.status === 'paused' ? 'warning' : 'primary'"
235
+ :value="getUploadForFile(file, index)?.progress.percentage || 0"
236
+ class="mt-2"
237
+ size="xs"
238
+ />
239
+ </template>
240
+
241
+ <template v-else>
242
+ <p class="text-xs text-muted">{{ formatFileSize(file.size) }}</p>
243
+ </template>
244
+ </div>
245
+
246
+ <!-- Actions -->
247
+ <div class="flex shrink-0 gap-0.5">
248
+ <UButton
249
+ v-if="getUploadForFile(file, index)?.status === 'uploading'"
250
+ color="neutral"
251
+ icon="i-lucide-pause"
252
+ size="xs"
253
+ variant="ghost"
254
+ @click.stop="pauseUpload(getUploadForFile(file, index)!.id)"
255
+ />
256
+ <UButton
257
+ v-if="getUploadForFile(file, index)?.status === 'paused'"
258
+ color="primary"
259
+ icon="i-lucide-play"
260
+ size="xs"
261
+ variant="ghost"
262
+ @click.stop="resumeUpload(getUploadForFile(file, index)!.id)"
263
+ />
264
+ <UButton
265
+ v-if="getUploadForFile(file, index)?.status === 'error'"
266
+ color="warning"
267
+ icon="i-lucide-refresh-cw"
268
+ size="xs"
269
+ variant="ghost"
270
+ @click.stop="retryUpload(getUploadForFile(file, index)!.id)"
271
+ />
272
+ <UButton
273
+ v-if="getUploadForFile(file, index)?.status !== 'completed'"
274
+ color="error"
275
+ icon="i-lucide-x"
276
+ size="xs"
277
+ variant="ghost"
278
+ @click.stop="cancelUpload(getUploadForFile(file, index)!.id)"
279
+ />
280
+ </div>
281
+ </div>
282
+ </template>
283
+
284
+ <!-- Actions Slot fuer Bulk-Actions -->
285
+ <template #files-bottom="{ files: fileList }">
286
+ <div v-if="fileList && Array.isArray(fileList) && fileList.length > 1 && uploads.length > 0" class="flex items-center justify-between border-t border-default pt-2">
287
+ <div class="text-xs text-muted">
288
+ <template v-if="isUploading"> {{ totalProgress.percentage }}% - {{ formatFileSize(totalProgress.speed) }}/s </template>
289
+ <template v-else> {{ uploads.filter((u) => u.status === 'completed').length }}/{{ uploads.length }} abgeschlossen </template>
290
+ </div>
291
+
292
+ <div class="flex gap-1">
293
+ <UButton v-if="isUploading" color="neutral" size="xs" variant="ghost" @click="pauseAll"> Alle pausieren </UButton>
294
+ <UButton v-if="uploads.some((u) => u.status === 'paused')" color="primary" size="xs" variant="ghost" @click="resumeAll"> Alle fortsetzen </UButton>
295
+ <UButton v-if="uploads.some((u) => u.status === 'completed')" color="neutral" size="xs" variant="ghost" @click="clearCompleted"> Fertige entfernen </UButton>
296
+ <UButton v-if="uploads.length > 0" color="error" size="xs" variant="ghost" @click="cancelAll"> Alle abbrechen </UButton>
297
+ </div>
298
+ </div>
299
+ </template>
300
+ </UFileUpload>
301
+ </div>
302
+ </template>
@@ -0,0 +1,25 @@
1
+ import { authClient } from '~/lib/auth-client';
2
+
3
+ export function useBetterAuth() {
4
+ const session = authClient.useSession();
5
+
6
+ const user = computed<null | User>(() => (session.value.data?.user as User) ?? null);
7
+ const isAuthenticated = computed<boolean>(() => !!user.value);
8
+ const isAdmin = computed<boolean>(() => user.value?.role === 'admin');
9
+ const is2FAEnabled = computed<boolean>(() => user.value?.twoFactorEnabled ?? false);
10
+ const isLoading = computed<boolean>(() => session.value.isPending);
11
+
12
+ return {
13
+ is2FAEnabled,
14
+ isAdmin,
15
+ isAuthenticated,
16
+ isLoading,
17
+ passkey: authClient.passkey,
18
+ session,
19
+ signIn: authClient.signIn,
20
+ signOut: authClient.signOut,
21
+ signUp: authClient.signUp,
22
+ twoFactor: authClient.twoFactor,
23
+ user,
24
+ };
25
+ }
@@ -1,10 +1,20 @@
1
- import { useFetch } from '@vueuse/core';
1
+ interface FileInfo {
2
+ [key: string]: unknown;
3
+ filename: string;
4
+ id: string;
5
+ mimetype: string;
6
+ size: number;
7
+ url?: string;
8
+ }
2
9
 
3
10
  export function useFile() {
4
- const { isValidMongoID } = useHelper();
5
- async function getFileInfo(id: string | undefined): Promise<any> {
6
- const config = useRuntimeConfig();
11
+ const config = useRuntimeConfig();
12
+
13
+ function isValidMongoID(id: string): boolean {
14
+ return /^[a-f\d]{24}$/i.test(id);
15
+ }
7
16
 
17
+ async function getFileInfo(id: string | undefined): Promise<FileInfo | null | string> {
8
18
  if (!id) {
9
19
  return null;
10
20
  }
@@ -13,8 +23,49 @@ export function useFile() {
13
23
  return id;
14
24
  }
15
25
 
16
- return useFetch<File>(config.public.apiUrl + '/files/info/' + id, { method: 'GET' });
26
+ try {
27
+ const response = await $fetch<FileInfo>(config.public.host + '/files/info/' + id, {
28
+ credentials: 'include',
29
+ method: 'GET',
30
+ });
31
+ return response;
32
+ } catch (error) {
33
+ console.error('Error fetching file info:', error);
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function getFileUrl(id: string): string {
39
+ return `${config.public.host}/files/${id}`;
40
+ }
41
+
42
+ function getDownloadUrl(id: string, filename?: string): string {
43
+ const base = `${config.public.host}/files/download/${id}`;
44
+ return filename ? `${base}?filename=${encodeURIComponent(filename)}` : base;
45
+ }
46
+
47
+ function formatFileSize(bytes: number): string {
48
+ if (bytes === 0) return '0 B';
49
+ const k = 1024;
50
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
51
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
52
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
53
+ }
54
+
55
+ function formatDuration(seconds: number): string {
56
+ if (seconds < 60) return `${seconds}s`;
57
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
58
+ const hours = Math.floor(seconds / 3600);
59
+ const minutes = Math.floor((seconds % 3600) / 60);
60
+ return `${hours}h ${minutes}m`;
17
61
  }
18
62
 
19
- return { getFileInfo };
63
+ return {
64
+ formatDuration,
65
+ formatFileSize,
66
+ getDownloadUrl,
67
+ getFileInfo,
68
+ getFileUrl,
69
+ isValidMongoID,
70
+ };
20
71
  }
@@ -1,18 +1,34 @@
1
- import ModalShare from '~/components/ModalShare.vue';
2
- import { useModal } from '~/composables/use-modal';
3
-
4
1
  export function useShare() {
5
2
  const route = useRoute();
6
3
 
7
- function share(title?: string, text?: string, url?: string) {
4
+ async function share(title?: string, text?: string, url?: string) {
5
+ if (!import.meta.client) {
6
+ return;
7
+ }
8
+
8
9
  if (window?.navigator?.share) {
9
- window.navigator.share({
10
- text: text ?? window.location.origin,
11
- title: title,
12
- url: url ?? route.fullPath,
13
- });
10
+ try {
11
+ await window.navigator.share({
12
+ text: text ?? window.location.origin,
13
+ title: title,
14
+ url: url ?? route.fullPath,
15
+ });
16
+ } catch (error) {
17
+ console.error('Error sharing:', error);
18
+ }
14
19
  } else {
15
- useModal().open({ component: ModalShare, data: { link: url ?? window.location.origin, name: window.name }, size: 'md' });
20
+ // Fallback: Copy to clipboard
21
+ try {
22
+ await navigator.clipboard.writeText(url ?? window.location.origin);
23
+ const toast = useToast();
24
+ toast.add({
25
+ color: 'success',
26
+ description: 'Der Link wurde in die Zwischenablage kopiert.',
27
+ title: 'Link kopiert',
28
+ });
29
+ } catch (error) {
30
+ console.error('Error copying to clipboard:', error);
31
+ }
16
32
  }
17
33
  }
18
34