@xenterprises/nuxt-x-auth-better 0.3.1 → 0.4.0

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.
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="text-center">
4
+ <h2 class="text-lg font-semibold text-highlighted">Create team</h2>
5
+ <p class="text-sm text-muted mt-1">Add a new team to this organization.</p>
6
+ </div>
7
+
8
+ <UForm :schema="schema" :state="state" class="space-y-4" @submit="handleCreate">
9
+ <UFormField label="Name" name="name">
10
+ <UInput v-model="state.name" placeholder="Engineering" class="w-full" />
11
+ </UFormField>
12
+
13
+ <UFormField label="Description" name="description">
14
+ <UInput v-model="state.description" placeholder="Team description" class="w-full" />
15
+ </UFormField>
16
+
17
+ <UButton
18
+ type="submit"
19
+ block
20
+ size="lg"
21
+ label="Create team"
22
+ :loading="creating"
23
+ />
24
+ </UForm>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import { z } from "zod"
30
+
31
+ const props = defineProps({
32
+ organization: {
33
+ type: Object,
34
+ required: true,
35
+ },
36
+ })
37
+
38
+ const router = useRouter()
39
+ const { createTeam } = useOrganization()
40
+
41
+ const creating = ref(false)
42
+
43
+ const state = reactive({
44
+ name: '',
45
+ description: '',
46
+ })
47
+
48
+ const schema = z.object({
49
+ name: z.string().min(1, 'Team name is required'),
50
+ description: z.string().optional(),
51
+ })
52
+
53
+ async function handleCreate() {
54
+ creating.value = true
55
+ try {
56
+ const result = await createTeam(state.name, props.organization.id)
57
+ if (result && !result.error) {
58
+ router.push(`/org/${props.organization.slug}/teams/${result.id}`)
59
+ }
60
+ } finally {
61
+ creating.value = false
62
+ }
63
+ }
64
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Teams</h2>
5
+ <UButton
6
+ v-if="isOrgAdmin"
7
+ size="sm"
8
+ to="teams/create"
9
+ label="New team"
10
+ icon="i-lucide-plus"
11
+ />
12
+ </div>
13
+
14
+ <div v-if="teams.length === 0" class="text-center py-8 text-muted">
15
+ No teams yet.
16
+ </div>
17
+
18
+ <div v-else class="grid gap-3">
19
+ <NuxtLink
20
+ v-for="team in teams"
21
+ :key="team.id"
22
+ :to="`/org/${organization.slug}/teams/${team.id}`"
23
+ class="flex items-center justify-between p-4 rounded-lg border border-neutral-200 dark:border-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-colors"
24
+ >
25
+ <div class="flex items-center gap-3">
26
+ <div class="flex size-10 items-center justify-center rounded-lg bg-secondary/10 text-secondary">
27
+ <UIcon name="i-lucide-users" class="size-5" />
28
+ </div>
29
+ <div>
30
+ <p class="font-medium text-highlighted">{{ team.name }}</p>
31
+ <p v-if="team.description" class="text-xs text-muted">{{ team.description }}</p>
32
+ </div>
33
+ </div>
34
+ <UIcon name="i-lucide-chevron-right" class="size-5 text-muted" />
35
+ </NuxtLink>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup>
41
+ const props = defineProps({
42
+ organization: {
43
+ type: Object,
44
+ required: true,
45
+ },
46
+ })
47
+
48
+ const { teams, isOrgAdmin } = useOrganization()
49
+ </script>
@@ -0,0 +1,123 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Team members</h2>
5
+ <UButton
6
+ v-if="isOrgAdmin"
7
+ size="sm"
8
+ label="Add member"
9
+ icon="i-lucide-plus"
10
+ @click="showAdd = true"
11
+ />
12
+ </div>
13
+
14
+ <div v-if="teamMembers.length === 0" class="text-center py-8 text-muted">
15
+ No members in this team yet.
16
+ </div>
17
+
18
+ <div v-else class="divide-y divide-neutral-200 dark:divide-neutral-800">
19
+ <div
20
+ v-for="member in teamMembers"
21
+ :key="member.id"
22
+ class="flex items-center justify-between py-3"
23
+ >
24
+ <div class="flex items-center gap-3">
25
+ <div class="flex size-9 items-center justify-center rounded-full bg-primary/10 text-primary">
26
+ <UIcon name="i-lucide-user" class="size-4" />
27
+ </div>
28
+ <div>
29
+ <p class="text-sm font-medium text-highlighted">{{ member.name || member.email }}</p>
30
+ <p class="text-xs text-muted">{{ member.email }}</p>
31
+ </div>
32
+ </div>
33
+
34
+ <UButton
35
+ v-if="isOrgAdmin"
36
+ color="error"
37
+ variant="soft"
38
+ size="xs"
39
+ label="Remove"
40
+ icon="i-lucide-trash"
41
+ @click="removeTeamMember(team.id, member.id)"
42
+ />
43
+ </div>
44
+ </div>
45
+
46
+ <UModal v-model:open="showAdd">
47
+ <div class="p-6 space-y-4">
48
+ <h3 class="text-lg font-semibold text-highlighted">Add member to team</h3>
49
+ <USelect
50
+ v-model="selectedMemberId"
51
+ :items="availableMembers"
52
+ placeholder="Select a member"
53
+ class="w-full"
54
+ />
55
+ <div class="flex justify-end gap-2">
56
+ <UButton variant="outline" label="Cancel" @click="showAdd = false" />
57
+ <UButton label="Add" :loading="adding" @click="handleAdd" />
58
+ </div>
59
+ </div>
60
+ </UModal>
61
+ </div>
62
+ </template>
63
+
64
+ <script setup>
65
+ const props = defineProps({
66
+ team: {
67
+ type: Object,
68
+ required: true,
69
+ },
70
+ organization: {
71
+ type: Object,
72
+ required: true,
73
+ },
74
+ })
75
+
76
+ const { members, isOrgAdmin, listTeamMembers, addTeamMember, removeTeamMember } = useOrganization()
77
+
78
+ const showAdd = ref(false)
79
+ const selectedMemberId = ref('')
80
+ const adding = ref(false)
81
+ const teamMembers = ref([])
82
+ const teamMembersLoading = ref(false)
83
+
84
+ onMounted(async () => {
85
+ teamMembersLoading.value = true
86
+ const result = await listTeamMembers(props.team.id)
87
+ if (result && !result.error) {
88
+ teamMembers.value = result || []
89
+ }
90
+ teamMembersLoading.value = false
91
+ })
92
+
93
+ const availableMembers = computed(() => {
94
+ const teamMemberIds = teamMembers.value.map((m) => m.id)
95
+ return members.value
96
+ .filter((m) => !teamMemberIds.includes(m.id))
97
+ .map((m) => ({ label: m.name || m.email || m.id, value: m.id }))
98
+ })
99
+
100
+ async function handleAdd() {
101
+ if (!selectedMemberId.value) return
102
+ adding.value = true
103
+ try {
104
+ const result = await addTeamMember(props.team.id, selectedMemberId.value, props.organization.id)
105
+ if (!result?.error) {
106
+ showAdd.value = false
107
+ selectedMemberId.value = ''
108
+ const refreshed = await listTeamMembers(props.team.id)
109
+ teamMembers.value = refreshed || []
110
+ }
111
+ } finally {
112
+ adding.value = false
113
+ }
114
+ }
115
+
116
+ async function handleRemove(memberId) {
117
+ const result = await removeTeamMember(props.team.id, memberId)
118
+ if (!result?.error) {
119
+ const refreshed = await listTeamMembers(props.team.id)
120
+ teamMembers.value = refreshed || []
121
+ }
122
+ }
123
+ </script>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Team settings</h2>
5
+ <UButton
6
+ v-if="isOrgAdmin"
7
+ color="error"
8
+ variant="soft"
9
+ size="sm"
10
+ label="Delete"
11
+ icon="i-lucide-trash"
12
+ @click="handleDelete"
13
+ />
14
+ </div>
15
+
16
+ <UForm :schema="schema" :state="state" class="space-y-4" @submit="handleUpdate">
17
+ <UFormField label="Name" name="name">
18
+ <UInput v-model="state.name" class="w-full" />
19
+ </UFormField>
20
+
21
+ <UFormField label="Description" name="description">
22
+ <UInput v-model="state.description" class="w-full" />
23
+ </UFormField>
24
+
25
+ <UButton
26
+ type="submit"
27
+ size="sm"
28
+ label="Save changes"
29
+ :loading="updating"
30
+ />
31
+ </UForm>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup>
36
+ import { z } from "zod"
37
+
38
+ const props = defineProps({
39
+ team: {
40
+ type: Object,
41
+ required: true,
42
+ },
43
+ organization: {
44
+ type: Object,
45
+ required: true,
46
+ },
47
+ })
48
+
49
+ const router = useRouter()
50
+ const { updateTeam, deleteTeam, isOrgAdmin } = useOrganization()
51
+
52
+ const updating = ref(false)
53
+
54
+ const state = reactive({
55
+ name: props.team.name || '',
56
+ description: props.team.description || '',
57
+ })
58
+
59
+ const schema = z.object({
60
+ name: z.string().min(1, 'Team name is required'),
61
+ description: z.string().optional(),
62
+ })
63
+
64
+ async function handleUpdate() {
65
+ updating.value = true
66
+ try {
67
+ await updateTeam(props.team.id, { name: state.name, description: state.description }, props.organization.id)
68
+ } finally {
69
+ updating.value = false
70
+ }
71
+ }
72
+
73
+ async function handleDelete() {
74
+ if (!confirm('Are you sure you want to delete this team?')) {
75
+ return
76
+ }
77
+ const result = await deleteTeam(props.team.id, props.organization.id)
78
+ if (!result?.error) {
79
+ router.push(`/org/${props.organization.slug}`)
80
+ }
81
+ }
82
+ </script>
@@ -7,6 +7,61 @@ export interface AuthUser {
7
7
  metadata?: Record<string, any>;
8
8
  }
