maxibase 0.0.0-stage → 1.0.1

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 (115) hide show
  1. package/README.md +270 -2
  2. package/docker/Dockerfile +22 -0
  3. package/docker/Dockerfile.dockerignore +25 -0
  4. package/docker/compose.yaml +19 -0
  5. package/layers/base/app/app.vue +22 -0
  6. package/layers/base/app/components/AuthCheck.vue +316 -0
  7. package/layers/base/app/components/DataExplorer.vue +38 -0
  8. package/layers/base/app/components/LocaleSwitcher.vue +28 -0
  9. package/layers/base/app/components/TheConfirmation.vue +48 -0
  10. package/layers/base/app/components/TheLoading.vue +36 -0
  11. package/layers/base/app/components/TheNotifications.vue +71 -0
  12. package/layers/base/app/components/files/FilesDelete.vue +33 -0
  13. package/layers/base/app/components/files/FilesUpload.vue +85 -0
  14. package/layers/base/app/error.vue +28 -0
  15. package/layers/base/app/layouts/default.vue +15 -0
  16. package/layers/base/app/layouts/simple.vue +3 -0
  17. package/layers/base/app/middleware/admin.ts +15 -0
  18. package/layers/base/app/middleware/authenticated.ts +10 -0
  19. package/layers/base/app/middleware/mfa.global.ts +8 -0
  20. package/layers/base/app/pages/admin/logs.vue +364 -0
  21. package/layers/base/app/pages/signin.vue +121 -0
  22. package/layers/base/app/pages/user/auth.vue +509 -0
  23. package/layers/base/app/plugins/error.ts +21 -0
  24. package/layers/base/app/plugins/session.ts +22 -0
  25. package/layers/base/app/stores/app.ts +100 -0
  26. package/layers/base/app/utils/clipboard.ts +6 -0
  27. package/layers/base/app/utils/files.ts +95 -0
  28. package/layers/base/app/utils/image.ts +33 -0
  29. package/layers/base/nuxt.config.ts +146 -0
  30. package/layers/base/public/images/auth/microsoft.svg +1 -0
  31. package/layers/base/public/robots.txt +2 -0
  32. package/layers/base/server/api/admin/logs.get.ts +49 -0
  33. package/layers/base/server/api/auth/anonymous.post.ts +31 -0
  34. package/layers/base/server/api/auth/email/get.post.ts +46 -0
  35. package/layers/base/server/api/auth/email/verify.post.ts +46 -0
  36. package/layers/base/server/api/auth/index.get.ts +10 -0
  37. package/layers/base/server/api/auth/index.post.ts +19 -0
  38. package/layers/base/server/api/auth/options.post.ts +26 -0
  39. package/layers/base/server/api/auth/otp/get.post.ts +74 -0
  40. package/layers/base/server/api/auth/otp/verify.post.ts +70 -0
  41. package/layers/base/server/api/auth/password.post.ts +42 -0
  42. package/layers/base/server/api/auth/refresh.delete.ts +4 -0
  43. package/layers/base/server/api/auth/refresh.post.ts +5 -0
  44. package/layers/base/server/api/auth/totp/confirm.post.ts +26 -0
  45. package/layers/base/server/api/auth/totp/disable.post.ts +33 -0
  46. package/layers/base/server/api/auth/totp/generate.get.ts +6 -0
  47. package/layers/base/server/api/auth/totp/verify.post.ts +36 -0
  48. package/layers/base/server/api/auth/webauthn/authenticate.post.ts +39 -0
  49. package/layers/base/server/api/auth/webauthn/index.delete.ts +43 -0
  50. package/layers/base/server/api/auth/webauthn/index.post.ts +17 -0
  51. package/layers/base/server/api/auth/webauthn/register.post.ts +49 -0
  52. package/layers/base/server/api/files/[...path].get.ts +37 -0
  53. package/layers/base/server/api/files/chunk.post.ts +36 -0
  54. package/layers/base/server/api/files/copy.post.ts +22 -0
  55. package/layers/base/server/api/files/index.delete.ts +12 -0
  56. package/layers/base/server/api/files/index.get.ts +19 -0
  57. package/layers/base/server/api/files/index.post.ts +15 -0
  58. package/layers/base/server/api/log.post.ts +20 -0
  59. package/layers/base/server/database/defaults/access.ts +14 -0
  60. package/layers/base/server/database/defaults/db.ts +13 -0
  61. package/layers/base/server/database/defaults/relations.ts +5 -0
  62. package/layers/base/server/database/relations.ts +35 -0
  63. package/layers/base/server/database/schema.ts +66 -0
  64. package/layers/base/server/middleware/0.rateLimit.ts +21 -0
  65. package/layers/base/server/middleware/1.session.ts +14 -0
  66. package/layers/base/server/middleware/2.mfa.ts +34 -0
  67. package/layers/base/server/middleware/3.admin.ts +4 -0
  68. package/layers/base/server/plugins/session.ts +9 -0
  69. package/layers/base/server/routes/auth/microsoft.get.ts +21 -0
  70. package/layers/base/server/routes/dev/email/[template].ts +15 -0
  71. package/layers/base/server/tasks/backup.ts +42 -0
  72. package/layers/base/server/tasks/clean.ts +39 -0
  73. package/layers/base/server/utils/access.ts +8 -0
  74. package/layers/base/server/utils/auth.ts +207 -0
  75. package/layers/base/server/utils/backup.ts +98 -0
  76. package/layers/base/server/utils/db.ts +3 -0
  77. package/layers/base/server/utils/email.ts +138 -0
  78. package/layers/base/server/utils/files.ts +751 -0
  79. package/layers/base/server/utils/ip.ts +15 -0
  80. package/layers/base/server/utils/log.ts +31 -0
  81. package/layers/base/server/utils/rateLimit.ts +69 -0
  82. package/layers/base/server/utils/schemas.ts +8 -0
  83. package/layers/base/server/utils/text.ts +4 -0
  84. package/layers/base/server/utils/time.ts +3 -0
  85. package/layers/base/shared/types/auth.d.ts +22 -0
  86. package/layers/base/shared/utils/logs.ts +9 -0
  87. package/layers/base/shared/utils/url.ts +5 -0
  88. package/locales/en.json +33 -0
  89. package/locales/fr.json +33 -0
  90. package/locales/pages/admin-logs/en.json +1 -0
  91. package/locales/pages/admin-logs/fr.json +1 -0
  92. package/locales/pages/index/en.json +1 -0
  93. package/locales/pages/index/fr.json +1 -0
  94. package/locales/pages/signin/en.json +5 -0
  95. package/locales/pages/signin/fr.json +5 -0
  96. package/locales/pages/user-auth/en.json +22 -0
  97. package/locales/pages/user-auth/fr.json +22 -0
  98. package/nuxt.config.ts +54 -0
  99. package/package.json +92 -4
  100. package/scripts/db/migrate.ts +9 -0
  101. package/scripts/db/push-server.sh +45 -0
  102. package/scripts/db/restore.sh +26 -0
  103. package/scripts/deploy.sh +34 -0
  104. package/scripts/env.sh +20 -0
  105. package/scripts/remote/switch.sh +48 -0
  106. package/scripts/rollback.sh +12 -0
  107. package/server/assets/emails/base.html +43 -0
  108. package/server/assets/emails/en/base.html +1 -0
  109. package/server/assets/emails/en/emailChange.html +4 -0
  110. package/server/assets/emails/en/emailChanged.html +2 -0
  111. package/server/assets/emails/en/otp.html +10 -0
  112. package/server/assets/emails/fr/base.html +1 -0
  113. package/server/assets/emails/fr/emailChange.html +4 -0
  114. package/server/assets/emails/fr/emailChanged.html +2 -0
  115. package/server/assets/emails/fr/otp.html +10 -0
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ const AsyncDataExplorer = defineAsyncComponent(() => import('./DataExplorer.vue'))
3
+
4
+ defineProps<{
5
+ data: any
6
+ }>()
7
+
8
+ const openKeys = ref(new Set<string | number>())
9
+
10
+ function toggle(key: string | number) {
11
+ if (openKeys.value.has(key)) openKeys.value.delete(key)
12
+ else openKeys.value.add(key)
13
+ }
14
+ </script>
15
+
16
+ <template>
17
+ <div v-if="typeof data === 'object'">
18
+ <div
19
+ v-for="(value, key) in data"
20
+ :key="key"
21
+ >
22
+ <div
23
+ :class="openKeys.has(key) ? '' : 'line'"
24
+ style="cursor: pointer"
25
+ @click="toggle(key)"
26
+ >
27
+ {{ key }}:
28
+ <span v-if="!openKeys.has(key) || typeof value !== 'object'">{{ value }}</span>
29
+ </div>
30
+ <AsyncDataExplorer
31
+ v-if="typeof value === 'object' && openKeys.has(key)"
32
+ :data="value"
33
+ class="bl pl1"
34
+ />
35
+ </div>
36
+ </div>
37
+ <span v-else>{{ data }}</span>
38
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup>
2
+ const { $getLocales, $switchLocale, $getLocale } = useI18n()
3
+ </script>
4
+
5
+ <template>
6
+ <select
7
+ v-if="$getLocales().length > 1"
8
+ class="locales"
9
+ :aria-label="$tc('language', 2)"
10
+ :id="$tc('language', 2)"
11
+ @change="$switchLocale($event.target.value)"
12
+ >
13
+ <option
14
+ v-for="locale in $getLocales()"
15
+ :key="locale.code"
16
+ :value="locale.code"
17
+ :selected="locale.code === $getLocale()"
18
+ >
19
+ {{ locale.code.toUpperCase() }}
20
+ </option>
21
+ </select>
22
+ </template>
23
+
24
+ <style scoped>
25
+ .locales {
26
+ width: 4em;
27
+ }
28
+ </style>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+
4
+ const app = useAppStore()
5
+ const isWorking = ref()
6
+
7
+ async function confirm() {
8
+ if (isWorking.value) return
9
+ isWorking.value = true
10
+ app.confirmation!.resolve(true)
11
+ isWorking.value = false
12
+ }
13
+
14
+ function cancel() {
15
+ app.confirmation!.resolve(false)
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <Transition name="fade">
21
+ <div
22
+ v-if="app.confirmation"
23
+ class="modal"
24
+ >
25
+ <div
26
+ class="p2"
27
+ style="max-width: 600px"
28
+ >
29
+ {{ app.confirmation.message || $t('areYouSure') }}
30
+ <div class="flex-row g2 mt1">
31
+ <button
32
+ :class="['flex-1', isWorking ? 'spin' : '']"
33
+ @click="confirm"
34
+ >
35
+ {{ $t('confirm') }}
36
+ </button>
37
+ <button
38
+ class="flex-1"
39
+ :disabled="isWorking"
40
+ @click="cancel"
41
+ >
42
+ {{ $t('cancel') }}
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </Transition>
48
+ </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ const appStore = useAppStore()
3
+ </script>
4
+
5
+ <template>
6
+ <transition
7
+ name="loading"
8
+ mode="out-in"
9
+ >
10
+ <div
11
+ v-if="appStore.isLoading"
12
+ class="loading primary spin bottom left shadow"
13
+ />
14
+ </transition>
15
+ </template>
16
+
17
+ <style scoped>
18
+ .loading {
19
+ position: fixed;
20
+ z-index: 999999;
21
+ border-radius: 50%;
22
+ height: 2em;
23
+ aspect-ratio: 1;
24
+ margin: 8px;
25
+ }
26
+
27
+ .loading-enter-active,
28
+ .loading-leave-active {
29
+ transition: all 0.3s ease-out;
30
+ }
31
+
32
+ .loading-enter-from,
33
+ .loading-leave-to {
34
+ opacity: 0;
35
+ }
36
+ </style>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ const appStore = useAppStore()
3
+ </script>
4
+
5
+ <template>
6
+ <TransitionGroup
7
+ name="notifications"
8
+ class="notifications flex-column fixed bottom right g2 m1"
9
+ tag="div"
10
+ >
11
+ <div
12
+ v-for="notification in appStore.notifications"
13
+ :key="notification.id"
14
+ :class="[
15
+ 'px2 py1 flex-row flex-center shadow',
16
+ notification.type === 'error' ? 'danger' : notification.type || 'fg',
17
+ ]"
18
+ >
19
+ {{ $t(notification.message, notification.params) }}
20
+ <button
21
+ v-if="notification.isSticky"
22
+ :class="[notification.type, notification.type + '-border', 'py0 pr0']"
23
+ @click="appStore.removeNotification(notification.id)"
24
+ >
25
+ <svg
26
+ width="10"
27
+ height="10"
28
+ viewBox="0 0 10 10"
29
+ >
30
+ <line
31
+ x1="0"
32
+ y1="0"
33
+ x2="10"
34
+ y2="10"
35
+ stroke="currentColor"
36
+ stroke-width="2"
37
+ />
38
+ <line
39
+ x1="0"
40
+ y1="10"
41
+ x2="10"
42
+ y2="0"
43
+ stroke="currentColor"
44
+ stroke-width="2"
45
+ />
46
+ </svg>
47
+ </button>
48
+ </div>
49
+ </TransitionGroup>
50
+ </template>
51
+
52
+ <style scoped>
53
+ .notifications {
54
+ z-index: 999999;
55
+ max-width: 400px;
56
+ }
57
+
58
+ .notifications-move,
59
+ .notifications-enter-active,
60
+ .notifications-leave-active {
61
+ transition: all 0.3s ease-in-out;
62
+ }
63
+
64
+ .notifications-enter-from {
65
+ transform: translateY(120%);
66
+ }
67
+
68
+ .notifications-leave-to {
69
+ transform: translateX(100%);
70
+ }
71
+ </style>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ const appStore = useAppStore()
3
+ const emits = defineEmits(['deleted'])
4
+ const props = defineProps({
5
+ path: String,
6
+ isPrivate: {
7
+ type: Boolean,
8
+ default: true,
9
+ },
10
+ })
11
+
12
+ async function deleteFile() {
13
+ try {
14
+ if (await appStore.confirm()) {
15
+ await deleteFiles(props.path!, props.isPrivate)
16
+ appStore.notify('deleted', 'success')
17
+ emits('deleted')
18
+ }
19
+ } catch (e: any) {
20
+ appStore.notify(e.data?.message, 'error')
21
+ }
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <button
27
+ type="button"
28
+ class="flex-row g2"
29
+ @click="deleteFile"
30
+ >
31
+ {{ $t('delete') }}
32
+ </button>
33
+ </template>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ import type { PropType } from 'vue'
3
+
4
+ const appStore = useAppStore()
5
+ const emits = defineEmits(['uploaded'])
6
+ const props = defineProps({
7
+ path: String,
8
+ isPrivate: {
9
+ type: Boolean,
10
+ default: true,
11
+ },
12
+ imageFormat: {
13
+ type: Object as PropType<{
14
+ maxHeight?: number
15
+ format?: string
16
+ quality?: number
17
+ }>,
18
+ },
19
+ })
20
+
21
+ const progress = ref<number>(0)
22
+
23
+ async function uploadFile(event: Event) {
24
+ const input = event.target as HTMLInputElement
25
+ if (!input.files || input.files.length === 0 || !props.path) return
26
+
27
+ try {
28
+ let files = Array.from(input.files)
29
+ if (props.imageFormat) {
30
+ progress.value = 1
31
+ const { maxHeight, format, quality } = props.imageFormat
32
+ const results = await Promise.allSettled(
33
+ files.map((file) =>
34
+ file.type.startsWith('image/')
35
+ ? formatImage(file, maxHeight, format, quality)
36
+ : Promise.resolve(file),
37
+ ),
38
+ )
39
+ const failed: string[] = []
40
+ files = files.flatMap((file, i) => {
41
+ const result = results[i]!
42
+ if (result.status === 'fulfilled') return result.value
43
+ failed.push(file.name)
44
+ return []
45
+ })
46
+ if (failed.length) {
47
+ appStore.notify('formatFailed', 'error', false, { files: failed.join(', ') })
48
+ }
49
+ }
50
+ if (files.length) {
51
+ let response = await uploadFiles(files, props.path, props.isPrivate, (p) => {
52
+ progress.value = p
53
+ })
54
+ emits('uploaded', response)
55
+ appStore.notify('saved', 'success')
56
+ }
57
+ } catch (e: any) {
58
+ appStore.notify(e?.data?.message || e?.message, 'error')
59
+ }
60
+ progress.value = 0
61
+ input.value = ''
62
+ }
63
+ </script>
64
+
65
+ <template>
66
+ <div
67
+ v-if="progress"
68
+ class="modal flex-center"
69
+ >
70
+ <progress
71
+ v-if="progress"
72
+ :value="progress"
73
+ max="100"
74
+ style="width: 90vw"
75
+ >
76
+ {{ progress }}%
77
+ </progress>
78
+ </div>
79
+ <input
80
+ v-else
81
+ type="file"
82
+ multiple
83
+ @change="uploadFile"
84
+ />
85
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import type { NuxtError } from '#app'
3
+
4
+ const props = defineProps<{ error: NuxtError }>()
5
+
6
+ const isClientError = props.error.status && props.error.status >= 400 && props.error.status < 500
7
+
8
+ if (import.meta.client && !isClientError) {
9
+ $fetch('/api/log', {
10
+ method: 'POST',
11
+ body: {
12
+ summary: props.error.message,
13
+ data: props.error,
14
+ origin: useRoute().fullPath,
15
+ type: 'error',
16
+ },
17
+ }).catch(() => {})
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <NuxtLayout name="default">
23
+ <div class="text-center flex-center flex-column m3 g3">
24
+ <h1 class="mt0">{{ error.message }}</h1>
25
+ <NuxtLink to="/">Go back home</NuxtLink>
26
+ </div>
27
+ </NuxtLayout>
28
+ </template>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div class="page flex-column">
3
+ <TheHeader />
4
+ <div class="flex-auto">
5
+ <slot />
6
+ </div>
7
+ <TheFooter />
8
+ </div>
9
+ </template>
10
+
11
+ <style scoped>
12
+ .page {
13
+ min-height: 100vh;
14
+ }
15
+ </style>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <slot />
3
+ </template>
@@ -0,0 +1,15 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const { user } = useUserSession()
3
+
4
+ if (!user.value) {
5
+ const redirectUrl =
6
+ '/signin' + (to.fullPath !== '/' ? '?goto=' + encodeURIComponent(to.fullPath) : '')
7
+ return navigateTo(redirectUrl, { replace: true })
8
+ } else if (user.value.role !== 'admin') {
9
+ return createError({
10
+ statusCode: 403,
11
+ statusMessage: 'Forbidden',
12
+ message: 'You do not have permission to access this page.',
13
+ })
14
+ }
15
+ })
@@ -0,0 +1,10 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const { loggedIn } = useUserSession()
3
+
4
+ if (!loggedIn.value) {
5
+ if (String(to.name)?.endsWith('signin')) return
6
+ const redirectUrl =
7
+ '/signin' + (to.fullPath !== '/' ? '?goto=' + encodeURIComponent(to.fullPath) : '')
8
+ return navigateTo(redirectUrl, { replace: true })
9
+ }
10
+ })
@@ -0,0 +1,8 @@
1
+ export default defineNuxtRouteMiddleware((to) => {
2
+ const { loggedIn, user } = useUserSession()
3
+
4
+ if (!loggedIn.value || !user.value?.requiresMfaSetup) return
5
+ if (to.path.endsWith('/user/auth')) return
6
+
7
+ return navigateTo('/user/auth', { replace: true })
8
+ })