@nuxt-customer-portal/core 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 (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/app/composables/useAutoPagination.ts +30 -0
  5. package/app/composables/useOrganization.ts +117 -0
  6. package/app/composables/usePaginatedResource.ts +167 -0
  7. package/app/composables/usePortalFeatures.ts +74 -0
  8. package/app/composables/usePortalSession.ts +20 -0
  9. package/app/composables/useUserStore.ts +460 -0
  10. package/app/middleware/auth.global.ts +32 -0
  11. package/app/plugins/auth.ts +28 -0
  12. package/app/plugins/portal-core-feature.ts +3 -0
  13. package/app/types/index.d.ts +75 -0
  14. package/app/utils/auth-client.ts +31 -0
  15. package/app/utils/permission-utils.ts +21 -0
  16. package/app/utils/random.ts +9 -0
  17. package/i18n/locales/en.json +81 -0
  18. package/i18n/locales/nl.json +81 -0
  19. package/migrations/0000_baseline.sql +119 -0
  20. package/migrations/0001_organization_types.sql +12 -0
  21. package/migrations/0002_invitation_created_at.sql +1 -0
  22. package/migrations/0003_system_admin_role.sql +5 -0
  23. package/migrations/0004_provider_organization_type.sql +16 -0
  24. package/migrations/0005_portal_email_settings.sql +15 -0
  25. package/migrations/meta/0000_snapshot.json +880 -0
  26. package/migrations/meta/_journal.json +13 -0
  27. package/nuxt.config.ts +55 -0
  28. package/package.json +66 -0
  29. package/portal.manifest.mjs +8 -0
  30. package/server/api/auth/[...].ts +18 -0
  31. package/server/api/auth/permissions.get.ts +64 -0
  32. package/server/api/notifications.ts +294 -0
  33. package/server/db/core-schema.ts +1 -0
  34. package/server/db/schema/auth-schema.ts +198 -0
  35. package/server/db/schema/index.ts +1 -0
  36. package/server/middleware/api-docs-auth.ts +26 -0
  37. package/server/middleware/auth.ts +61 -0
  38. package/server/portal.ts +212 -0
  39. package/server/routes/api-docs/openapi.json.get.ts +22 -0
  40. package/server/utils/admin-email-allowlist.ts +12 -0
  41. package/server/utils/auth.ts +326 -0
  42. package/server/utils/business-hooks.ts +28 -0
  43. package/server/utils/db.ts +18 -0
  44. package/server/utils/email-template.html +105 -0
  45. package/server/utils/email-texts.ts +110 -0
  46. package/server/utils/email.ts +37 -0
  47. package/server/utils/invitation-management.ts +29 -0
  48. package/server/utils/openapi-auth.ts +67 -0
  49. package/server/utils/openapi-contracts.ts +129 -0
  50. package/server/utils/openapi-order.ts +136 -0
  51. package/server/utils/permissions.ts +179 -0
  52. package/server/utils/portal-email.ts +424 -0
  53. package/server/utils/query-builder.ts +270 -0
  54. package/server/utils/route-meta.ts +28 -0
  55. package/shared/core-feature.ts +152 -0
  56. package/shared/feature-registry.ts +62 -0
  57. package/shared/invitation-validation.ts +8 -0
  58. package/shared/organization-avatar.ts +11 -0
  59. package/shared/permissions.ts +26 -0
  60. package/shared/portal-session.ts +17 -0
  61. package/shared/types/feature.ts +130 -0
  62. package/shared/types/index.ts +40 -0
  63. package/shared/types/sections/api.ts +134 -0
  64. package/shared/types/sections/auth.ts +71 -0
  65. package/shared/types/sections/database.ts +86 -0
  66. package/shared/types/sections/queryBuilder.ts +31 -0
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod'
2
+
3
+ type RouteMetaInput = {
4
+ openAPI: Record<string, unknown>
5
+ query?: z.ZodType
6
+ body?: z.ZodType
7
+ }
8
+
9
+ const jsonSchema = (schema: z.ZodType): Record<string, unknown> => {
10
+ const result = z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' }) as Record<string, unknown>
11
+ delete result.$schema
12
+ return result
13
+ }
14
+
15
+ export const definePortalRouteMeta = ({ openAPI, query, body }: RouteMetaInput) => ({
16
+ openAPI: {
17
+ ...openAPI,
18
+ ...(query ? { parameters: [{ in: 'query', schema: jsonSchema(query) }] } : {}),
19
+ ...(body
20
+ ? {
21
+ requestBody: {
22
+ required: true,
23
+ content: { 'application/json': { schema: jsonSchema(body) } }
24
+ }
25
+ }
26
+ : {})
27
+ }
28
+ })
@@ -0,0 +1,152 @@
1
+ import type { PortalFeatureDefinition } from './types/feature'
2
+
3
+ export const coreFeature: PortalFeatureDefinition = {
4
+ id: 'portal-core',
5
+ navigation: [
6
+ {
7
+ id: 'dashboard',
8
+ labelKey: 'menu.dashboard',
9
+ icon: 'i-lucide-layout-dashboard',
10
+ to: '/dashboard',
11
+ audiences: ['authenticated'],
12
+ order: 10
13
+ }
14
+ ],
15
+ modules: [
16
+ {
17
+ id: 'dashboard',
18
+ labelKey: 'menu.dashboard',
19
+ icon: 'i-lucide-layout-dashboard',
20
+ to: '/dashboard',
21
+ routePrefixes: ['/dashboard'],
22
+ audiences: ['authenticated'],
23
+ order: 10,
24
+ menuItems: [
25
+ {
26
+ id: 'dashboard',
27
+ labelKey: 'menu.dashboard',
28
+ icon: 'i-lucide-layout-dashboard',
29
+ to: '/dashboard',
30
+ audiences: ['authenticated']
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ emails: [
36
+ {
37
+ id: 'email-verification',
38
+ labelKey: 'admin.email.messages.emailVerification',
39
+ defaults: {
40
+ en: {
41
+ subject: 'Verify your {{brand_name}} email address',
42
+ body: 'Your verification code is: <code>{{otp}}</code>',
43
+ footer: 'This code will expire soon. If you did not request this, please ignore this email.'
44
+ },
45
+ nl: {
46
+ subject: 'Verifieer uw e-mailadres voor {{brand_name}}',
47
+ body: 'Uw verificatiecode is: <code>{{otp}}</code>',
48
+ footer: 'Deze code verloopt binnenkort. Negeer dit bericht als u dit niet heeft aangevraagd.'
49
+ }
50
+ },
51
+ placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
52
+ },
53
+ {
54
+ id: 'sign-in',
55
+ labelKey: 'admin.email.messages.signIn',
56
+ defaults: {
57
+ en: {
58
+ subject: 'Your {{brand_name}} sign-in code',
59
+ body: 'Your sign-in code is: <code>{{otp}}</code>',
60
+ footer: 'This code will expire soon. If you did not request this, please ignore this email.'
61
+ },
62
+ nl: {
63
+ subject: 'Uw inlogcode voor {{brand_name}}',
64
+ body: 'Uw inlogcode is: <code>{{otp}}</code>',
65
+ footer: 'Deze code verloopt binnenkort. Negeer dit bericht als u dit niet heeft aangevraagd.'
66
+ }
67
+ },
68
+ placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
69
+ },
70
+ {
71
+ id: 'forget-password',
72
+ labelKey: 'admin.email.messages.passwordReset',
73
+ defaults: {
74
+ en: {
75
+ subject: 'Reset your {{brand_name}} password',
76
+ body: 'Your password reset code is: <code>{{otp}}</code>',
77
+ footer: 'This code will expire soon. If you did not request this, please ignore this email.'
78
+ },
79
+ nl: {
80
+ subject: 'Stel uw wachtwoord voor {{brand_name}} opnieuw in',
81
+ body: 'Uw code voor het opnieuw instellen van uw wachtwoord is: <code>{{otp}}</code>',
82
+ footer: 'Deze code verloopt binnenkort. Negeer dit bericht als u dit niet heeft aangevraagd.'
83
+ }
84
+ },
85
+ placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
86
+ },
87
+ {
88
+ id: 'change-email',
89
+ labelKey: 'admin.email.messages.changeEmail',
90
+ defaults: {
91
+ en: {
92
+ subject: 'Confirm your new {{brand_name}} email address',
93
+ body: 'Your confirmation code is: <code>{{otp}}</code>',
94
+ footer: 'This code will expire soon. If you did not request this, please ignore this email.'
95
+ },
96
+ nl: {
97
+ subject: 'Bevestig uw nieuwe e-mailadres voor {{brand_name}}',
98
+ body: 'Uw bevestigingscode is: <code>{{otp}}</code>',
99
+ footer: 'Deze code verloopt binnenkort. Negeer dit bericht als u dit niet heeft aangevraagd.'
100
+ }
101
+ },
102
+ placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
103
+ },
104
+ {
105
+ id: 'invitation',
106
+ labelKey: 'admin.email.messages.invitation',
107
+ defaults: {
108
+ en: {
109
+ subject: "You've been invited to join {{organization_name}}",
110
+ body: '{{inviter_name}} has invited you to join <strong>{{organization_name}}</strong> as {{role}}.<p style="text-align:center"><a href="{{action_url}}" class="button">Accept invitation</a></p>',
111
+ footer: 'This invitation will expire soon. If you did not expect it, please ignore this email.'
112
+ },
113
+ nl: {
114
+ subject: 'U bent uitgenodigd voor {{organization_name}}',
115
+ body: '{{inviter_name}} heeft u uitgenodigd om als {{role}} lid te worden van <strong>{{organization_name}}</strong>.<p style="text-align:center"><a href="{{action_url}}" class="button">Uitnodiging accepteren</a></p>',
116
+ footer: 'Deze uitnodiging verloopt binnenkort. Negeer dit bericht als u dit niet verwachtte.'
117
+ }
118
+ },
119
+ placeholders: [
120
+ { key: 'inviter_name', labelKey: 'admin.email.placeholders.inviterName', example: 'Alex Morgan' },
121
+ { key: 'organization_name', labelKey: 'admin.email.placeholders.organizationName', example: 'Example Company' },
122
+ { key: 'role', labelKey: 'admin.email.placeholders.role', example: 'member' },
123
+ { key: 'action_url', labelKey: 'admin.email.placeholders.actionUrl', example: 'https://example.com/signup' }
124
+ ]
125
+ },
126
+ {
127
+ id: 'account-deletion',
128
+ labelKey: 'admin.email.messages.accountDeletion',
129
+ defaults: {
130
+ en: {
131
+ subject: 'Confirm account deletion',
132
+ body: 'Hello {{user_name}}, you requested to delete your account.<p style="text-align:center"><a href="{{action_url}}" class="button">Delete account</a></p>',
133
+ footer: 'If you did not request this, ignore this email and your account will remain active.'
134
+ },
135
+ nl: {
136
+ subject: 'Bevestig het verwijderen van uw account',
137
+ body: 'Hallo {{user_name}}, u heeft gevraagd uw account te verwijderen.<p style="text-align:center"><a href="{{action_url}}" class="button">Account verwijderen</a></p>',
138
+ footer: 'Negeer dit bericht als u dit niet heeft aangevraagd; uw account blijft dan actief.'
139
+ }
140
+ },
141
+ placeholders: [
142
+ { key: 'user_name', labelKey: 'admin.email.placeholders.userName', example: 'Alex Morgan' },
143
+ {
144
+ key: 'action_url',
145
+ labelKey: 'admin.email.placeholders.actionUrl',
146
+ example: 'https://example.com/delete-account'
147
+ }
148
+ ]
149
+ }
150
+ ],
151
+ policy: { owner: [], admin: [], member: [] }
152
+ }
@@ -0,0 +1,62 @@
1
+ import type {
2
+ PortalFeatureDefinition,
3
+ PortalFeaturePolicy,
4
+ PortalModuleContribution,
5
+ PortalModuleMenuContribution,
6
+ PortalOrganizationRole,
7
+ PortalOrganizationType,
8
+ PortalRolePolicy
9
+ } from './types/feature'
10
+
11
+ export const mergePortalModuleMenuContributions = (
12
+ modules: readonly PortalModuleContribution[],
13
+ contributions: readonly PortalModuleMenuContribution[]
14
+ ): PortalModuleContribution[] =>
15
+ modules.map((module) => {
16
+ const additions = contributions
17
+ .filter((contribution) => contribution.moduleId === module.id)
18
+ .map((contribution) => contribution.item)
19
+ const items = new Map((module.menuItems ?? []).map((item) => [item.id, item]))
20
+ for (const item of additions) {
21
+ items.set(item.id, item)
22
+ }
23
+ return { ...module, menuItems: [...items.values()] }
24
+ })
25
+
26
+ export const upsertPortalFeature = (
27
+ features: PortalFeatureDefinition[],
28
+ feature: PortalFeatureDefinition
29
+ ): PortalFeatureDefinition[] => {
30
+ const index = features.findIndex((item) => item.id === feature.id)
31
+ if (index === -1) {
32
+ return [...features, feature]
33
+ }
34
+ return features.map((item, itemIndex) => (itemIndex === index ? feature : item))
35
+ }
36
+
37
+ const dashboardAreaOrder = { attention: 0, main: 1, aside: 2 } as const
38
+
39
+ export const sortPortalDashboardWidgets = <
40
+ T extends { id: string; area: keyof typeof dashboardAreaOrder; order: number }
41
+ >(
42
+ widgets: readonly T[]
43
+ ): T[] =>
44
+ [...widgets].sort(
45
+ (left, right) =>
46
+ dashboardAreaOrder[left.area] - dashboardAreaOrder[right.area] ||
47
+ left.order - right.order ||
48
+ left.id.localeCompare(right.id)
49
+ )
50
+
51
+ export const isPortalActionAllowed = <Action extends string>(
52
+ policy: PortalFeaturePolicy<Action>,
53
+ role: PortalOrganizationRole | null,
54
+ action: Action,
55
+ organizationType: PortalOrganizationType = 'PROVIDER'
56
+ ): boolean => {
57
+ const contextual = ('PROVIDER' in policy ? policy[organizationType] : policy) as PortalRolePolicy<Action>
58
+ return Boolean(role && contextual[role].includes(action))
59
+ }
60
+
61
+ export const canViewOrganizationDirectory = (role: PortalOrganizationRole | string | null | undefined): boolean =>
62
+ role === 'owner' || role === 'admin'
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod'
2
+
3
+ export const invitationRoleSchema = z.object({ role: z.enum(['member', 'admin', 'owner']) }).strict()
4
+ export type InvitationRole = z.infer<typeof invitationRoleSchema>['role']
5
+ export const invitationChangeSchema = z.union([
6
+ invitationRoleSchema,
7
+ z.object({ status: z.literal('canceled') }).strict()
8
+ ])
@@ -0,0 +1,11 @@
1
+ export const getOrganizationAvatar = (organization: { metadata?: string | null; logo?: string | null }) => {
2
+ try {
3
+ const metadata = JSON.parse(organization.metadata || '{}')
4
+ if (typeof metadata?.avatarLogo === 'string' && metadata.avatarLogo) {
5
+ return metadata.avatarLogo
6
+ }
7
+ } catch {
8
+ // Older organizations may not have valid metadata.
9
+ }
10
+ return organization.logo || undefined
11
+ }
@@ -0,0 +1,26 @@
1
+ import { createAccessControl } from 'better-auth/plugins/access'
2
+ import { defaultStatements, adminAc } from 'better-auth/plugins/admin/access'
3
+
4
+ // Core portal statements. Feature authorization is owned by each feature policy.
5
+ export const statement = {
6
+ ...defaultStatements,
7
+ // Organization-related permissions
8
+ organization: ['read', 'create', 'update', 'delete'],
9
+ member: ['read', 'list', 'create', 'update', 'delete', 'update-name'],
10
+ invitation: ['list', 'create', 'resend', 'cancel', 'delete']
11
+ } as const
12
+
13
+ // Create the access control instance
14
+ const ac = createAccessControl(statement)
15
+
16
+ // Define our roles with their permissions
17
+ export const user = ac.newRole({
18
+ organization: ['read']
19
+ })
20
+
21
+ export const admin = ac.newRole({
22
+ ...adminAc.statements
23
+ })
24
+
25
+ // Export the access control instance and roles
26
+ export { ac }
@@ -0,0 +1,17 @@
1
+ export interface PortalSession {
2
+ user: {
3
+ id: string
4
+ name?: string | null
5
+ email?: string
6
+ role?: string | null
7
+ }
8
+ session?: {
9
+ activeOrganizationId?: string | null
10
+ }
11
+ activeOrganizationId?: string | null
12
+ }
13
+
14
+ export type PortalOrganizationType = 'PROVIDER' | 'CLIENT'
15
+
16
+ export const getActiveOrganizationId = (session: PortalSession): string | null =>
17
+ session.session?.activeOrganizationId ?? session.activeOrganizationId ?? null
@@ -0,0 +1,130 @@
1
+ export type PortalAudience =
2
+ | 'public'
3
+ | 'authenticated'
4
+ | 'organizationAdmin'
5
+ | 'admin'
6
+ | 'providerAuthenticated'
7
+ | 'providerAdmin'
8
+ | 'clientAuthenticated'
9
+ | 'clientAdmin'
10
+ export type PortalOrganizationRole = 'owner' | 'admin' | 'member'
11
+ export type PortalOrganizationType = 'PROVIDER' | 'CLIENT'
12
+
13
+ export interface PortalBadge {
14
+ label: string | number
15
+ color?: 'primary' | 'neutral' | 'success' | 'info' | 'warning' | 'error'
16
+ variant?: 'solid' | 'outline' | 'soft' | 'subtle'
17
+ square?: boolean
18
+ class?: string
19
+ }
20
+
21
+ export type PortalBadgeValue = string | number | PortalBadge
22
+
23
+ export interface PortalNavigationItem {
24
+ id: string
25
+ labelKey: string
26
+ icon?: string
27
+ to: string
28
+ audiences: PortalAudience[]
29
+ location?: 'main' | 'admin'
30
+ order?: number
31
+ badge?: PortalBadgeValue
32
+ }
33
+
34
+ export interface PortalDashboardWidget {
35
+ id: string
36
+ component: string
37
+ area: 'attention' | 'main' | 'aside'
38
+ size: 'full' | 'half' | 'third'
39
+ order: number
40
+ }
41
+
42
+ export interface PortalModuleMenuItem {
43
+ id: string
44
+ labelKey: string
45
+ icon?: string
46
+ to: string
47
+ exact?: boolean
48
+ audiences: PortalAudience[]
49
+ order?: number
50
+ badge?: PortalBadgeValue
51
+ }
52
+
53
+ export interface PortalModuleContribution {
54
+ id: string
55
+ labelKey: string
56
+ icon?: string
57
+ badge?: PortalBadgeValue
58
+ to: string
59
+ routePrefixes: string[]
60
+ audiences: PortalAudience[]
61
+ order?: number
62
+ menuItems?: readonly PortalModuleMenuItem[]
63
+ }
64
+
65
+ export interface PortalModuleMenuContribution {
66
+ moduleId: string
67
+ item: PortalModuleMenuItem
68
+ }
69
+
70
+ export interface PortalRolePolicy<Action extends string = string> {
71
+ owner: readonly Action[]
72
+ admin: readonly Action[]
73
+ member: readonly Action[]
74
+ }
75
+
76
+ export type PortalFeaturePolicy<Action extends string = string> =
77
+ | PortalRolePolicy<Action>
78
+ | {
79
+ PROVIDER: PortalRolePolicy<Action>
80
+ CLIENT: PortalRolePolicy<Action>
81
+ }
82
+
83
+ export interface PortalClientIntegration {
84
+ moduleId: string
85
+ labelKey: string
86
+ descriptionKey?: string
87
+ detailComponent?: string
88
+ }
89
+
90
+ export interface PortalSurfaceContribution {
91
+ id: string
92
+ surface: 'administration.organization.detail'
93
+ component: string
94
+ order?: number
95
+ }
96
+
97
+ export type PortalEmailLocale = 'en' | 'nl'
98
+
99
+ export interface PortalEmailPlaceholder {
100
+ key: string
101
+ labelKey: string
102
+ descriptionKey?: string
103
+ example: string
104
+ }
105
+
106
+ export interface PortalEmailText {
107
+ subject: string
108
+ body: string
109
+ footer?: string
110
+ }
111
+
112
+ export interface PortalEmailDefinition {
113
+ id: string
114
+ labelKey: string
115
+ descriptionKey?: string
116
+ defaults: Record<PortalEmailLocale, PortalEmailText>
117
+ placeholders: readonly PortalEmailPlaceholder[]
118
+ }
119
+
120
+ export interface PortalFeatureDefinition<Action extends string = string> {
121
+ id: string
122
+ navigation?: readonly PortalNavigationItem[]
123
+ modules?: readonly PortalModuleContribution[]
124
+ moduleMenuItems?: readonly PortalModuleMenuContribution[]
125
+ dashboardWidgets?: readonly PortalDashboardWidget[]
126
+ surfaces?: readonly PortalSurfaceContribution[]
127
+ clientIntegration?: PortalClientIntegration
128
+ emails?: readonly PortalEmailDefinition[]
129
+ policy: PortalFeaturePolicy<Action>
130
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Main export file for all shared types
3
+ * This makes all types available throughout the application
4
+ */
5
+
6
+ // Database entity types
7
+ export type { User, Organization, Member, Invitation, Session, Account, Verification } from './sections/database'
8
+
9
+ // Auth-related types
10
+ export type {
11
+ UserRole,
12
+ OrganizationType,
13
+ MemberRole,
14
+ InvitationStatus,
15
+ SessionUser,
16
+ ApiError,
17
+ OrganizationMemberWithUser
18
+ } from './sections/auth'
19
+
20
+ // API request/response types
21
+ export type {
22
+ AdminOrganizationsResponse,
23
+ AdminUserResponse,
24
+ AdminUsersResponse,
25
+ UpdateUserRoleRequest,
26
+ UpdateUserRoleResponse,
27
+ OrganizationInvitationsResponse,
28
+ BanUserRequest,
29
+ UnbanUserRequest,
30
+ ImpersonateUserRequest,
31
+ ImpersonateUserResponse,
32
+ UserSession,
33
+ ListUserSessionsResponse,
34
+ RevokeUserSessionRequest,
35
+ RevokeUserSessionsRequest,
36
+ SetUserPasswordRequest,
37
+ UpdateUserRequest
38
+ } from './sections/api'
39
+
40
+ export type { FilterOperator, Filter, QueryInput, QueryResult } from './sections/queryBuilder'
@@ -0,0 +1,134 @@
1
+ /**
2
+ * API request/response types for all API endpoints
3
+ */
4
+ import type { Organization, User, Invitation } from './database'
5
+ import type { UserRole } from './auth'
6
+
7
+ /**
8
+ * GET /api/admin/organizations
9
+ * Response: Array of organizations
10
+ */
11
+ export type AdminOrganizationsResponse = Organization[]
12
+
13
+ /**
14
+ * GET /api/admin/users
15
+ * Response: Array of users with selected fields
16
+ */
17
+ export type AdminUserResponse = User
18
+
19
+ export type AdminUsersResponse = AdminUserResponse[]
20
+
21
+ /**
22
+ * PATCH /api/admin/users/[id]/role
23
+ * Request body for updating user role
24
+ */
25
+ export interface UpdateUserRoleRequest {
26
+ role: UserRole
27
+ }
28
+
29
+ /**
30
+ * PATCH /api/admin/users/[id]/role
31
+ * Response after updating user role
32
+ */
33
+ export interface UpdateUserRoleResponse {
34
+ success: boolean
35
+ message: string
36
+ }
37
+
38
+ /**
39
+ * GET /api/organizations/[id]/invitations
40
+ * Response: Array of invitations for an organization
41
+ */
42
+ export type OrganizationInvitationsResponse = Invitation[]
43
+
44
+ /**
45
+ * Admin user management types
46
+ */
47
+
48
+ /**
49
+ * Ban user request
50
+ */
51
+ export interface BanUserRequest {
52
+ userId: string
53
+ banReason?: string
54
+ banExpiresIn?: number // seconds
55
+ }
56
+
57
+ /**
58
+ * Unban user request
59
+ */
60
+ export interface UnbanUserRequest {
61
+ userId: string
62
+ }
63
+
64
+ /**
65
+ * Impersonate user request
66
+ */
67
+ export interface ImpersonateUserRequest {
68
+ userId: string
69
+ }
70
+
71
+ /**
72
+ * Impersonate user response
73
+ */
74
+ export interface ImpersonateUserResponse {
75
+ success: boolean
76
+ message?: string
77
+ }
78
+
79
+ /**
80
+ * User session from better-auth
81
+ */
82
+ export interface UserSession {
83
+ id: string
84
+ expiresAt: Date
85
+ token: string
86
+ createdAt: Date
87
+ updatedAt: Date
88
+ ipAddress?: string
89
+ userAgent?: string
90
+ userId: string
91
+ activeOrganizationId?: string
92
+ impersonatedBy?: string
93
+ }
94
+
95
+ /**
96
+ * List user sessions response
97
+ */
98
+ export interface ListUserSessionsResponse {
99
+ sessions: UserSession[]
100
+ }
101
+
102
+ /**
103
+ * Revoke user session request
104
+ */
105
+ export interface RevokeUserSessionRequest {
106
+ sessionToken: string
107
+ }
108
+
109
+ /**
110
+ * Revoke all user sessions request
111
+ */
112
+ export interface RevokeUserSessionsRequest {
113
+ userId: string
114
+ }
115
+
116
+ /**
117
+ * Set user password request
118
+ */
119
+ export interface SetUserPasswordRequest {
120
+ userId: string
121
+ newPassword: string
122
+ }
123
+
124
+ /**
125
+ * Update user request
126
+ */
127
+ export interface UpdateUserRequest {
128
+ userId: string
129
+ data: {
130
+ name?: string
131
+ image?: string | null
132
+ [key: string]: unknown
133
+ }
134
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Auth-related types including roles, session user, and permissions
3
+ */
4
+ import type { User } from './database'
5
+
6
+ /**
7
+ * User role type - possible values for user.role
8
+ */
9
+ export type UserRole = 'user' | 'admin'
10
+
11
+ export type OrganizationType = 'PROVIDER' | 'CLIENT'
12
+
13
+ /**
14
+ * Member role type - possible values for organization member roles
15
+ */
16
+ export type MemberRole = 'admin' | 'member' | 'owner'
17
+
18
+ /**
19
+ * Invitation status type
20
+ */
21
+ export type InvitationStatus = 'pending' | 'accepted' | 'rejected'
22
+
23
+ /**
24
+ * Session user extends User with providerId added by customSession hook
25
+ * This is the user object that appears in session.user
26
+ */
27
+ export interface SessionUser extends User {
28
+ providerId?: string | null
29
+ activeOrganizationId?: string | null
30
+ }
31
+
32
+ /**
33
+ * Standard API error response type
34
+ */
35
+ export interface ApiError {
36
+ statusCode: number
37
+ message: string
38
+ data?: unknown
39
+ }
40
+
41
+ /**
42
+ * Organization member with nested user object
43
+ * This is the structure returned by better-auth organization.listMembers()
44
+ */
45
+ export interface OrganizationMemberWithUser {
46
+ id: string
47
+ organizationId: string
48
+ userId: string
49
+ role: MemberRole | MemberRole[]
50
+ createdAt: Date
51
+ user: {
52
+ id: string
53
+ name: string | null
54
+ email: string
55
+ image: string | null
56
+ emailVerified: boolean
57
+ role: UserRole
58
+ createdAt: Date
59
+ updatedAt: Date
60
+ banned: boolean
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Response from listMembers API which may include pagination
66
+ */
67
+ export interface ListMembersResponse {
68
+ members?: OrganizationMemberWithUser[]
69
+ data?: OrganizationMemberWithUser[]
70
+ total?: number
71
+ }