@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.
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/app/composables/useAutoPagination.ts +30 -0
- package/app/composables/useOrganization.ts +117 -0
- package/app/composables/usePaginatedResource.ts +167 -0
- package/app/composables/usePortalFeatures.ts +74 -0
- package/app/composables/usePortalSession.ts +20 -0
- package/app/composables/useUserStore.ts +460 -0
- package/app/middleware/auth.global.ts +32 -0
- package/app/plugins/auth.ts +28 -0
- package/app/plugins/portal-core-feature.ts +3 -0
- package/app/types/index.d.ts +75 -0
- package/app/utils/auth-client.ts +31 -0
- package/app/utils/permission-utils.ts +21 -0
- package/app/utils/random.ts +9 -0
- package/i18n/locales/en.json +81 -0
- package/i18n/locales/nl.json +81 -0
- package/migrations/0000_baseline.sql +119 -0
- package/migrations/0001_organization_types.sql +12 -0
- package/migrations/0002_invitation_created_at.sql +1 -0
- package/migrations/0003_system_admin_role.sql +5 -0
- package/migrations/0004_provider_organization_type.sql +16 -0
- package/migrations/0005_portal_email_settings.sql +15 -0
- package/migrations/meta/0000_snapshot.json +880 -0
- package/migrations/meta/_journal.json +13 -0
- package/nuxt.config.ts +55 -0
- package/package.json +66 -0
- package/portal.manifest.mjs +8 -0
- package/server/api/auth/[...].ts +18 -0
- package/server/api/auth/permissions.get.ts +64 -0
- package/server/api/notifications.ts +294 -0
- package/server/db/core-schema.ts +1 -0
- package/server/db/schema/auth-schema.ts +198 -0
- package/server/db/schema/index.ts +1 -0
- package/server/middleware/api-docs-auth.ts +26 -0
- package/server/middleware/auth.ts +61 -0
- package/server/portal.ts +212 -0
- package/server/routes/api-docs/openapi.json.get.ts +22 -0
- package/server/utils/admin-email-allowlist.ts +12 -0
- package/server/utils/auth.ts +326 -0
- package/server/utils/business-hooks.ts +28 -0
- package/server/utils/db.ts +18 -0
- package/server/utils/email-template.html +105 -0
- package/server/utils/email-texts.ts +110 -0
- package/server/utils/email.ts +37 -0
- package/server/utils/invitation-management.ts +29 -0
- package/server/utils/openapi-auth.ts +67 -0
- package/server/utils/openapi-contracts.ts +129 -0
- package/server/utils/openapi-order.ts +136 -0
- package/server/utils/permissions.ts +179 -0
- package/server/utils/portal-email.ts +424 -0
- package/server/utils/query-builder.ts +270 -0
- package/server/utils/route-meta.ts +28 -0
- package/shared/core-feature.ts +152 -0
- package/shared/feature-registry.ts +62 -0
- package/shared/invitation-validation.ts +8 -0
- package/shared/organization-avatar.ts +11 -0
- package/shared/permissions.ts +26 -0
- package/shared/portal-session.ts +17 -0
- package/shared/types/feature.ts +130 -0
- package/shared/types/index.ts +40 -0
- package/shared/types/sections/api.ts +134 -0
- package/shared/types/sections/auth.ts +71 -0
- package/shared/types/sections/database.ts +86 -0
- 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
|
+
}
|