create-nuxt-base 0.3.17 → 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.
- package/.github/workflows/publish.yml +4 -2
- package/.oxfmtrc.jsonc +7 -0
- package/CHANGELOG.md +20 -8
- package/nuxt-base-template/.dockerignore +44 -0
- package/nuxt-base-template/.nuxtrc +1 -0
- package/nuxt-base-template/.oxfmtrc.jsonc +8 -0
- package/nuxt-base-template/Dockerfile.dev +23 -0
- package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +117 -0
- package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +302 -0
- package/nuxt-base-template/app/composables/use-better-auth.ts +25 -0
- package/nuxt-base-template/app/composables/use-file.ts +39 -4
- package/nuxt-base-template/app/composables/use-share.ts +1 -1
- package/nuxt-base-template/app/composables/use-tus-upload.ts +278 -0
- package/nuxt-base-template/app/interfaces/upload.interface.ts +58 -0
- package/nuxt-base-template/app/interfaces/user.interface.ts +12 -0
- package/nuxt-base-template/app/lib/auth-client.ts +135 -0
- package/nuxt-base-template/app/middleware/admin.global.ts +23 -0
- package/nuxt-base-template/app/middleware/auth.global.ts +18 -0
- package/nuxt-base-template/app/middleware/guest.global.ts +18 -0
- package/nuxt-base-template/app/pages/app/settings/security.vue +409 -0
- package/nuxt-base-template/app/pages/auth/2fa.vue +120 -0
- package/nuxt-base-template/app/pages/auth/forgot-password.vue +72 -21
- package/nuxt-base-template/app/pages/auth/login.vue +75 -11
- package/nuxt-base-template/app/pages/auth/register.vue +184 -0
- package/nuxt-base-template/app/pages/auth/reset-password.vue +153 -0
- package/nuxt-base-template/app/utils/crypto.ts +13 -0
- package/nuxt-base-template/docker-entrypoint.sh +21 -0
- package/nuxt-base-template/nuxt.config.ts +4 -1
- package/nuxt-base-template/oxlint.json +14 -0
- package/nuxt-base-template/package-lock.json +11582 -10675
- package/nuxt-base-template/package.json +35 -32
- package/nuxt-base-template/tests/iam.spec.ts +247 -0
- package/package.json +14 -11
- package/.eslintignore +0 -14
- package/.eslintrc +0 -3
- package/.prettierignore +0 -5
- package/.prettierrc +0 -6
- package/nuxt-base-template/CLAUDE.md +0 -361
- package/nuxt-base-template/app/pages/auth/reset-password/[token].vue +0 -110
- package/nuxt-base-template/app/public/favicon.ico +0 -0
- package/nuxt-base-template/eslint.config.mjs +0 -4
|
@@ -8,6 +8,10 @@ on:
|
|
|
8
8
|
env:
|
|
9
9
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
10
10
|
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # Required for OIDC
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
11
15
|
jobs:
|
|
12
16
|
publish:
|
|
13
17
|
runs-on: ubuntu-latest
|
|
@@ -26,8 +30,6 @@ jobs:
|
|
|
26
30
|
|
|
27
31
|
- name: Publish to npm 🚀
|
|
28
32
|
run: npm publish
|
|
29
|
-
env:
|
|
30
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
31
33
|
|
|
32
34
|
- name: Deploy notification
|
|
33
35
|
if: always()
|
package/.oxfmtrc.jsonc
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -2,35 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.2](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.1...v1.0.2) (2026-01-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* add repository field in package.json ([1f43eae](https://github.com/lenneTech/nuxt-base-starter/commit/1f43eae4445b2f8a54cf4442c79be1bd55cf711c))
|
|
11
|
+
|
|
12
|
+
### [1.0.1](https://github.com/lenneTech/nuxt-base-starter/compare/v1.0.0...v1.0.1) (2026-01-12)
|
|
13
|
+
|
|
14
|
+
## [1.0.0](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.17...v1.0.0) (2026-01-12)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **DEV-609:** removed duplicate public folder inside app ([#10](https://github.com/lenneTech/nuxt-base-starter/issues/10)) ([25fe0fe](https://github.com/lenneTech/nuxt-base-starter/commit/25fe0fe3c53bc3400373d9c3f0a4b6705952171b))
|
|
20
|
+
|
|
5
21
|
### [0.3.17](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.16...v0.3.17) (2025-10-17)
|
|
6
22
|
|
|
7
23
|
### [0.3.16](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.15...v0.3.16) (2025-09-05)
|
|
8
24
|
|
|
9
|
-
|
|
10
25
|
### Bug Fixes
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
- Update environment variable for bug feature toggle ([d875b3e](https://github.com/lenneTech/nuxt-base-starter/commit/d875b3eb586368499b9e628cdabd2f971e7cce7f))
|
|
13
28
|
|
|
14
29
|
### [0.3.15](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.14...v0.3.15) (2025-09-05)
|
|
15
30
|
|
|
16
|
-
|
|
17
31
|
### Features
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
- Add Linear API configuration and update dependencies ([291adfd](https://github.com/lenneTech/nuxt-base-starter/commit/291adfd8ee6e2c464c4b7c74e4c27531bda3892e))
|
|
20
34
|
|
|
21
35
|
### [0.3.14](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.13...v0.3.14) (2025-08-01)
|
|
22
36
|
|
|
23
|
-
|
|
24
37
|
### Bug Fixes
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
- **DEV-446:** Replace pnpm with npm for dependency installation and project commands ([8274c25](https://github.com/lenneTech/nuxt-base-starter/commit/8274c255384b841862d2d70a12c19ea30ecbb9d8))
|
|
27
40
|
|
|
28
41
|
### [0.3.13](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.12...v0.3.13) (2025-07-24)
|
|
29
42
|
|
|
30
|
-
|
|
31
43
|
### Bug Fixes
|
|
32
44
|
|
|
33
|
-
|
|
45
|
+
- Update [@lenne](https://github.com/lenne).tech/nuxt-base dependency to latest version ([71dd666](https://github.com/lenneTech/nuxt-base-starter/commit/71dd6669fd9ec25671a102b4d2ad294ac88daa10))
|
|
34
46
|
|
|
35
47
|
### [0.3.12](https://github.com/lenneTech/nuxt-base-starter/compare/v0.3.11...v0.3.12) (2025-07-24)
|
|
36
48
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
.npm
|
|
4
|
+
|
|
5
|
+
# Build output
|
|
6
|
+
.nuxt
|
|
7
|
+
.output
|
|
8
|
+
dist
|
|
9
|
+
|
|
10
|
+
# IDE
|
|
11
|
+
.idea
|
|
12
|
+
.vscode
|
|
13
|
+
*.swp
|
|
14
|
+
*.swo
|
|
15
|
+
|
|
16
|
+
# Logs
|
|
17
|
+
*.log
|
|
18
|
+
npm-debug.log*
|
|
19
|
+
|
|
20
|
+
# Testing
|
|
21
|
+
coverage
|
|
22
|
+
test-results
|
|
23
|
+
playwright-report
|
|
24
|
+
|
|
25
|
+
# Environment
|
|
26
|
+
.env.local
|
|
27
|
+
.env.*.local
|
|
28
|
+
|
|
29
|
+
# Git
|
|
30
|
+
.git
|
|
31
|
+
.gitignore
|
|
32
|
+
|
|
33
|
+
# Docker
|
|
34
|
+
Dockerfile*
|
|
35
|
+
docker-compose*
|
|
36
|
+
.dockerignore
|
|
37
|
+
|
|
38
|
+
# Documentation
|
|
39
|
+
*.md
|
|
40
|
+
!README.md
|
|
41
|
+
|
|
42
|
+
# Misc
|
|
43
|
+
.DS_Store
|
|
44
|
+
*.tgz
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
setups.@nuxt/test-utils="3.23.0"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
FROM node:22-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install dependencies for native modules
|
|
6
|
+
RUN apk add --no-cache python3 make g++
|
|
7
|
+
|
|
8
|
+
# Copy package files first for better caching
|
|
9
|
+
COPY package*.json ./
|
|
10
|
+
COPY .npmrc ./
|
|
11
|
+
|
|
12
|
+
# Install dependencies
|
|
13
|
+
RUN npm ci
|
|
14
|
+
|
|
15
|
+
# Copy entrypoint script
|
|
16
|
+
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
|
17
|
+
RUN chmod +x /docker-entrypoint.sh
|
|
18
|
+
|
|
19
|
+
# Expose port
|
|
20
|
+
EXPOSE 3001
|
|
21
|
+
|
|
22
|
+
ENTRYPOINT ["/docker-entrypoint.sh"]
|
|
23
|
+
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
|
@@ -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,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
|
+
}
|
|
@@ -8,11 +8,13 @@ interface FileInfo {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function useFile() {
|
|
11
|
-
const
|
|
11
|
+
const config = useRuntimeConfig();
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
function isValidMongoID(id: string): boolean {
|
|
14
|
+
return /^[a-f\d]{24}$/i.test(id);
|
|
15
|
+
}
|
|
15
16
|
|
|
17
|
+
async function getFileInfo(id: string | undefined): Promise<FileInfo | null | string> {
|
|
16
18
|
if (!id) {
|
|
17
19
|
return null;
|
|
18
20
|
}
|
|
@@ -23,6 +25,7 @@ export function useFile() {
|
|
|
23
25
|
|
|
24
26
|
try {
|
|
25
27
|
const response = await $fetch<FileInfo>(config.public.host + '/files/info/' + id, {
|
|
28
|
+
credentials: 'include',
|
|
26
29
|
method: 'GET',
|
|
27
30
|
});
|
|
28
31
|
return response;
|
|
@@ -32,5 +35,37 @@ export function useFile() {
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
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`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
formatDuration,
|
|
65
|
+
formatFileSize,
|
|
66
|
+
getDownloadUrl,
|
|
67
|
+
getFileInfo,
|
|
68
|
+
getFileUrl,
|
|
69
|
+
isValidMongoID,
|
|
70
|
+
};
|
|
36
71
|
}
|