Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/responders/user-responders.js
Show All 26 Lines | |||||
import { | import { | ||||
userSettingsTypes, | userSettingsTypes, | ||||
notificationTypeValues, | notificationTypeValues, | ||||
logInActionSources, | logInActionSources, | ||||
} from 'lib/types/account-types.js'; | } from 'lib/types/account-types.js'; | ||||
import { | import { | ||||
type ClientAvatar, | type ClientAvatar, | ||||
clientAvatarValidator, | clientAvatarValidator, | ||||
type UpdateUserAvatarRequest, | |||||
type UpdateUserAvatarResponse, | type UpdateUserAvatarResponse, | ||||
} from 'lib/types/avatar-types.js'; | } from 'lib/types/avatar-types.js'; | ||||
import type { | import type { | ||||
IdentityKeysBlob, | IdentityKeysBlob, | ||||
SignedIdentityKeysBlob, | SignedIdentityKeysBlob, | ||||
} from 'lib/types/crypto-types.js'; | } from 'lib/types/crypto-types.js'; | ||||
import { | import { | ||||
type CalendarQuery, | type CalendarQuery, | ||||
▲ Show 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | import { | ||||
fetchLoggedInUserInfo, | fetchLoggedInUserInfo, | ||||
fetchUserIDForEthereumAddress, | fetchUserIDForEthereumAddress, | ||||
} from '../fetchers/user-fetchers.js'; | } from '../fetchers/user-fetchers.js'; | ||||
import { | import { | ||||
createNewAnonymousCookie, | createNewAnonymousCookie, | ||||
createNewUserCookie, | createNewUserCookie, | ||||
setNewSession, | setNewSession, | ||||
} from '../session/cookies.js'; | } from '../session/cookies.js'; | ||||
import { verifyClientSupported } from '../session/version.js'; | |||||
import type { Viewer } from '../session/viewer.js'; | import type { Viewer } from '../session/viewer.js'; | ||||
import { | import { | ||||
accountUpdater, | accountUpdater, | ||||
checkAndSendVerificationEmail, | checkAndSendVerificationEmail, | ||||
checkAndSendPasswordResetEmail, | checkAndSendPasswordResetEmail, | ||||
updatePassword, | updatePassword, | ||||
updateUserSettings, | updateUserSettings, | ||||
updateUserAvatar, | updateUserAvatar, | ||||
} from '../updaters/account-updaters.js'; | } from '../updaters/account-updaters.js'; | ||||
import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; | import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; | ||||
import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; | import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; | ||||
import { getOlmUtility } from '../utils/olm-utils.js'; | import { getOlmUtility } from '../utils/olm-utils.js'; | ||||
import { validateInput, validateOutput } from '../utils/validation-utils.js'; | import { validateInput, validateOutput } from '../utils/validation-utils.js'; | ||||
const subscriptionUpdateRequestInputValidator = tShape({ | const subscriptionUpdateRequestInputValidator = | ||||
tShape<SubscriptionUpdateRequest>({ | |||||
threadID: tID, | threadID: tID, | ||||
updatedFields: tShape({ | updatedFields: tShape({ | ||||
pushNotifs: t.maybe(t.Boolean), | pushNotifs: t.maybe(t.Boolean), | ||||
home: t.maybe(t.Boolean), | home: t.maybe(t.Boolean), | ||||
}), | }), | ||||
}); | }); | ||||
export const subscriptionUpdateResponseValidator: TInterface<SubscriptionUpdateResponse> = | export const subscriptionUpdateResponseValidator: TInterface<SubscriptionUpdateResponse> = | ||||
tShape<SubscriptionUpdateResponse>({ | tShape<SubscriptionUpdateResponse>({ | ||||
threadSubscription: threadSubscriptionValidator, | threadSubscription: threadSubscriptionValidator, | ||||
}); | }); | ||||
async function userSubscriptionUpdateResponder( | async function userSubscriptionUpdateResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<SubscriptionUpdateResponse> { | ): Promise<SubscriptionUpdateResponse> { | ||||
const request: SubscriptionUpdateRequest = input; | const request = await validateInput( | ||||
await validateInput(viewer, subscriptionUpdateRequestInputValidator, request); | viewer, | ||||
subscriptionUpdateRequestInputValidator, | |||||
input, | |||||
); | |||||
const threadSubscription = await userSubscriptionUpdater(viewer, request); | const threadSubscription = await userSubscriptionUpdater(viewer, request); | ||||
return validateOutput(viewer, subscriptionUpdateResponseValidator, { | return validateOutput(viewer, subscriptionUpdateResponseValidator, { | ||||
threadSubscription, | threadSubscription, | ||||
}); | }); | ||||
} | } | ||||
const accountUpdateInputValidator = tShape({ | const accountUpdateInputValidator = tShape<PasswordUpdate>({ | ||||
updatedFields: tShape({ | updatedFields: tShape({ | ||||
email: t.maybe(tEmail), | email: t.maybe(tEmail), | ||||
password: t.maybe(tPassword), | password: t.maybe(tPassword), | ||||
}), | }), | ||||
currentPassword: tPassword, | currentPassword: tPassword, | ||||
}); | }); | ||||
async function passwordUpdateResponder( | async function passwordUpdateResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<void> { | ): Promise<void> { | ||||
const request: PasswordUpdate = input; | const request = await validateInput( | ||||
await validateInput(viewer, accountUpdateInputValidator, request); | viewer, | ||||
accountUpdateInputValidator, | |||||
input, | |||||
); | |||||
await accountUpdater(viewer, request); | await accountUpdater(viewer, request); | ||||
} | } | ||||
async function sendVerificationEmailResponder(viewer: Viewer): Promise<void> { | async function sendVerificationEmailResponder(viewer: Viewer): Promise<void> { | ||||
await validateInput(viewer, null, null); | if (!viewer.isSocket) { | ||||
await verifyClientSupported(viewer, viewer.platformDetails); | |||||
} | |||||
await checkAndSendVerificationEmail(viewer); | await checkAndSendVerificationEmail(viewer); | ||||
} | } | ||||
const resetPasswordRequestInputValidator = tShape({ | const resetPasswordRequestInputValidator = tShape<ResetPasswordRequest>({ | ||||
usernameOrEmail: t.union([tEmail, tOldValidUsername]), | usernameOrEmail: t.union([tEmail, tOldValidUsername]), | ||||
}); | }); | ||||
async function sendPasswordResetEmailResponder( | async function sendPasswordResetEmailResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<void> { | ): Promise<void> { | ||||
const request: ResetPasswordRequest = input; | const request = await validateInput( | ||||
await validateInput(viewer, resetPasswordRequestInputValidator, request); | viewer, | ||||
resetPasswordRequestInputValidator, | |||||
input, | |||||
); | |||||
await checkAndSendPasswordResetEmail(request); | await checkAndSendPasswordResetEmail(request); | ||||
} | } | ||||
export const logOutResponseValidator: TInterface<LogOutResponse> = | export const logOutResponseValidator: TInterface<LogOutResponse> = | ||||
tShape<LogOutResponse>({ | tShape<LogOutResponse>({ | ||||
currentUserInfo: loggedOutUserInfoValidator, | currentUserInfo: loggedOutUserInfoValidator, | ||||
}); | }); | ||||
async function logOutResponder(viewer: Viewer): Promise<LogOutResponse> { | async function logOutResponder(viewer: Viewer): Promise<LogOutResponse> { | ||||
await validateInput(viewer, null, null); | if (!viewer.isSocket) { | ||||
await verifyClientSupported(viewer, viewer.platformDetails); | |||||
} | |||||
if (viewer.loggedIn) { | if (viewer.loggedIn) { | ||||
const [anonymousViewerData] = await Promise.all([ | const [anonymousViewerData] = await Promise.all([ | ||||
createNewAnonymousCookie({ | createNewAnonymousCookie({ | ||||
platformDetails: viewer.platformDetails, | platformDetails: viewer.platformDetails, | ||||
deviceToken: viewer.deviceToken, | deviceToken: viewer.deviceToken, | ||||
}), | }), | ||||
deleteCookie(viewer.cookieID), | deleteCookie(viewer.cookieID), | ||||
]); | ]); | ||||
viewer.setNewCookie(anonymousViewerData); | viewer.setNewCookie(anonymousViewerData); | ||||
} | } | ||||
const response = { | const response = { | ||||
currentUserInfo: { | currentUserInfo: { | ||||
id: viewer.id, | id: viewer.id, | ||||
anonymous: true, | anonymous: true, | ||||
}, | }, | ||||
}; | }; | ||||
return validateOutput(viewer, logOutResponseValidator, response); | return validateOutput(viewer, logOutResponseValidator, response); | ||||
} | } | ||||
const deleteAccountRequestInputValidator = tShape({ | const deleteAccountRequestInputValidator = tShape<DeleteAccountRequest>({ | ||||
password: t.maybe(tPassword), | password: t.maybe(tPassword), | ||||
}); | }); | ||||
async function accountDeletionResponder( | async function accountDeletionResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<LogOutResponse> { | ): Promise<LogOutResponse> { | ||||
const request: DeleteAccountRequest = input; | const request = await validateInput( | ||||
await validateInput(viewer, deleteAccountRequestInputValidator, request); | viewer, | ||||
deleteAccountRequestInputValidator, | |||||
input, | |||||
); | |||||
const result = await deleteAccount(viewer, request); | const result = await deleteAccount(viewer, request); | ||||
invariant(result, 'deleteAccount should return result if handed request'); | invariant(result, 'deleteAccount should return result if handed request'); | ||||
return validateOutput(viewer, logOutResponseValidator, result); | return validateOutput(viewer, logOutResponseValidator, result); | ||||
} | } | ||||
const deviceTokenUpdateRequestInputValidator = tShape({ | const deviceTokenUpdateRequestInputValidator = tShape({ | ||||
deviceType: t.maybe(t.enums.of(['ios', 'android'])), | deviceType: t.maybe(t.enums.of(['ios', 'android'])), | ||||
deviceToken: t.String, | deviceToken: t.String, | ||||
}); | }); | ||||
const registerRequestInputValidator = tShape({ | const registerRequestInputValidator = tShape<RegisterRequest>({ | ||||
username: t.String, | username: t.String, | ||||
email: t.maybe(tEmail), | email: t.maybe(tEmail), | ||||
password: tPassword, | password: tPassword, | ||||
calendarQuery: t.maybe(newEntryQueryInputValidator), | calendarQuery: t.maybe(newEntryQueryInputValidator), | ||||
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | ||||
platformDetails: tPlatformDetails, | platformDetails: tPlatformDetails, | ||||
// We include `primaryIdentityPublicKey` to avoid breaking | // We include `primaryIdentityPublicKey` to avoid breaking | ||||
// old clients, but we no longer do anything with it. | // old clients, but we no longer do anything with it. | ||||
Show All 12 Lines | tShape<RegisterResponse>({ | ||||
cookieChange: tShape({ | cookieChange: tShape({ | ||||
threadInfos: t.dict(t.String, rawThreadInfoValidator), | threadInfos: t.dict(t.String, rawThreadInfoValidator), | ||||
userInfos: t.list(userInfoValidator), | userInfos: t.list(userInfoValidator), | ||||
}), | }), | ||||
}); | }); | ||||
async function accountCreationResponder( | async function accountCreationResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<RegisterResponse> { | ): Promise<RegisterResponse> { | ||||
const request: RegisterRequest = input; | const request = await validateInput( | ||||
await validateInput(viewer, registerRequestInputValidator, request); | viewer, | ||||
registerRequestInputValidator, | |||||
input, | |||||
); | |||||
const { signedIdentityKeysBlob } = request; | const { signedIdentityKeysBlob } = request; | ||||
if (signedIdentityKeysBlob) { | if (signedIdentityKeysBlob) { | ||||
const identityKeys: IdentityKeysBlob = JSON.parse( | const identityKeys: IdentityKeysBlob = JSON.parse( | ||||
signedIdentityKeysBlob.payload, | signedIdentityKeysBlob.payload, | ||||
); | ); | ||||
if (!identityKeysBlobValidator.is(identityKeys)) { | if (!identityKeysBlobValidator.is(identityKeys)) { | ||||
throw new ServerError('invalid_identity_keys_blob'); | throw new ServerError('invalid_identity_keys_blob'); | ||||
} | } | ||||
▲ Show 20 Lines • Show All 111 Lines • ▼ Show 20 Lines | if (rawEntryInfos) { | ||||
return { | return { | ||||
...response, | ...response, | ||||
rawEntryInfos, | rawEntryInfos, | ||||
}; | }; | ||||
} | } | ||||
return response; | return response; | ||||
} | } | ||||
const logInRequestInputValidator = tShape({ | const logInRequestInputValidator = tShape<LogInRequest>({ | ||||
username: t.maybe(t.String), | username: t.maybe(t.String), | ||||
usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), | usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), | ||||
password: tPassword, | password: tPassword, | ||||
watchedIDs: t.list(tID), | watchedIDs: t.list(tID), | ||||
calendarQuery: t.maybe(entryQueryInputValidator), | calendarQuery: t.maybe(entryQueryInputValidator), | ||||
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | ||||
platformDetails: tPlatformDetails, | platformDetails: tPlatformDetails, | ||||
source: t.maybe(t.enums.of(values(logInActionSources))), | source: t.maybe(t.enums.of(values(logInActionSources))), | ||||
Show All 18 Lines | cookieChange: tShape({ | ||||
threadInfos: t.dict(tID, rawThreadInfoValidator), | threadInfos: t.dict(tID, rawThreadInfoValidator), | ||||
userInfos: t.list(userInfoValidator), | userInfos: t.list(userInfoValidator), | ||||
}), | }), | ||||
notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), | notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), | ||||
}); | }); | ||||
async function logInResponder( | async function logInResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<LogInResponse> { | ): Promise<LogInResponse> { | ||||
await validateInput(viewer, logInRequestInputValidator, input); | const request = await validateInput( | ||||
const request: LogInRequest = input; | viewer, | ||||
logInRequestInputValidator, | |||||
input, | |||||
); | |||||
let identityKeys: ?IdentityKeysBlob; | let identityKeys: ?IdentityKeysBlob; | ||||
const { signedIdentityKeysBlob } = request; | const { signedIdentityKeysBlob } = request; | ||||
if (signedIdentityKeysBlob) { | if (signedIdentityKeysBlob) { | ||||
identityKeys = JSON.parse(signedIdentityKeysBlob.payload); | identityKeys = JSON.parse(signedIdentityKeysBlob.payload); | ||||
const olmUtil: OlmUtility = getOlmUtility(); | const olmUtil: OlmUtility = getOlmUtility(); | ||||
try { | try { | ||||
▲ Show 20 Lines • Show All 54 Lines • ▼ Show 20 Lines | const response = await processSuccessfulLogin({ | ||||
input, | input, | ||||
userID: id, | userID: id, | ||||
calendarQuery, | calendarQuery, | ||||
signedIdentityKeysBlob, | signedIdentityKeysBlob, | ||||
}); | }); | ||||
return validateOutput(viewer, logInResponseValidator, response); | return validateOutput(viewer, logInResponseValidator, response); | ||||
} | } | ||||
const siweAuthRequestInputValidator = tShape({ | const siweAuthRequestInputValidator = tShape<SIWEAuthRequest>({ | ||||
signature: t.String, | signature: t.String, | ||||
message: t.String, | message: t.String, | ||||
calendarQuery: entryQueryInputValidator, | calendarQuery: entryQueryInputValidator, | ||||
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | ||||
platformDetails: tPlatformDetails, | platformDetails: tPlatformDetails, | ||||
watchedIDs: t.list(tID), | watchedIDs: t.list(tID), | ||||
signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), | signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), | ||||
}); | }); | ||||
async function siweAuthResponder( | async function siweAuthResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<LogInResponse> { | ): Promise<LogInResponse> { | ||||
await validateInput(viewer, siweAuthRequestInputValidator, input); | const request = await validateInput( | ||||
const request: SIWEAuthRequest = input; | viewer, | ||||
siweAuthRequestInputValidator, | |||||
input, | |||||
); | |||||
const { | const { | ||||
message, | message, | ||||
signature, | signature, | ||||
deviceTokenUpdateRequest, | deviceTokenUpdateRequest, | ||||
platformDetails, | platformDetails, | ||||
signedIdentityKeysBlob, | signedIdentityKeysBlob, | ||||
} = request; | } = request; | ||||
const calendarQuery = normalizeCalendarQuery(request.calendarQuery); | const calendarQuery = normalizeCalendarQuery(request.calendarQuery); | ||||
▲ Show 20 Lines • Show All 104 Lines • ▼ Show 20 Lines | const response = await processSuccessfulLogin({ | ||||
userID, | userID, | ||||
calendarQuery, | calendarQuery, | ||||
socialProof, | socialProof, | ||||
signedIdentityKeysBlob, | signedIdentityKeysBlob, | ||||
}); | }); | ||||
return validateOutput(viewer, logInResponseValidator, response); | return validateOutput(viewer, logInResponseValidator, response); | ||||
} | } | ||||
const updatePasswordRequestInputValidator = tShape({ | const updatePasswordRequestInputValidator = tShape<UpdatePasswordRequest>({ | ||||
code: t.String, | code: t.String, | ||||
password: tPassword, | password: tPassword, | ||||
watchedIDs: t.list(tID), | watchedIDs: t.list(tID), | ||||
calendarQuery: t.maybe(entryQueryInputValidator), | calendarQuery: t.maybe(entryQueryInputValidator), | ||||
deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), | ||||
platformDetails: tPlatformDetails, | platformDetails: tPlatformDetails, | ||||
}); | }); | ||||
async function oldPasswordUpdateResponder( | async function oldPasswordUpdateResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<LogInResponse> { | ): Promise<LogInResponse> { | ||||
await validateInput(viewer, updatePasswordRequestInputValidator, input); | const request = await validateInput( | ||||
const request: UpdatePasswordRequest = input; | viewer, | ||||
updatePasswordRequestInputValidator, | |||||
input, | |||||
); | |||||
if (request.calendarQuery) { | if (request.calendarQuery) { | ||||
request.calendarQuery = normalizeCalendarQuery(request.calendarQuery); | request.calendarQuery = normalizeCalendarQuery(request.calendarQuery); | ||||
} | } | ||||
const response = await updatePassword(viewer, request); | const response = await updatePassword(viewer, request); | ||||
return validateOutput(viewer, logInResponseValidator, response); | return validateOutput(viewer, logInResponseValidator, response); | ||||
} | } | ||||
const updateUserSettingsInputValidator = tShape({ | const updateUserSettingsInputValidator = tShape<UpdateUserSettingsRequest>({ | ||||
name: t.irreducible( | name: t.irreducible( | ||||
userSettingsTypes.DEFAULT_NOTIFICATIONS, | userSettingsTypes.DEFAULT_NOTIFICATIONS, | ||||
x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, | x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, | ||||
), | ), | ||||
data: t.enums.of(notificationTypeValues), | data: t.enums.of(notificationTypeValues), | ||||
}); | }); | ||||
async function updateUserSettingsResponder( | async function updateUserSettingsResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<void> { | ): Promise<void> { | ||||
const request: UpdateUserSettingsRequest = input; | const request = await validateInput( | ||||
await validateInput(viewer, updateUserSettingsInputValidator, request); | viewer, | ||||
updateUserSettingsInputValidator, | |||||
input, | |||||
); | |||||
await updateUserSettings(viewer, request); | await updateUserSettings(viewer, request); | ||||
} | } | ||||
const policyAcknowledgmentRequestInputValidator = tShape({ | const policyAcknowledgmentRequestInputValidator = | ||||
tShape<PolicyAcknowledgmentRequest>({ | |||||
policy: t.maybe(t.enums.of(policies)), | policy: t.maybe(t.enums.of(policies)), | ||||
}); | }); | ||||
async function policyAcknowledgmentResponder( | async function policyAcknowledgmentResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<void> { | ): Promise<void> { | ||||
const request: PolicyAcknowledgmentRequest = input; | const request = await validateInput( | ||||
await validateInput( | |||||
viewer, | viewer, | ||||
policyAcknowledgmentRequestInputValidator, | policyAcknowledgmentRequestInputValidator, | ||||
request, | input, | ||||
); | ); | ||||
await viewerAcknowledgmentUpdater(viewer, request.policy); | await viewerAcknowledgmentUpdater(viewer, request.policy); | ||||
} | } | ||||
export const updateUserAvatarResponseValidator: TInterface<UpdateUserAvatarResponse> = | export const updateUserAvatarResponseValidator: TInterface<UpdateUserAvatarResponse> = | ||||
tShape<UpdateUserAvatarResponse>({ | tShape<UpdateUserAvatarResponse>({ | ||||
updates: createUpdatesResultValidator, | updates: createUpdatesResultValidator, | ||||
}); | }); | ||||
const updateUserAvatarResponderValidator = t.union([ | const updateUserAvatarResponderValidator = t.union([ | ||||
t.maybe(clientAvatarValidator), | t.maybe(clientAvatarValidator), | ||||
updateUserAvatarResponseValidator, | updateUserAvatarResponseValidator, | ||||
]); | ]); | ||||
async function updateUserAvatarResponder( | async function updateUserAvatarResponder( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
input: any, | input: mixed, | ||||
): Promise<?ClientAvatar | UpdateUserAvatarResponse> { | ): Promise<?ClientAvatar | UpdateUserAvatarResponse> { | ||||
const request: UpdateUserAvatarRequest = input; | const request = await validateInput( | ||||
await validateInput(viewer, updateUserAvatarRequestValidator, request); | viewer, | ||||
updateUserAvatarRequestValidator, | |||||
input, | |||||
); | |||||
const result = await updateUserAvatar(viewer, request); | const result = await updateUserAvatar(viewer, request); | ||||
return validateOutput(viewer, updateUserAvatarResponderValidator, result); | return validateOutput(viewer, updateUserAvatarResponderValidator, result); | ||||
} | } | ||||
export { | export { | ||||
userSubscriptionUpdateResponder, | userSubscriptionUpdateResponder, | ||||
passwordUpdateResponder, | passwordUpdateResponder, | ||||
sendVerificationEmailResponder, | sendVerificationEmailResponder, | ||||
Show All 11 Lines |