@nuxt-customer-portal/core 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -0
  3. package/app/components/PortalTimezoneSelect.vue +28 -0
  4. package/app/composables/useClientConfiguration.ts +8 -0
  5. package/app/composables/usePaginatedResource.ts +2 -1
  6. package/app/composables/usePortalSession.ts +2 -0
  7. package/app/composables/useUserStore.ts +5 -0
  8. package/app/utils/auth-client.ts +4 -1
  9. package/i18n/locales/en.json +22 -0
  10. package/i18n/locales/nl.json +22 -0
  11. package/migrations/0006_timezone_preferences.sql +22 -0
  12. package/migrations/0007_user_names.sql +2 -0
  13. package/migrations/0008_better_auth_api_keys.sql +27 -0
  14. package/nuxt.config.ts +16 -5
  15. package/package.json +8 -3
  16. package/portal.manifest.mjs +1 -1
  17. package/server/api/auth/permissions.get.ts +4 -1
  18. package/server/api/timezones.get.ts +9 -0
  19. package/server/db/schema/auth-schema.ts +46 -1
  20. package/server/middleware/auth.ts +10 -0
  21. package/server/utils/auth.ts +150 -12
  22. package/server/utils/business-hooks.ts +34 -0
  23. package/server/utils/client-account-policy.ts +15 -0
  24. package/server/utils/client-configuration.ts +9 -0
  25. package/server/utils/email-texts.ts +27 -0
  26. package/server/utils/email.ts +25 -2
  27. package/server/utils/invitation-management.ts +4 -0
  28. package/server/utils/openapi-contracts.ts +8 -0
  29. package/server/utils/permissions.ts +4 -0
  30. package/server/utils/portal-email.ts +12 -4
  31. package/server/utils/public-api-route.ts +14 -0
  32. package/server/utils/public-invitation-route.ts +3 -0
  33. package/server/utils/timezone-validation.ts +4 -0
  34. package/server/utils/timezones.ts +28 -0
  35. package/shared/appointment-time.ts +43 -0
  36. package/shared/core-feature.ts +82 -29
  37. package/shared/countries.ts +18 -0
  38. package/shared/email-recipient.ts +7 -0
  39. package/shared/languages.ts +7 -0
  40. package/shared/permissions.ts +25 -0
  41. package/shared/portal-session.ts +2 -0
  42. package/shared/social-auth.ts +5 -0
  43. package/shared/timezone.ts +21 -0
  44. package/shared/types/feature.ts +11 -0
  45. package/shared/types/sections/database.ts +3 -0
@@ -1,5 +1,11 @@
1
1
  import type { PortalFeatureDefinition } from './types/feature'
2
2
 
