@xenterprises/nuxt-x-auth-better 0.3.2 → 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.
- package/README.md +193 -0
- package/app/app.config.ts +19 -0
- package/app/components/XAuth/OrganizationCreate.vue +65 -0
- package/app/components/XAuth/OrganizationInvitations.vue +48 -0
- package/app/components/XAuth/OrganizationInvite.vue +73 -0
- package/app/components/XAuth/OrganizationMembers.vue +103 -0
- package/app/components/XAuth/OrganizationSettings.vue +85 -0
- package/app/components/XAuth/OrganizationSwitcher.vue +61 -0
- package/app/components/XAuth/TeamCreate.vue +64 -0
- package/app/components/XAuth/TeamList.vue +49 -0
- package/app/components/XAuth/TeamMembers.vue +123 -0
- package/app/components/XAuth/TeamSettings.vue +82 -0
- package/app/composables/types.ts +73 -0
- package/app/composables/useOrganization.ts +532 -0
- package/app/composables/useXAuth.ts +11 -5
- package/app/middleware/organization.global.ts +125 -0
- package/app/pages/auth/handler/invitation.vue +63 -0
- package/app/pages/org/[slug]/index.vue +53 -0
- package/app/pages/org/[slug]/invitations.vue +23 -0
- package/app/pages/org/[slug]/invite.vue +30 -0
- package/app/pages/org/[slug]/members.vue +37 -0
- package/app/pages/org/[slug]/settings.vue +39 -0
- package/app/pages/org/[slug]/teams/[teamId]/index.vue +51 -0
- package/app/pages/org/[slug]/teams/create.vue +30 -0
- package/app/pages/org/[slug]/teams/index.vue +37 -0
- package/app/pages/org/create.vue +13 -0
- package/app/pages/org/index.vue +20 -0
- package/package.json +2 -2
|
@@ -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>
|
package/app/composables/types.ts
CHANGED
|
@@ -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;
|