@empyria/webui 0.1.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.
- package/.oxfmtrc.json +9 -0
- package/.oxlintrc.json +3 -0
- package/README.md +18 -0
- package/app/app.config.ts +7 -0
- package/app/app.vue +37 -0
- package/app/assets/css/main.css +189 -0
- package/app/consts.ts +5 -0
- package/app/layouts/default.vue +3 -0
- package/app/pages/index.vue +7 -0
- package/app/plugins/trpc.client.ts +74 -0
- package/app/stores/credential.ts +45 -0
- package/app/utils/Date.ts +33 -0
- package/app/utils/colors.ts +44 -0
- package/app/utils/workflow.ts +7 -0
- package/nuxt.config.ts +57 -0
- package/package.json +61 -0
- package/public/favicon.ico +0 -0
- package/renovate.json +13 -0
- package/server/api/trpc/[trpc].ts +16 -0
- package/server/env.js +7 -0
- package/server/routers/index.ts +6 -0
- package/server/trpc/context.ts +33 -0
- package/server/trpc/index.ts +72 -0
- package/tsconfig.json +10 -0
package/.oxfmtrc.json
ADDED
package/.oxlintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Principia
|
|
2
|
+
|
|
3
|
+
The Architecture:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Browser (Client) Nuxt Server Moleculer Backend
|
|
7
|
+
│ │ │
|
|
8
|
+
├─ login() ──────────────>│ │
|
|
9
|
+
│ ├─ call('v1.Auth.login') ───>│
|
|
10
|
+
│ │<── returns user data ──────┤
|
|
11
|
+
│ ├─ setCookie(tokenKey) ──────┤ (secure, httpOnly)
|
|
12
|
+
│<── returns user data ───┤
|
|
13
|
+
├─ update Pinia store
|
|
14
|
+
│
|
|
15
|
+
├─ Future requests ───────>│
|
|
16
|
+
│ ├─ reads cookie ─────────────┤ (auto-included)
|
|
17
|
+
│ ├─ moleculer.call() ─────────>│ (with tokenKey)
|
|
18
|
+
```
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const colorMode = useColorMode()
|
|
3
|
+
|
|
4
|
+
const toaster = { duration: 5000 }
|
|
5
|
+
|
|
6
|
+
const color = computed(() => (colorMode.value === 'dark' ? '#1b1718' : 'white'))
|
|
7
|
+
|
|
8
|
+
useHead({
|
|
9
|
+
meta: [
|
|
10
|
+
{ charset: 'utf-8' },
|
|
11
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
12
|
+
{ key: 'theme-color', name: 'theme-color', content: color },
|
|
13
|
+
],
|
|
14
|
+
link: [{ rel: 'icon', href: '/favicon.ico' }],
|
|
15
|
+
htmlAttrs: {
|
|
16
|
+
lang: 'en',
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const title = 'Principia Fluxus'
|
|
21
|
+
const description = 'AI-powered Data Lake platform'
|
|
22
|
+
|
|
23
|
+
useSeoMeta({
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
ogTitle: title,
|
|
27
|
+
ogDescription: description,
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<UApp :toaster="toaster">
|
|
33
|
+
<NuxtLayout>
|
|
34
|
+
<NuxtPage />
|
|
35
|
+
</NuxtLayout>
|
|
36
|
+
</UApp>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import '@nuxt/ui';
|
|
3
|
+
|
|
4
|
+
@plugin "@tailwindcss/typography";
|
|
5
|
+
|
|
6
|
+
@theme static {
|
|
7
|
+
--font-sans: 'Public Sans', sans-serif;
|
|
8
|
+
|
|
9
|
+
--color-green-50: #effdf5;
|
|
10
|
+
--color-green-100: #d9fbe8;
|
|
11
|
+
--color-green-200: #b3f5d1;
|
|
12
|
+
--color-green-300: #75edae;
|
|
13
|
+
--color-green-400: #00dc82;
|
|
14
|
+
--color-green-500: #00c16a;
|
|
15
|
+
--color-green-600: #00a155;
|
|
16
|
+
--color-green-700: #007f45;
|
|
17
|
+
--color-green-800: #016538;
|
|
18
|
+
--color-green-900: #0a5331;
|
|
19
|
+
--color-green-950: #052e16;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@theme {
|
|
23
|
+
--color-cbh-50: #f4f6fb;
|
|
24
|
+
--color-cbh-100: #e8ebf6;
|
|
25
|
+
--color-cbh-200: #cdd6ea;
|
|
26
|
+
--color-cbh-300: #a1b3d8;
|
|
27
|
+
--color-cbh-400: #6e8bc2;
|
|
28
|
+
--color-cbh-500: #4c6dab;
|
|
29
|
+
--color-cbh-600: #3c5896;
|
|
30
|
+
--color-cbh-700: #2f4475;
|
|
31
|
+
--color-cbh-800: #2a3a62;
|
|
32
|
+
--color-cbh-900: #283452;
|
|
33
|
+
--color-cbh-950: #1a2137;
|
|
34
|
+
|
|
35
|
+
--font-sans: 'Montserrat', system-ui, sans-serif;
|
|
36
|
+
--breakpoint-3xl: 1920px;
|
|
37
|
+
--breakpoint-4xl: 2560px;
|
|
38
|
+
--breakpoint-5xl: 3840px;
|
|
39
|
+
|
|
40
|
+
@keyframes tada {
|
|
41
|
+
from {
|
|
42
|
+
transform: scale3d(1, 1, 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
10%,
|
|
46
|
+
20% {
|
|
47
|
+
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
30%,
|
|
51
|
+
50%,
|
|
52
|
+
70%,
|
|
53
|
+
90% {
|
|
54
|
+
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
40%,
|
|
58
|
+
60%,
|
|
59
|
+
80% {
|
|
60
|
+
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
to {
|
|
64
|
+
transform: scale3d(1, 1, 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
--animate-tada: tada 1s ease-in-out infinite;
|
|
69
|
+
|
|
70
|
+
@keyframes backOutLeft {
|
|
71
|
+
0% {
|
|
72
|
+
transform: scale(1);
|
|
73
|
+
opacity: 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
20% {
|
|
77
|
+
transform: translateX(0px) scale(0.8);
|
|
78
|
+
opacity: 0.7;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
100% {
|
|
82
|
+
transform: translateX(-100px) scale(0.8);
|
|
83
|
+
opacity: 0.7;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
--animate-backOutLeft: backOutLeft 0.8s ease-in-out infinite;
|
|
88
|
+
|
|
89
|
+
@keyframes backOutRight {
|
|
90
|
+
0% {
|
|
91
|
+
transform: scale(1);
|
|
92
|
+
opacity: 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
20% {
|
|
96
|
+
transform: translateX(0px) scale(0.8);
|
|
97
|
+
opacity: 0.7;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
100% {
|
|
101
|
+
transform: translateX(50px) scale(0.8);
|
|
102
|
+
opacity: 0.5;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
--animate-backOutRight: backOutRight 0.8s ease-in-out infinite;
|
|
107
|
+
|
|
108
|
+
@keyframes outRight {
|
|
109
|
+
0% {
|
|
110
|
+
transform: scale(1);
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
100% {
|
|
115
|
+
transform: translateX(50px);
|
|
116
|
+
opacity: 0.5;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
--animate-outRight: outRight 0.8s ease-in-out infinite;
|
|
121
|
+
|
|
122
|
+
@keyframes outDown {
|
|
123
|
+
0% {
|
|
124
|
+
transform: scale(1);
|
|
125
|
+
opacity: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
100% {
|
|
129
|
+
transform: translateY(50px);
|
|
130
|
+
opacity: 0.5;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
--animate-outDown: outDown 0.8s ease-in-out infinite;
|
|
135
|
+
|
|
136
|
+
@keyframes outUp {
|
|
137
|
+
0% {
|
|
138
|
+
transform: scale(1);
|
|
139
|
+
opacity: 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
100% {
|
|
143
|
+
transform: translateY(-50px);
|
|
144
|
+
opacity: 0.5;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
--animate-outUp: outUp 0.8s ease-in-out infinite;
|
|
149
|
+
|
|
150
|
+
@keyframes spinning {
|
|
151
|
+
0% {
|
|
152
|
+
transform: rotate(0deg);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
100% {
|
|
156
|
+
transform: rotate(360deg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
--animate-spinning: spinning 0.8s linear infinite;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@utility animate-tada {
|
|
164
|
+
animation: var(--animate-tada);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@utility animate-backOutLeft {
|
|
168
|
+
animation: var(--animate-backOutLeft);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@utility animate-backOutRight {
|
|
172
|
+
animation: var(--animate-backOutRight);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@utility animate-outRight {
|
|
176
|
+
animation: var(--animate-outRight);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@utility animate-outDown {
|
|
180
|
+
animation: var(--animate-outDown);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@utility animate-outUp {
|
|
184
|
+
animation: var(--animate-outUp);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@utility animate-spinning {
|
|
188
|
+
animation: var(--animate-spinning);
|
|
189
|
+
}
|
package/app/consts.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createTRPCProxyClient, httpBatchStreamLink, httpLink, splitLink } from '@trpc/client'
|
|
2
|
+
import type { AppRouter } from '~/server/trpc/router'
|
|
3
|
+
|
|
4
|
+
export default defineNuxtPlugin(() => {
|
|
5
|
+
const isLoading = ref(false)
|
|
6
|
+
const toast = useToast()
|
|
7
|
+
|
|
8
|
+
let activeRequests = 0
|
|
9
|
+
|
|
10
|
+
const getBaseUrl = () => {
|
|
11
|
+
if (import.meta.server) {
|
|
12
|
+
return process.env.NUXT_PUBLIC_API_URL || 'http://localhost:3000'
|
|
13
|
+
}
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const client = createTRPCProxyClient<AppRouter>({
|
|
18
|
+
links: [
|
|
19
|
+
splitLink({
|
|
20
|
+
condition: (op) => op.path.includes('login') || op.path.includes('auth'),
|
|
21
|
+
true: httpLink({
|
|
22
|
+
url: `${getBaseUrl()}/api/trpc`,
|
|
23
|
+
fetch(url, options) {
|
|
24
|
+
return fetch(url, { ...options, credentials: 'include' })
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
false: httpBatchStreamLink({
|
|
28
|
+
url: `${getBaseUrl()}/api/trpc`,
|
|
29
|
+
fetch(url, options) {
|
|
30
|
+
activeRequests++
|
|
31
|
+
isLoading.value = true
|
|
32
|
+
return fetch(url, {
|
|
33
|
+
...options,
|
|
34
|
+
credentials: 'include',
|
|
35
|
+
})
|
|
36
|
+
.then((response) => {
|
|
37
|
+
if (response.status >= 500) {
|
|
38
|
+
toast.add({
|
|
39
|
+
title: 'Server Error',
|
|
40
|
+
description: 'Something went wrong. Please try again.',
|
|
41
|
+
icon: 'i-lucide-server-crash',
|
|
42
|
+
color: 'error',
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return response
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
toast.add({
|
|
49
|
+
title: 'Connection Error',
|
|
50
|
+
description: 'Unable to reach the server.',
|
|
51
|
+
icon: 'i-lucide-server-off',
|
|
52
|
+
color: 'error',
|
|
53
|
+
})
|
|
54
|
+
throw error
|
|
55
|
+
})
|
|
56
|
+
.finally(() => {
|
|
57
|
+
activeRequests--
|
|
58
|
+
if (activeRequests === 0) {
|
|
59
|
+
isLoading.value = false
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
provide: {
|
|
70
|
+
client,
|
|
71
|
+
trpcLoading: readonly(isLoading),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
|
|
3
|
+
export const useCredentialStore = defineStore('principia-credential', {
|
|
4
|
+
state: () => ({
|
|
5
|
+
email: '',
|
|
6
|
+
actor: '',
|
|
7
|
+
federation: '',
|
|
8
|
+
role: '',
|
|
9
|
+
flowID: '',
|
|
10
|
+
processID: '',
|
|
11
|
+
cookieExpiration: 0,
|
|
12
|
+
}),
|
|
13
|
+
getters: {
|
|
14
|
+
isAuthenticated: (state) => state.actor !== null && state.federation !== null,
|
|
15
|
+
},
|
|
16
|
+
actions: {
|
|
17
|
+
setCredentials(credentials: {
|
|
18
|
+
email: string
|
|
19
|
+
actor: string
|
|
20
|
+
federation: string
|
|
21
|
+
role: string
|
|
22
|
+
flowID: string
|
|
23
|
+
processID: string
|
|
24
|
+
cookieExpiration: number
|
|
25
|
+
}) {
|
|
26
|
+
this.email = credentials.email
|
|
27
|
+
this.actor = credentials.actor
|
|
28
|
+
this.federation = credentials.federation
|
|
29
|
+
this.role = credentials.role
|
|
30
|
+
this.flowID = credentials.flowID
|
|
31
|
+
this.processID = credentials.processID
|
|
32
|
+
this.cookieExpiration = credentials.cookieExpiration
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
logout() {
|
|
36
|
+
this.email = ''
|
|
37
|
+
this.actor = ''
|
|
38
|
+
this.federation = ''
|
|
39
|
+
this.role = ''
|
|
40
|
+
this.cookieExpiration = 0
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
persist: true,
|
|
45
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
|
|
3
|
+
const DATE_FORMAT = 'yyyy-LL-dd'
|
|
4
|
+
|
|
5
|
+
export const DISPLAY_DATE = 'dd / LLL / yyyy'
|
|
6
|
+
export const DISPLAY_TIME = 'dd / LLL / yyyy HH:mm'
|
|
7
|
+
|
|
8
|
+
export function dateToMillis(format: string) {
|
|
9
|
+
return DateTime.fromFormat(format, DATE_FORMAT).toMillis()
|
|
10
|
+
}
|
|
11
|
+
export function dateStartToMillis(format: string) {
|
|
12
|
+
return DateTime.fromFormat(format, DATE_FORMAT).startOf('day').toMillis()
|
|
13
|
+
}
|
|
14
|
+
export function dateEndToMillis(format: string) {
|
|
15
|
+
return DateTime.fromFormat(format, DATE_FORMAT).endOf('day').toMillis()
|
|
16
|
+
}
|
|
17
|
+
export function dateToFormat(millis?: number, format?: string) {
|
|
18
|
+
if (!millis) return undefined
|
|
19
|
+
return DateTime.fromMillis(millis).toFormat(format || DATE_FORMAT)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function timeToFormat(millis?: number, format?: string) {
|
|
23
|
+
if (!millis) return undefined
|
|
24
|
+
return DateTime.fromMillis(millis).toFormat(format || DISPLAY_TIME)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isoToFormat(isoDate?: string, format?: string) {
|
|
28
|
+
return DateTime.fromISO(isoDate).toFormat(format || DATE_FORMAT)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function nowISO() {
|
|
32
|
+
return DateTime.now().toISO()
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"
|
|
2
|
+
// status — possible values: pending, scheduled, ready, running, paused, backing-off, suspended, completed
|
|
3
|
+
// completion_result — only set when status = 'completed': success or failure
|
|
4
|
+
|
|
5
|
+
export const colors = {
|
|
6
|
+
status: {
|
|
7
|
+
completed: 'success',
|
|
8
|
+
pending: 'info',
|
|
9
|
+
scheduled: 'info',
|
|
10
|
+
ready: 'info',
|
|
11
|
+
running: 'warning',
|
|
12
|
+
'backing-off': 'warning',
|
|
13
|
+
suspended: 'warning',
|
|
14
|
+
default: 'neutral',
|
|
15
|
+
},
|
|
16
|
+
completion: {
|
|
17
|
+
success: 'success',
|
|
18
|
+
failure: 'error',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function componentColor(status: string, completion: string): string {
|
|
23
|
+
return colors.completion[completion] ?? colors.status[status] ?? colors.status.default
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function dotColor(status, completion_result): string {
|
|
27
|
+
return status !== 'completed'
|
|
28
|
+
? 'yellow'
|
|
29
|
+
: !completion_result
|
|
30
|
+
? 'ghost'
|
|
31
|
+
: completion_result === 'failure'
|
|
32
|
+
? 'red'
|
|
33
|
+
: 'green'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function textColor(status, completion_result): string {
|
|
37
|
+
return status !== 'completed'
|
|
38
|
+
? 'text-warning'
|
|
39
|
+
: !completion_result
|
|
40
|
+
? 'text-info'
|
|
41
|
+
: completion_result === 'failure'
|
|
42
|
+
? 'text-error'
|
|
43
|
+
: 'text-success'
|
|
44
|
+
}
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
ssr: false,
|
|
4
|
+
compatibilityDate: '2025-07-15',
|
|
5
|
+
devtools: { enabled: true },
|
|
6
|
+
experimental: {
|
|
7
|
+
asyncContext: true,
|
|
8
|
+
},
|
|
9
|
+
build: {
|
|
10
|
+
transpile: [],
|
|
11
|
+
},
|
|
12
|
+
modules: [
|
|
13
|
+
'@vueuse/nuxt',
|
|
14
|
+
'@pinia/nuxt',
|
|
15
|
+
'pinia-plugin-persistedstate/nuxt',
|
|
16
|
+
'@nuxtjs/color-mode',
|
|
17
|
+
'@nuxt/ui',
|
|
18
|
+
'@nuxt/fonts',
|
|
19
|
+
'@nuxtjs/tailwindcss',
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
css: ['~/assets/css/main.css'],
|
|
23
|
+
|
|
24
|
+
fonts: {
|
|
25
|
+
families: [{ name: 'Montserrat', provider: 'google' }],
|
|
26
|
+
providers: {
|
|
27
|
+
fontshare: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
ui: {
|
|
32
|
+
theme: {
|
|
33
|
+
colors: [
|
|
34
|
+
'primary',
|
|
35
|
+
'secondary',
|
|
36
|
+
'success',
|
|
37
|
+
'info',
|
|
38
|
+
'warning',
|
|
39
|
+
'error',
|
|
40
|
+
'neutral',
|
|
41
|
+
'cbh',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
vite: {
|
|
47
|
+
server: {
|
|
48
|
+
watch: {
|
|
49
|
+
usePolling: true,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
nitro: {
|
|
55
|
+
plugins: [],
|
|
56
|
+
},
|
|
57
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@empyria/webui",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Restate.dev helpers for the Empyria nanoservice framework",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Imre Fazekas <imre.fazekas@icloud.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/empyria-nano/guard-web.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "nuxt build",
|
|
14
|
+
"dev": "nuxt dev",
|
|
15
|
+
"generate": "nuxt generate",
|
|
16
|
+
"preview": "nuxt preview",
|
|
17
|
+
"postinstall": "nuxt prepare",
|
|
18
|
+
"typecheck": "nuxt typecheck",
|
|
19
|
+
"clean": "pkill -9 -f 'nuxt dev' 2>/dev/null; rm -rf .nuxt .output dist node_modules/.cache node_modules/.vite",
|
|
20
|
+
"format": "bun oxfmt --check",
|
|
21
|
+
"format:fix": "bun oxfmt",
|
|
22
|
+
"lint": "bun oxlint",
|
|
23
|
+
"lint:fix": "bun oxlint --fix"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@empyria/common": "0.1.2",
|
|
27
|
+
"@iconify-json/lucide": "^1.2.124",
|
|
28
|
+
"@iconify-json/simple-icons": "^1.2.93",
|
|
29
|
+
"@pinia/nuxt": "^1.0.2",
|
|
30
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
31
|
+
"@trpc/client": "^11.18.0",
|
|
32
|
+
"@trpc/server": "^11.18.0",
|
|
33
|
+
"@vueuse/core": "^14.4.0",
|
|
34
|
+
"@vueuse/nuxt": "^14.4.0",
|
|
35
|
+
"croner": "^10.0.1",
|
|
36
|
+
"cronstrue": "^3.24.0",
|
|
37
|
+
"moleculer": "0.15.1",
|
|
38
|
+
"ms": "^2.1.3",
|
|
39
|
+
"pinia-plugin-persistedstate": "^4.7.1",
|
|
40
|
+
"vue": "^3.5.41",
|
|
41
|
+
"vue-cal": "^5.0.1-rc.34",
|
|
42
|
+
"vue-router": "^5.2.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@nuxt/fonts": "^0.14.0",
|
|
46
|
+
"@nuxt/ui": "^4.10.0",
|
|
47
|
+
"@nuxtjs/tailwindcss": "7.0.0-beta.1",
|
|
48
|
+
"@tailwindcss/typography": "^0.5.20",
|
|
49
|
+
"nuxt": "^4.5.2",
|
|
50
|
+
"tailwindcss": "^4.3.3",
|
|
51
|
+
"typescript": "^7.0.2",
|
|
52
|
+
"vue-tsc": "^3.3.10",
|
|
53
|
+
"oxfmt": "0.64.0",
|
|
54
|
+
"oxlint": "1.79.0"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"bun": ">=1.4.0",
|
|
58
|
+
"node": ">=26"
|
|
59
|
+
},
|
|
60
|
+
"packageManager": "bun@1.4.0"
|
|
61
|
+
}
|
|
Binary file
|
package/renovate.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
|
2
|
+
import { createContext } from '../../trpc/context'
|
|
3
|
+
import { appRouter } from '../../routers'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
// console.log('✅ Test route hit!')
|
|
7
|
+
|
|
8
|
+
const request = toWebRequest(event)
|
|
9
|
+
|
|
10
|
+
return fetchRequestHandler({
|
|
11
|
+
endpoint: '/api/trpc',
|
|
12
|
+
req: request,
|
|
13
|
+
router: appRouter,
|
|
14
|
+
createContext: () => createContext(event),
|
|
15
|
+
})
|
|
16
|
+
})
|
package/server/env.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CookieSerializeOptions } from 'cookie-es'
|
|
2
|
+
import {
|
|
3
|
+
type H3Event,
|
|
4
|
+
parseCookies,
|
|
5
|
+
getCookie as getCookieFromEvent,
|
|
6
|
+
setCookie as setCookieOnEvent,
|
|
7
|
+
deleteCookie as deleteCookieOnEvent,
|
|
8
|
+
} from 'h3'
|
|
9
|
+
import { AccessTokenCookieKey } from '../../app/consts'
|
|
10
|
+
|
|
11
|
+
export const createContext = async (event: H3Event) => {
|
|
12
|
+
const getCookie = (name: string) => getCookieFromEvent(event, name)
|
|
13
|
+
const setCookie = (name: string, value: string, serializeOptions?: CookieSerializeOptions) =>
|
|
14
|
+
setCookieOnEvent(event, name, value, serializeOptions)
|
|
15
|
+
const deleteCookie = (name: string, serializeOptions?: CookieSerializeOptions) =>
|
|
16
|
+
deleteCookieOnEvent(event, name, serializeOptions)
|
|
17
|
+
|
|
18
|
+
const tokenKey = getCookie(AccessTokenCookieKey)
|
|
19
|
+
if (tokenKey) {
|
|
20
|
+
event.context.tokenKey = tokenKey
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
event,
|
|
25
|
+
cookies: parseCookies(event),
|
|
26
|
+
getCookie,
|
|
27
|
+
setCookie,
|
|
28
|
+
deleteCookie,
|
|
29
|
+
headers: event.headers,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Context = Awaited<ReturnType<typeof createContext>>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// server/trpc/trpc.ts
|
|
2
|
+
import { initTRPC, TRPCError } from '@trpc/server'
|
|
3
|
+
|
|
4
|
+
import { createValidator } from '@empyria/common'
|
|
5
|
+
|
|
6
|
+
import { AccessTokenCookieKey } from '../../app/consts'
|
|
7
|
+
import type { Context } from './context'
|
|
8
|
+
|
|
9
|
+
const t = initTRPC.context<Context>().create()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Public procedure - no authentication required
|
|
13
|
+
*/
|
|
14
|
+
export const publicProcedure = t.procedure
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Authorized procedure - requires access token in cookie
|
|
18
|
+
*/
|
|
19
|
+
export const authorizedProcedure = t.procedure.use(async ({ ctx, next }) => {
|
|
20
|
+
const tokenKey = ctx.getCookie(AccessTokenCookieKey)
|
|
21
|
+
|
|
22
|
+
if (!tokenKey) {
|
|
23
|
+
throw new TRPCError({
|
|
24
|
+
code: 'UNAUTHORIZED',
|
|
25
|
+
message: 'You are not authorized to make this request',
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Token is validated, proceed with request
|
|
30
|
+
return next()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Passthrough parser - accepts any input and returns it as-is
|
|
34
|
+
const passthroughParser = {
|
|
35
|
+
parse: (input: unknown) => input,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const validatedMiddleware = (schema: object) => {
|
|
39
|
+
const validate = createValidator(schema)
|
|
40
|
+
|
|
41
|
+
return middleware(async ({ next, getRawInput }) => {
|
|
42
|
+
const rawInput = await getRawInput()
|
|
43
|
+
try {
|
|
44
|
+
validate(rawInput)
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new TRPCError({
|
|
47
|
+
code: 'BAD_REQUEST',
|
|
48
|
+
message: 'Validation failed',
|
|
49
|
+
cause: {
|
|
50
|
+
errors: [err],
|
|
51
|
+
input: rawInput,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
return next()
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add AJV validation to any procedure
|
|
61
|
+
* @param baseProcedure - The base procedure (publicProcedure or authorizedProcedure)
|
|
62
|
+
* @param schema - The AJV schema to validate against
|
|
63
|
+
*/
|
|
64
|
+
export const validatedProcedure = <T extends typeof publicProcedure>(
|
|
65
|
+
baseProcedure: T,
|
|
66
|
+
schema: object,
|
|
67
|
+
) => {
|
|
68
|
+
return baseProcedure.input(passthroughParser).use(validatedMiddleware(schema))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const router = t.router
|
|
72
|
+
export const middleware = t.middleware
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
// https://nuxt.com/docs/guide/concepts/typescript
|
|
3
|
+
"files": [],
|
|
4
|
+
"references": [
|
|
5
|
+
{ "path": "./.nuxt/tsconfig.app.json" },
|
|
6
|
+
{ "path": "./.nuxt/tsconfig.server.json" },
|
|
7
|
+
{ "path": "./.nuxt/tsconfig.shared.json" },
|
|
8
|
+
{ "path": "./.nuxt/tsconfig.node.json" }
|
|
9
|
+
]
|
|
10
|
+
}
|