@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +3 -0
  4. package/app/components/DeleteAccountModal.vue +139 -0
  5. package/app/components/OrganizationMembers.vue +298 -0
  6. package/app/components/OrganizationSettings.vue +315 -0
  7. package/app/components/OrganizationSwitcher.vue +166 -0
  8. package/app/components/organization/DetailsCard.vue +84 -0
  9. package/app/components/organization/InvitationsCard.vue +112 -0
  10. package/app/components/organization/InviteMemberModal.vue +101 -0
  11. package/app/components/organization/MembersCard.vue +108 -0
  12. package/app/pages/my-organizations.vue +125 -0
  13. package/app/pages/settings/index.vue +248 -0
  14. package/app/pages/settings/organization.vue +171 -0
  15. package/app/pages/settings/security.vue +195 -0
  16. package/app/pages/settings.vue +101 -0
  17. package/app/plugins/account-organizations-feature.ts +3 -0
  18. package/i18n/locales/en.json +208 -0
  19. package/i18n/locales/nl.json +208 -0
  20. package/nuxt.config.ts +11 -0
  21. package/package.json +51 -0
  22. package/portal.manifest.mjs +6 -0
  23. package/server/api/organizations/[id]/index.get.ts +54 -0
  24. package/server/api/organizations/[id]/invitations/[invitationId]/delete.post.ts +42 -0
  25. package/server/api/organizations/[id]/invitations/[invitationId]/resend.post.ts +91 -0
  26. package/server/api/organizations/[id]/invitations/[invitationId].patch.ts +18 -0
  27. package/server/api/organizations/[id]/invitations.get.ts +49 -0
  28. package/server/api/organizations/accept-invitation.post.ts +131 -0
  29. package/server/api/organizations/get-invitation.get.ts +73 -0
  30. package/server/api/profile.patch.ts +99 -0
  31. 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
+ }