feat(frontend): Clean-up schemas, remove dead code, move types

This commit is contained in:
2025-10-03 08:44:15 +02:00
parent 17a3a76054
commit 6c88b4fd96
12 changed files with 136 additions and 351 deletions

View File

@@ -9,7 +9,7 @@
</template>
<template #body>
<UForm :state="state" :schema="schema" class="space-y-4" @submit="onSubmit">
<UForm :state="state" :schema="organizationSchema" class="space-y-4" @submit="onCreateOrganizationSubmit">
<UFormField label="Name" name="name">
<UInput v-model="state.name" class="w-full" />
</UFormField>
@@ -33,25 +33,14 @@
</template>
<script setup lang="ts">
import * as z from 'zod'
const emit = defineEmits<{
(e: 'organizationCreated', id: string | undefined): void
}>()
import { organizationSchema, type OrganizationSchema } from '~/types/schemas'
const { createOrganization } = useOrganizationStore()
const open = ref(false)
const loading = ref(false)
const isSlugEdited = ref(false)
type Schema = z.output<typeof schema>
const schema = z.object({
name: z.string().min(2, 'Too short'),
slug: z.string().min(2, 'Too short'),
logo: z.string().optional()
})
const state = reactive<Partial<Schema>>({
const state = reactive<Partial<OrganizationSchema>>({
name: undefined,
slug: undefined,
logo: undefined
@@ -87,13 +76,12 @@ function handleLogoChange(event: Event) {
reader.readAsDataURL(file)
}
async function onSubmit() {
async function onCreateOrganizationSubmit() {
loading.value = true
if (!state.name || !state.slug) return
const organization = await createOrganization(state.name, state.slug, state.logo)
emit('organizationCreated', organization.data?.id)
await createOrganization(state.name, state.slug, state.logo)
loading.value = false
open.value = false
}

View File

@@ -1,80 +0,0 @@
<template>
<div class="form-container">
<h2>Login</h2>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label for="email-login">Email:</label>
<input id="email-login" v-model="email" type="email" required />
</div>
<div class="form-group">
<label for="password-login">Passwort:</label>
<input id="password-login" v-model="password" type="password" required />
</div>
<button type="submit">Login</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const email = ref('')
const password = ref('')
const handleLogin = () => {
if (!email.value || !password.value) {
alert('Bitte alle Felder ausfüllen')
return
}
authClient.signIn.email(
{
email: email.value,
password: password.value
},
{
onRequest: (ctx) => {
console.log('Sending login request', ctx)
},
onSuccess: (ctx) => {
console.log('Successfully logged in!')
},
onError: (ctx) => {
console.log(ctx.error.message)
}
}
)
}
</script>
<style scoped>
.form-container {
max-width: 300px;
margin: auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.form-group {
margin-bottom: 10px;
}
input {
width: 100%;
padding: 8px;
margin-top: 4px;
}
button {
width: 100%;
padding: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>

View File

@@ -1,97 +0,0 @@
<template>
<div class="form-container">
<h2>Registrieren</h2>
<form @submit.prevent="handleRegister">
<div class="form-group">
<label for="username">Benutzername:</label>
<input id="username" v-model="username" type="text" required />
</div>
<div class="form-group">
<label for="email">Email:</label>
<input id="email" v-model="email" type="email" required />
</div>
<div class="form-group">
<label for="password">Passwort:</label>
<input id="password" v-model="password" type="password" required />
</div>
<div class="form-group">
<label for="confirmPassword">Passwort bestätigen:</label>
<input id="confirmPassword" v-model="confirmPassword" type="password" required />
</div>
<button type="submit">Registrieren</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const username = ref('')
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const handleRegister = () => {
if (!username.value || !email.value || !password.value || !confirmPassword.value) {
alert('Bitte alle Felder ausfüllen')
return
}
if (password.value !== confirmPassword.value) {
alert('Passwörter stimmen nicht überein')
return
}
authClient.signUp.email(
{
email: email.value,
password: password.value,
name: username.value
},
{
onRequest: (ctx) => {
console.log('Sending register request', ctx)
},
onSuccess: (ctx) => {
console.log('Successfully registered!')
},
onError: (ctx) => {
console.log(ctx.error.message)
}
}
)
}
</script>
<style scoped>
.form-container {
max-width: 300px;
margin: auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.form-group {
margin-bottom: 10px;
}
input {
width: 100%;
padding: 8px;
margin-top: 4px;
}
button {
width: 100%;
padding: 10px;
background: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #218838;
}
</style>

View File

@@ -1,5 +1,5 @@
import type { LegalRole } from '~/server/utils/permissions'
import type { ListMembersOptions } from '~/composables/useAuth'
import type { ListMembersOptions } from '~/types/auth'
export function useOrganizationApi() {
const { organization } = useAuth()

View File

@@ -2,47 +2,20 @@
import { defu } from 'defu'
import { createAuthClient } from 'better-auth/vue'
import type { InferSessionFromClient, InferUserFromClient, ClientOptions } from 'better-auth/client'
import { organizationClient, jwtClient } from 'better-auth/client/plugins'
import type { ClientOptions, InferSessionFromClient, InferUserFromClient } from 'better-auth/client'
import { jwtClient, organizationClient } from 'better-auth/client/plugins'
import type { RouteLocationRaw } from 'vue-router'
import {
accessControl,
employerRole,
worksCouncilMemberRole,
employeeRole,
adminRole,
employeeRole,
employerRole,
ownerRole,
ROLES
ROLES,
worksCouncilMemberRole
} from '~/server/utils/permissions'
import type { RuntimeAuthConfig } from '~/types/auth'
interface RuntimeAuthConfig {
redirectUserTo: RouteLocationRaw | string
redirectGuestTo: RouteLocationRaw | string
}
// Types can be found here: https://github.com/better-auth/better-auth/blob/3f574ec70bb15c155a78673d42c5e25f7376ced3/packages/better-auth/src/plugins/organization/routes/crud-invites.ts#L531
type Client = ReturnType<typeof useAuth>['client']
export type Session = Client['$Infer']['Session']
export type User = Session['user']
export type ActiveOrganization = Client['$Infer']['ActiveOrganization']
export type Organization = Client['$Infer']['Organization']
export type Invitation = Client['$Infer']['Invitation']
export type Member = Client['$Infer']['Member']
export type ListMembersOptions = Parameters<Client['organization']['listMembers']>[0]
export type ListMembersResponse = Awaited<ReturnType<Client['organization']['listMembers']>>
export type ListMembersQuery = NonNullable<ListMembersOptions>['query']
// Extended invitation type with additional organization and inviter details
export type CustomInvitation =
| (Invitation & {
organizationName: string
organizationSlug: string
inviterEmail: string
})
| null
// TODO: Move into pinia store
const session = ref<InferSessionFromClient<ClientOptions> | null>(null)
const user = ref<InferUserFromClient<ClientOptions> | null>(null)
const sessionFetching = import.meta.server ? ref(false) : ref(false)
@@ -195,7 +168,6 @@ export function useAuth() {
user,
loggedIn: computed(() => !!session.value),
signIn: client.signIn,
signUp: client.signUp,
deleteUser: client.deleteUser,
signOut,
organization: client.organization,

View File

@@ -69,7 +69,7 @@
</template>
<script setup lang="ts">
import type { CustomInvitation } from '~/composables/useAuth'
import type { CustomInvitation } from '~/types/auth'
const invitationId = useRoute().params.id as string

View File

@@ -1,11 +1,11 @@
<template>
<UAuthForm
:fields="fields"
:schema="schema"
:schema="signInSchema"
:providers="providers"
title="Welcome back"
icon="i-lucide-lock"
@submit="onSubmit"
@submit="onLoginSubmit"
>
<template #description>
Don't have an account? <ULink to="/signup" class="text-primary-500 font-medium">Sign up</ULink>.
@@ -22,15 +22,15 @@
</template>
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import { signInSchema, type SignInSchema } from '~/types/schemas'
definePageMeta({ layout: 'auth' })
useSeoMeta({ title: 'Login' })
const toast = useToast()
const { signIn } = useAuth()
const { signIn } = useUserStore()
const fields = [
{
@@ -70,45 +70,11 @@ const providers = [
}
]
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
type Schema = z.output<typeof schema>
function onSubmit(payload: FormSubmitEvent<Schema>) {
function onLoginSubmit(payload: FormSubmitEvent<SignInSchema>) {
if (!payload.data.email || !payload.data.password) {
alert('Bitte alle Felder ausfüllen')
return
}
signIn.email(
{
email: payload.data.email,
password: payload.data.password
},
{
onRequest: () => {
// TODO: Show loading spinner
console.log('Sending login request')
},
onResponse: () => {
// TODO: Hide loading spinner
console.log('Receiving login response')
},
onSuccess: async (ctx) => {
console.log('Successfully logged in!', ctx)
await navigateTo('/')
},
onError: (ctx) => {
console.log(ctx.error.message)
useToast().add({
title: 'Fehler bei der Anmeldung',
description: ctx.error.message,
color: 'error'
})
}
}
)
signIn(payload)
}
</script>

View File

@@ -1,11 +1,11 @@
<template>
<UAuthForm
:fields="fields"
:schema="schema"
:schema="signUpSchema"
:providers="providers"
title="Create an account"
:submit="{ label: 'Create account' }"
@submit="onSubmit"
@submit="onSignUpSubmit"
>
<template #description>
Already have an account? <ULink to="/login" class="text-primary-500 font-medium">Login</ULink>.
@@ -18,17 +18,15 @@
</template>
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import { UserRole } from '~/.api-client'
import { signUpSchema, type SignUpSchema } from '~/types/schemas'
definePageMeta({ layout: 'auth' })
useSeoMeta({ title: 'Sign up' })
const toast = useToast()
const { signUp, deleteUser } = useAuth()
const { createUser } = useUser()
const { signUp } = useUserStore()
const fields = [
{
@@ -68,63 +66,7 @@ const providers = [
}
]
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
type Schema = z.output<typeof schema>
function onSubmit(payload: FormSubmitEvent<Schema>) {
signUp.email(
{
email: payload.data.email,
password: payload.data.password,
name: payload.data.name
},
{
onRequest: () => {
// TODO: Show loading spinner
console.log('Sending register request')
},
onResponse: () => {
// TODO: Hide loading spinner
console.log('Receiving register response')
},
onSuccess: async (ctx) => {
console.log('Successfully registered!')
// Create user in backend after successful Better Auth registration
try {
console.log('Creating user in backend...', ctx.data)
await createUser({
id: ctx.data.user.id,
name: ctx.data.user.name,
status: 'ACTIVE',
role: UserRole.Employee
})
console.log('User created in backend successfully')
} catch (error) {
console.error('Failed to create user in backend:', error)
toast.add({
title: 'Warning',
description: 'Account created but there was an issue with backend setup. Please contact support.',
color: 'warning'
})
}
await navigateTo('/')
},
onError: async (ctx) => {
console.log(ctx.error.message)
useToast().add({
title: 'Fehler bei der Registrierung',
description: ctx.error.message,
color: 'error'
})
}
}
)
function onSignUpSubmit(payload: FormSubmitEvent<SignUpSchema>) {
signUp(payload)
}
</script>

View File

@@ -1,13 +1,15 @@
import type {
ActiveOrganization,
Organization,
Invitation,
ListMembersOptions,
ListMembersResponse,
ListMembersQuery
} from '~/composables/useAuth'
import { useOrganizationApi } from '~/composables/organization/useOrganizationApi'
import type { LegalRole } from '~/server/utils/permissions'
import type {
ActiveOrganization,
CustomInvitation,
Invitation,
ListMembersOptions,
ListMembersQuery,
ListMembersResponse,
Member,
Organization
} from '~/types/auth'
export const useOrganizationStore = defineStore('Organization', () => {
const activeOrganization = ref<ActiveOrganization | null>(null)

View File

@@ -1,39 +1,82 @@
import type { FormSubmitEvent } from '@nuxt/ui'
import { UserRole } from '~/.api-client'
import type { SignInSchema, SignUpSchema } from '~/types/schemas'
export const useUserStore = defineStore('User', () => {
const { createUser } = useUser()
const name = ref('')
const toast = useToast()
const { client } = useAuth()
async function signUp(payload: FormSubmitEvent<Schema>) {
await authClient.signUp.email(
async function signUp(payload: FormSubmitEvent<SignUpSchema>) {
await client.signUp.email(
{
email: payload.data.email,
password: payload.data.password,
name: payload.data.name
},
{
onRequest: (ctx) => {
// TODO: Show loading spinner
onRequest: () => {
console.log('Sending register request')
},
onResponse: () => {
// TODO: Hide loading spinner
console.log('Receiving register response')
},
onSuccess: async (ctx) => {
console.log('Successfully registered!')
// Create user in backend after successful Better Auth registration
try {
console.log('Creating user in backend...', ctx.data)
await createUser({
id: ctx.data.user.id,
name: ctx.data.user.name,
status: 'ACTIVE',
role: UserRole.Employee
})
console.log('User created in backend successfully')
} catch (error) {
console.error('Failed to create user in backend:', error)
toast.add({
title: 'Warning',
description: 'Account created but there was an issue with backend setup. Please contact support.',
color: 'warning'
})
}
await navigateTo('/')
},
onError: (ctx) => {
onError: async (ctx) => {
console.log(ctx.error.message)
useToast().add({
title: 'Fehler bei der Registrierung',
description: ctx.error.message,
color: 'success'
color: 'error'
})
}
}
)
}
return { name, signUp }
async function signIn(payload: FormSubmitEvent<SignInSchema>) {
await client.signIn.email(
{
email: payload.data.email,
password: payload.data.password
},
{
onRequest: () => {
console.log('Sending login request')
},
onSuccess: () => {
console.log('Successfully logged in!')
},
onError: (ctx) => {
console.log(ctx.error.message)
}
}
)
}
return { name, signUp, signIn }
})

View File

@@ -0,0 +1,27 @@
import type { RouteLocationRaw } from '#vue-router'
import type { useAuth } from '~/composables/useAuth'
export interface RuntimeAuthConfig {
redirectUserTo: RouteLocationRaw | string
redirectGuestTo: RouteLocationRaw | string
}
// Types can be found here: https://github.com/better-auth/better-auth/blob/3f574ec70bb15c155a78673d42c5e25f7376ced3/packages/better-auth/src/plugins/organization/routes/crud-invites.ts#L531
type Client = ReturnType<typeof useAuth>['client']
export type Session = Client['$Infer']['Session']
export type User = Session['user']
export type ActiveOrganization = Client['$Infer']['ActiveOrganization']
export type Organization = Client['$Infer']['Organization']
export type Invitation = Client['$Infer']['Invitation']
export type Member = Client['$Infer']['Member']
export type ListMembersOptions = Parameters<Client['organization']['listMembers']>[0]
export type ListMembersResponse = Awaited<ReturnType<Client['organization']['listMembers']>>
export type ListMembersQuery = NonNullable<ListMembersOptions>['query']
// Extended invitation type with additional organization and inviter details
export type CustomInvitation =
| (Invitation & {
organizationName: string
organizationSlug: string
inviterEmail: string
})
| null

View File

@@ -0,0 +1,22 @@
import * as z from 'zod'
export const signUpSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
export const signInSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be at least 8 characters')
})
export const organizationSchema = z.object({
name: z.string().min(2, 'Too short'),
slug: z.string().min(2, 'Too short'),
logo: z.string().optional()
})
export type SignUpSchema = z.output<typeof signUpSchema>
export type SignInSchema = z.output<typeof signInSchema>
export type OrganizationSchema = z.output<typeof organizationSchema>