feat(frontend): Clean-up schemas, remove dead code, move types
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
27
legalconsenthub/types/auth.ts
Normal file
27
legalconsenthub/types/auth.ts
Normal 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
|
||||
22
legalconsenthub/types/schemas.ts
Normal file
22
legalconsenthub/types/schemas.ts
Normal 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>
|
||||
Reference in New Issue
Block a user