3
+ const recipientNamePlaceholder = {
4
+ key: 'recipient_name',
5
+ labelKey: 'admin.email.placeholders.recipientName',
6
+ example: 'Alex'
7
+ }
8
+
3
9
  export const coreFeature: PortalFeatureDefinition = {
4
10
  id: 'portal-core',
5
11
  navigation: [
@@ -39,16 +45,21 @@ export const coreFeature: PortalFeatureDefinition = {
39
45
  defaults: {
40
46
  en: {
41
47
  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.'
48
+ body: 'Dear {{recipient_name}},\n\nWelcome to **{{brand_name}}**. To finish verifying your email address, enter the following code:\n\n`{{otp}}`\n\nOnce your email address is verified, you can continue setting up and using your account.',
49
+ footer:
50
+ 'For your security, this code will expire soon and can only be used once. If you did not create or verify an account, you can safely ignore this email.'
44
51
  },
45
52
  nl: {
46
53
  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.'
54
+ body: 'Beste {{recipient_name}},\n\nWelkom bij **{{brand_name}}**. Voer de volgende code in om de verificatie van uw e-mailadres af te ronden:\n\n`{{otp}}`\n\nNa de verificatie kunt u doorgaan met het instellen en gebruiken van uw account.',
55
+ footer:
56
+ 'Voor uw veiligheid verloopt deze code binnenkort en kan deze maar één keer worden gebruikt. Heeft u geen account aangemaakt of geverifieerd? Dan kunt u deze e-mail veilig negeren.'
49
57
  }
50
58
  },
51
- placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
59
+ placeholders: [
60
+ recipientNamePlaceholder,
61
+ { key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
62
+ ]
52
63
  },
53
64
  {
54
65
  id: 'sign-in',
@@ -56,16 +67,21 @@ export const coreFeature: PortalFeatureDefinition = {
56
67
  defaults: {
57
68
  en: {
58
69
  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.'
70
+ body: 'Dear {{recipient_name}},\n\nWe received a request to sign in to your **{{brand_name}}** account. Use the code below to continue:\n\n`{{otp}}`\n\nReturn to the sign-in screen and enter this code to securely access your account.',
71
+ footer:
72
+ 'This code will expire soon and can only be used once. If you did not try to sign in, you can ignore this email; no changes will be made to your account.'
61
73
  },
62
74
  nl: {
63
75
  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.'
76
+ body: 'Beste {{recipient_name}},\n\nWe hebben een verzoek ontvangen om in te loggen op uw **{{brand_name}}**-account. Gebruik de onderstaande code om verder te gaan:\n\n`{{otp}}`\n\nGa terug naar het inlogscherm en voer deze code in om veilig toegang te krijgen tot uw account.',
77
+ footer:
78
+ 'Deze code verloopt binnenkort en kan maar één keer worden gebruikt. Heeft u niet geprobeerd in te loggen? Dan kunt u deze e-mail negeren; er wordt niets aan uw account gewijzigd.'
66
79
  }
67
80
  },
68
- placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
81
+ placeholders: [
82
+ recipientNamePlaceholder,
83
+ { key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
84
+ ]
69
85
  },
70
86
  {
71
87
  id: 'forget-password',
@@ -73,16 +89,21 @@ export const coreFeature: PortalFeatureDefinition = {
73
89
  defaults: {
74
90
  en: {
75
91
  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.'
92
+ body: 'Dear {{recipient_name}},\n\nWe received a request to reset the password for your **{{brand_name}}** account. Enter the following code on the password reset screen:\n\n`{{otp}}`\n\nAfter verification, you will be able to choose a new password.',
93
+ footer:
94
+ 'For your security, this code will expire soon and can only be used once. If you did not request a password reset, ignore this email and your current password will remain unchanged.'
78
95
  },
79
96
  nl: {
80
97
  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.'
98
+ body: 'Beste {{recipient_name}},\n\nWe hebben een verzoek ontvangen om het wachtwoord van uw **{{brand_name}}**-account opnieuw in te stellen. Voer de volgende code in op het scherm voor wachtwoordherstel:\n\n`{{otp}}`\n\nNa de verificatie kunt u een nieuw wachtwoord kiezen.',
99
+ footer:
100
+ 'Voor uw veiligheid verloopt deze code binnenkort en kan deze maar één keer worden gebruikt. Heeft u geen wachtwoordherstel aangevraagd? Negeer dan deze e-mail; uw huidige wachtwoord blijft ongewijzigd.'
83
101
  }
84
102
  },
85
- placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
103
+ placeholders: [
104
+ recipientNamePlaceholder,
105
+ { key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
106
+ ]
86
107
  },
87
108
  {
88
109
  id: 'change-email',
@@ -90,16 +111,21 @@ export const coreFeature: PortalFeatureDefinition = {
90
111
  defaults: {
91
112
  en: {
92
113
  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.'
114
+ body: 'Dear {{recipient_name}},\n\nYou are changing the email address connected to your **{{brand_name}}** account. Enter the following code to confirm that this new address belongs to you:\n\n`{{otp}}`\n\nAfter confirmation, future account messages will be sent to this address.',
115
+ footer:
116
+ 'This code will expire soon and can only be used once. If you did not request this change, ignore this email and review your account security.'
95
117
  },
96
118
  nl: {
97
119
  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.'
120
+ body: 'Beste {{recipient_name}},\n\nU wijzigt het e-mailadres dat aan uw **{{brand_name}}**-account is gekoppeld. Voer de volgende code in om te bevestigen dat dit nieuwe adres van u is:\n\n`{{otp}}`\n\nNa de bevestiging worden toekomstige accountberichten naar dit adres verzonden.',
121
+ footer:
122
+ 'Deze code verloopt binnenkort en kan maar één keer worden gebruikt. Heeft u deze wijziging niet aangevraagd? Negeer dan deze e-mail en controleer de beveiliging van uw account.'
100
123
  }
101
124
  },
102
- placeholders: [{ key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }]
125
+ placeholders: [
126
+ recipientNamePlaceholder,
127
+ { key: 'otp', labelKey: 'admin.email.placeholders.otp', example: '123456' }
128
+ ]
103
129
  },
104
130
  {
105
131
  id: 'invitation',
@@ -107,38 +133,65 @@ export const coreFeature: PortalFeatureDefinition = {
107
133
  defaults: {
108
134
  en: {
109
135
  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.'
136
+ body: 'Dear {{recipient_name}},\n\n**{{inviter_name}}** has invited you to join **{{organization_name}}** as **{{role}}**. By accepting, you will gain access to the organization and the features available for your role.\n\n[Review and accept the invitation]({{action_url}})\n\nWe look forward to welcoming you.',
137
+ footer:
138
+ 'This personal invitation will expire soon. If you were not expecting it, you can safely ignore this email.'
112
139
  },
113
140
  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.'
141
+ subject: 'Je bent uitgenodigd voor {{organization_name}}',
142
+ body: 'Beste {{recipient_name}},\n\n**{{inviter_name}}** heeft je uitgenodigd om als **{{role}}** lid te worden van **{{organization_name}}**. Na acceptatie krijg je toegang tot de organisatie en de functies die bij je rol horen.\n\n[Uitnodiging bekijken en accepteren]({{action_url}})\n\nWe heten je graag welkom.',
143
+ footer:
144
+ 'Deze persoonlijke uitnodiging verloopt binnenkort. Verwachtte je deze uitnodiging niet? Dan kun je deze e-mail veilig negeren.'
117
145
  }
118
146
  },
119
147
  placeholders: [
148
+ recipientNamePlaceholder,
120
149
  { key: 'inviter_name', labelKey: 'admin.email.placeholders.inviterName', example: 'Alex Morgan' },
121
150
  { key: 'organization_name', labelKey: 'admin.email.placeholders.organizationName', example: 'Example Company' },
122
151
  { key: 'role', labelKey: 'admin.email.placeholders.role', example: 'member' },
123
152
  { key: 'action_url', labelKey: 'admin.email.placeholders.actionUrl', example: 'https://example.com/signup' }
124
153
  ]
125
154
  },
155
+ {
156
+ id: 'personal-account-invitation',
157
+ labelKey: 'admin.email.messages.personalAccountInvitation',
158
+ defaults: {
159
+ en: {
160
+ subject: 'Set up your personal portal account',
161
+ body: 'Dear {{recipient_name}},\n\nA personal portal profile has been created for you. Set up your account to access your purchases, appointments, invoices, and shared files.\n\n[Set up your account]({{action_url}})\n\nUse the same email address that received this message.',
162
+ footer: 'This account setup link expires soon. If you did not expect this message, you can safely ignore it.'
163
+ },
164
+ nl: {
165
+ subject: 'Stel je persoonlijke portaalaccount in',
166
+ body: 'Beste {{recipient_name}},\n\nEr is een persoonlijk portaalprofiel voor je aangemaakt. Stel je account in om toegang te krijgen tot je aankopen, afspraken, facturen en gedeelde bestanden.\n\n[Je account instellen]({{action_url}})\n\nGebruik hetzelfde e-mailadres waarop je dit bericht hebt ontvangen.',
167
+ footer:
168
+ 'Deze link om je account in te stellen verloopt binnenkort. Verwachtte je dit bericht niet? Dan kun je het veilig negeren.'
169
+ }
170
+ },
171
+ placeholders: [
172
+ recipientNamePlaceholder,
173
+ { key: 'action_url', labelKey: 'admin.email.placeholders.actionUrl', example: 'https://example.com/signup' }
174
+ ]
175
+ },
126
176
  {
127
177
  id: 'account-deletion',
128
178
  labelKey: 'admin.email.messages.accountDeletion',
129
179
  defaults: {
130
180
  en: {
131
181
  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.'
182
+ body: 'Dear {{recipient_name}},\n\nWe received a request to permanently delete your account. Because this action cannot be undone, please confirm that you want to continue.\n\n[Confirm account deletion]({{action_url}})\n\nYour account will remain active until you complete this confirmation.',
183
+ footer:
184
+ 'If you did not request account deletion, do not use the link. You can safely ignore this email and your account will remain active.'
134
185
  },
135
186
  nl: {
136
187
  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.'
188
+ body: 'Beste {{recipient_name}},\n\nWe hebben een verzoek ontvangen om uw account definitief te verwijderen. Omdat dit niet ongedaan kan worden gemaakt, vragen we u te bevestigen dat u wilt doorgaan.\n\n[Verwijderen van account bevestigen]({{action_url}})\n\nUw account blijft actief totdat u deze bevestiging heeft voltooid.',
189
+ footer:
190
+ 'Heeft u niet gevraagd om uw account te verwijderen? Gebruik de link dan niet. U kunt deze e-mail veilig negeren; uw account blijft actief.'
139
191
  }
140
192
  },
141
193
  placeholders: [
194
+ recipientNamePlaceholder,
142
195
  { key: 'user_name', labelKey: 'admin.email.placeholders.userName', example: 'Alex Morgan' },
143
196
  {
144
197
  key: 'action_url',
@@ -0,0 +1,18 @@
1
+ // ISO 3166-1 alpha-2 codes; display names are supplied by the browser's Intl data.
2
+ export const countryCodes =
3
+ 'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW'.split(
4
+ ' '
5
+ )
6
+
7
+ export function browserCountry(languages: readonly string[]): string | undefined {
8
+ for (const language of languages) {
9
+ try {
10
+ const region = new Intl.Locale(language).region
11
+ if (region && countryCodes.includes(region)) {
12
+ return region
13
+ }
14
+ } catch {
15
+ // Ignore unsupported language tags.
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ export function emailRecipientName(input: {
2
+ firstName?: string | null
3
+ displayName?: string | null
4
+ email?: string | null
5
+ }) {
6
+ return input.firstName?.trim() || input.displayName?.trim() || input.email?.trim() || ''
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Languages with bundled portal interface translations. */
2
+ export const portalLanguageCodes = ['en', 'nl'] as const
3
+ export type PortalLanguage = (typeof portalLanguageCodes)[number]
4
+ export const portalLanguages = [
5
+ { value: 'en' as const, label: '🇺🇸 English' },
6
+ { value: 'nl' as const, label: '🇳🇱 Nederlands' }
7
+ ]
@@ -1,5 +1,11 @@
1
1
  import { createAccessControl } from 'better-auth/plugins/access'
2
2
  import { defaultStatements, adminAc } from 'better-auth/plugins/admin/access'
3
+ import {
4
+ defaultStatements as organizationDefaultStatements,
5
+ adminAc as organizationAdminAc,
6
+ memberAc as organizationMemberAc,
7
+ ownerAc as organizationOwnerAc
8
+ } from 'better-auth/plugins/organization/access'
3
9
 
4
10
  // Core portal statements. Feature authorization is owned by each feature policy.
5
11
  export const statement = {
@@ -24,3 +30,22 @@ export const admin = ac.newRole({
24
30
 
25
31
  // Export the access control instance and roles
26
32
  export { ac }
33
+
34
+ export const organizationStatement = {
35
+ ...organizationDefaultStatements,
36
+ apiKey: ['create', 'read', 'update', 'delete']
37
+ } as const
38
+
39
+ export const organizationAc = createAccessControl(organizationStatement)
40
+ export const organizationOwner = organizationAc.newRole({
41
+ ...organizationOwnerAc.statements,
42
+ apiKey: ['create', 'read', 'update', 'delete']
43
+ })
44
+ export const organizationAdmin = organizationAc.newRole({
45
+ ...organizationAdminAc.statements,
46
+ apiKey: ['create', 'read', 'update', 'delete']
47
+ })
48
+ export const organizationMember = organizationAc.newRole({
49
+ ...organizationMemberAc.statements,
50
+ apiKey: []
51
+ })
@@ -4,6 +4,8 @@ export interface PortalSession {
4
4
  name?: string | null
5
5
  email?: string
6
6
  role?: string | null
7
+ emailVerified?: boolean
8
+ timezone?: string | null
7
9
  }
8
10
  session?: {
9
11
  activeOrganizationId?: string | null
@@ -0,0 +1,5 @@
1
+ export const socialAuthProviderEnabled = (
2
+ enabled: string | undefined,
3
+ clientId: string | undefined,
4
+ clientSecret: string | undefined
5
+ ) => enabled === 'true' && Boolean(clientId?.trim()) && Boolean(clientSecret?.trim())
@@ -0,0 +1,21 @@
1
+ /** Named zones retain daylight-saving rules; offsets do not. */
2
+ export const DEFAULT_TIMEZONE = 'Europe/Amsterdam'
3
+ export const isValidTimezone = (value: string): boolean => {
4
+ if (!value || /^[+-]/.test(value)) {
5
+ return false
6
+ }
7
+ try {
8
+ new Intl.DateTimeFormat('en', { timeZone: value }).format()
9
+ return true
10
+ } catch {
11
+ return false
12
+ }
13
+ }
14
+ export const resolveTimezones = (input: {
15
+ providerTimezone: string
16
+ clientTimezone?: string | null
17
+ userTimezone?: string | null
18
+ }) => {
19
+ const schedulingTimezone = input.clientTimezone || input.providerTimezone
20
+ return { ...input, schedulingTimezone, displayTimezone: input.userTimezone || schedulingTimezone }
21
+ }
@@ -7,6 +7,7 @@ export type PortalAudience =
7
7
  | 'providerAdmin'
8
8
  | 'clientAuthenticated'
9
9
  | 'clientAdmin'
10
+ | 'clientOrganizationAdmin'
10
11
  export type PortalOrganizationRole = 'owner' | 'admin' | 'member'
11
12
  export type PortalOrganizationType = 'PROVIDER' | 'CLIENT'
12
13
 
@@ -58,6 +59,7 @@ export interface PortalModuleContribution {
58
59
  to: string
59
60
  routePrefixes: string[]
60
61
  audiences: PortalAudience[]
62
+ navigationAudiences?: PortalAudience[]
61
63
  order?: number
62
64
  menuItems?: readonly PortalModuleMenuItem[]
63
65
  }
@@ -117,6 +119,13 @@ export interface PortalEmailDefinition {
117
119
  placeholders: readonly PortalEmailPlaceholder[]
118
120
  }
119
121
 
122
+ export interface PortalApiScopeDefinition {
123
+ id: string
124
+ action: string
125
+ labelKey: string
126
+ descriptionKey?: string
127
+ }
128
+
120
129
  export interface PortalFeatureDefinition<Action extends string = string> {
121
130
  id: string
122
131
  navigation?: readonly PortalNavigationItem[]
@@ -126,5 +135,7 @@ export interface PortalFeatureDefinition<Action extends string = string> {
126
135
  surfaces?: readonly PortalSurfaceContribution[]
127
136
  clientIntegration?: PortalClientIntegration
128
137
  emails?: readonly PortalEmailDefinition[]
138
+ /** API capabilities this module intentionally exposes to organization API keys. */
139
+ apiScopes?: readonly PortalApiScopeDefinition[]
129
140
  policy: PortalFeaturePolicy<Action>
130
141
  }
@@ -7,6 +7,9 @@ export interface User {
7
7
  id: string
8
8
  name: string
9
9
  email: string
10
+ firstName?: string | null
11
+ lastName?: string | null
12
+ timezone?: string | null
10
13
  emailVerified: boolean
11
14
  image?: string
12
15
  createdAt: Date