9
9
 
10
+ export interface Organization {
11
+ id: string;
12
+ name: string;
13
+ slug: string;
14
+ logo?: string | null;
15
+ metadata?: Record<string, any>;
16
+ createdAt: Date | string;
17
+ }
18
+
19
+ export interface OrganizationMember {
20
+ id: string;
21
+ organizationId: string;
22
+ userId: string;
23
+ email?: string;
24
+ name?: string;
25
+ role: string;
26
+ createdAt: Date | string;
27
+ }
28
+
29
+ export type InvitationStatus = 'pending' | 'accepted' | 'rejected' | 'canceled';
30
+
31
+ export interface OrganizationInvitation {
32
+ id: string;
33
+ organizationId: string;
34
+ email: string;
35
+ role: string;
36
+ status: InvitationStatus;
37
+ inviterId: string;
38
+ expiresAt: Date | string;
39
+ createdAt: Date | string;
40
+ }
41
+
42
+ export interface OrganizationTeam {
43
+ id: string;
44
+ organizationId: string;
45
+ name: string;
46
+ slug?: string;
47
+ description?: string;
48
+ createdAt: Date | string;
49
+ updatedAt: Date | string;
50
+ }
51
+
52
+ export interface PermissionCheck {
53
+ role: string;
54
+ permissions: Record<string, string[]>;
55
+ }
56
+
57
+ export interface OrganizationRoute {
58
+ path: string;
59
+ requireMember?: boolean;
60
+ requireAdmin?: boolean;
61
+ requireOwner?: boolean;
62
+ requiredPermissions?: PermissionCheck[];
63
+ }
64
+
10
65
  export interface AuthResult {
11
66
  status: "ok" | "error";
12
67
  user?: AuthUser | null;
@@ -52,12 +107,30 @@ export interface AuthConfig {
52
107
  otp?: boolean;
53
108
  forgotPassword?: boolean;
54
109
  signup?: boolean;
110
+ organization?: boolean;
111
+ teams?: boolean;
55
112
  };
56
113
  oauthProviders?: Array<{
57
114
  id: string;
58
115
  label: string;
59
116
  icon: string;
60
117
  }>;
118
+ organization?: {
119
+ enabled?: boolean;
120
+ teams?: boolean;
121
+ allowUserToCreateOrganization?: boolean;
122
+ requireMemberEmailVerification?: boolean;
123
+ defaultRole?: 'owner' | 'admin' | 'member' | string;
124
+ protectedRoutes?: Array<string | OrganizationRoute>;
125
+ redirects?: {
126
+ create?: string;
127
+ list?: string;
128
+ settings?: string;
129
+ invitations?: string;
130
+ notMember?: string;
131
+ noPermission?: string;
132
+ };
133
+ };
61
134
  ui?: {
62
135
  showLogo?: boolean;
63
136
  logoUrl?: string;