@nuxt-customer-portal/organizations 0.3.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/app/components/DeleteAccountModal.vue +139 -0
- package/app/components/OrganizationMembers.vue +298 -0
- package/app/components/OrganizationSettings.vue +315 -0
- package/app/components/OrganizationSwitcher.vue +166 -0
- package/app/components/organization/DetailsCard.vue +84 -0
- package/app/components/organization/InvitationsCard.vue +112 -0
- package/app/components/organization/InviteMemberModal.vue +101 -0
- package/app/components/organization/MembersCard.vue +108 -0
- package/app/pages/my-organizations.vue +125 -0
- package/app/pages/settings/index.vue +248 -0
- package/app/pages/settings/organization.vue +171 -0
- package/app/pages/settings/security.vue +195 -0
- package/app/pages/settings.vue +101 -0
- package/app/plugins/account-organizations-feature.ts +3 -0
- package/i18n/locales/en.json +208 -0
- package/i18n/locales/nl.json +208 -0
- package/nuxt.config.ts +11 -0
- package/package.json +51 -0
- package/portal.manifest.mjs +6 -0
- package/server/api/organizations/[id]/index.get.ts +54 -0
- package/server/api/organizations/[id]/invitations/[invitationId]/delete.post.ts +42 -0
- package/server/api/organizations/[id]/invitations/[invitationId]/resend.post.ts +91 -0
- package/server/api/organizations/[id]/invitations/[invitationId].patch.ts +18 -0
- package/server/api/organizations/[id]/invitations.get.ts +49 -0
- package/server/api/organizations/accept-invitation.post.ts +131 -0
- package/server/api/organizations/get-invitation.get.ts +73 -0
- package/server/api/profile.patch.ts +99 -0
- package/shared/feature.ts +24 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineEventHandler, createError, getQuery } from 'h3'
|
|
2
|
+
import { db } from '@nuxt-customer-portal/core/server/utils/db'
|
|
3
|
+
import {
|
|
4
|
+
invitation as invitationTable,
|
|
5
|
+
organization as organizationTable
|
|
6
|
+
} from '@nuxt-customer-portal/core/server/db/schema/auth-schema'
|
|
7
|
+
import { eq } from 'drizzle-orm'
|
|
8
|
+
|
|
9
|
+
defineRouteMeta({
|
|
10
|
+
openAPI: {
|
|
11
|
+
tags: ['General'],
|
|
12
|
+
operationId: 'generalOrganizationsGetInvitationGet',
|
|
13
|
+
summary: 'Get an organization invitation',
|
|
14
|
+
description:
|
|
15
|
+
'Get an organization invitation. Uses the current authenticated session and enforces the relevant portal permissions.'
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom endpoint to get invitation details, bypassing Better Auth's inviter membership check.
|
|
21
|
+
* This is necessary because admins who create organizations and invite owners
|
|
22
|
+
* are removed as members, but their invitations should still be accessible.
|
|
23
|
+
*
|
|
24
|
+
* This endpoint is public (no authentication required) so users can view invitation
|
|
25
|
+
* details before signing up or logging in.
|
|
26
|
+
*/
|
|
27
|
+
export default defineEventHandler(async (event) => {
|
|
28
|
+
const query = getQuery(event)
|
|
29
|
+
const invitationId = query.id as string | undefined
|
|
30
|
+
|
|
31
|
+
if (!invitationId) {
|
|
32
|
+
throw createError({ statusCode: 400, message: 'Invitation ID is required' })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Fetch the invitation
|
|
37
|
+
const [invitation] = await db.select().from(invitationTable).where(eq(invitationTable.id, invitationId)).limit(1)
|
|
38
|
+
|
|
39
|
+
if (!invitation) {
|
|
40
|
+
throw createError({ statusCode: 404, message: 'Invitation not found' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get organization details
|
|
44
|
+
const [organization] = await db
|
|
45
|
+
.select()
|
|
46
|
+
.from(organizationTable)
|
|
47
|
+
.where(eq(organizationTable.id, invitation.organizationId))
|
|
48
|
+
.limit(1)
|
|
49
|
+
|
|
50
|
+
if (!organization) {
|
|
51
|
+
throw createError({ statusCode: 404, message: 'Organization not found' })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Return invitation data in the same format as Better Auth's getInvitation
|
|
55
|
+
return {
|
|
56
|
+
id: invitation.id,
|
|
57
|
+
organizationId: invitation.organizationId,
|
|
58
|
+
email: invitation.email,
|
|
59
|
+
role: invitation.role,
|
|
60
|
+
status: invitation.status,
|
|
61
|
+
expiresAt: invitation.expiresAt,
|
|
62
|
+
inviterId: invitation.inviterId,
|
|
63
|
+
organizationName: organization.name,
|
|
64
|
+
organizationSlug: organization.slug
|
|
65
|
+
}
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
if (err instanceof Error && 'statusCode' in err) {
|
|
68
|
+
throw err
|
|
69
|
+
}
|
|
70
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch invitation'
|
|
71
|
+
throw createError({ statusCode: 500, message: errorMessage })
|
|
72
|
+
}
|
|
73
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineEventHandler, createError, readBody } from 'h3'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { auth } from '@nuxt-customer-portal/core/server/utils/auth'
|
|
4
|
+
import { db } from '@nuxt-customer-portal/core/server/utils/db'
|
|
5
|
+
import { user as userTable } from '@nuxt-customer-portal/core/server/db/schema/auth-schema'
|
|
6
|
+
import { eq } from 'drizzle-orm'
|
|
7
|
+
import type { SessionUser } from '@nuxt-customer-portal/core/shared/types/index'
|
|
8
|
+
|
|
9
|
+
defineRouteMeta({
|
|
10
|
+
openAPI: {
|
|
11
|
+
tags: ['General'],
|
|
12
|
+
operationId: 'generalProfilePatch',
|
|
13
|
+
summary: 'Update the current user profile',
|
|
14
|
+
description:
|
|
15
|
+
'Update the current user profile. Uses the current authenticated session and enforces the relevant portal permissions.'
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Zod schema for profile update request
|
|
20
|
+
const updateProfileSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
name: z
|
|
23
|
+
.string()
|
|
24
|
+
.trim()
|
|
25
|
+
.min(1, 'Name must be a non-empty string')
|
|
26
|
+
.max(255, 'Name must be less than 255 characters')
|
|
27
|
+
.optional(),
|
|
28
|
+
image: z
|
|
29
|
+
.union([z.string().url('Image must be a valid URL'), z.literal('').transform(() => null), z.null()])
|
|
30
|
+
.optional()
|
|
31
|
+
})
|
|
32
|
+
.refine((data) => data.name !== undefined || data.image !== undefined, {
|
|
33
|
+
message: 'At least one field (name or image) must be provided'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
interface UpdateProfileResponse {
|
|
37
|
+
success: boolean
|
|
38
|
+
message: string
|
|
39
|
+
user?: {
|
|
40
|
+
id: string
|
|
41
|
+
name: string
|
|
42
|
+
email: string
|
|
43
|
+
image: string | null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default defineEventHandler(async (event): Promise<UpdateProfileResponse> => {
|
|
48
|
+
const session = await auth.api.getSession({ headers: event.headers })
|
|
49
|
+
if (!session?.user) {
|
|
50
|
+
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const user = session.user as SessionUser
|
|
54
|
+
const body = await readBody(event)
|
|
55
|
+
|
|
56
|
+
// Validate request body with Zod
|
|
57
|
+
const validationResult = updateProfileSchema.safeParse(body)
|
|
58
|
+
if (!validationResult.success) {
|
|
59
|
+
const firstError = validationResult.error.issues[0]
|
|
60
|
+
throw createError({
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
message: firstError?.message || 'Validation failed'
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const validatedData = validationResult.data
|
|
67
|
+
|
|
68
|
+
// Build update object
|
|
69
|
+
const updateData: { name?: string; image?: string | null } = {}
|
|
70
|
+
if (validatedData.name !== undefined) {
|
|
71
|
+
updateData.name = validatedData.name.trim()
|
|
72
|
+
}
|
|
73
|
+
if (validatedData.image !== undefined) {
|
|
74
|
+
updateData.image = validatedData.image
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Update user in database
|
|
78
|
+
const [updatedUser] = await db.update(userTable).set(updateData).where(eq(userTable.id, user.id)).returning({
|
|
79
|
+
id: userTable.id,
|
|
80
|
+
name: userTable.name,
|
|
81
|
+
email: userTable.email,
|
|
82
|
+
image: userTable.image
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!updatedUser) {
|
|
86
|
+
throw createError({ statusCode: 500, message: 'Failed to update profile' })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
message: 'Profile updated successfully',
|
|
92
|
+
user: {
|
|
93
|
+
id: updatedUser.id,
|
|
94
|
+
name: updatedUser.name,
|
|
95
|
+
email: updatedUser.email,
|
|
96
|
+
image: updatedUser.image
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PortalFeatureDefinition } from '@nuxt-customer-portal/core/shared/types/feature'
|
|
2
|
+
|
|
3
|
+
export const accountOrganizationsFeature: PortalFeatureDefinition = {
|
|
4
|
+
id: 'organizations',
|
|
5
|
+
navigation: [
|
|
6
|
+
{
|
|
7
|
+
id: 'my-organizations',
|
|
8
|
+
labelKey: 'myOrganizations.title',
|
|
9
|
+
icon: 'i-lucide-building',
|
|
10
|
+
to: '/my-organizations',
|
|
11
|
+
audiences: ['authenticated'],
|
|
12
|
+
order: 80
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'settings',
|
|
16
|
+
labelKey: 'menu.settings.title',
|
|
17
|
+
icon: 'i-lucide-settings',
|
|
18
|
+
to: '/settings',
|
|
19
|
+
audiences: ['authenticated'],
|
|
20
|
+
order: 90
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
policy: { owner: [], admin: [], member: [] }
|
|
24
|
+
}
|