Skip to content

Commit b743c11

Browse files
authored
feat: Add notifications page email and sms verification (#3684)
1 parent df2ed4c commit b743c11

File tree

20 files changed

+586
-102
lines changed

20 files changed

+586
-102
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mutation verifyAddress($data: VerifyAddressInput!) {
2+
verifyAddress(data: $data) {
3+
success
4+
message
5+
}
6+
}

packages/bff-types-generated/queries/notificationsettingForCurrentUser.fragment.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ fragment NotificationsettingForCurrentUserFields on NotificationSettingsResponse
44
resourceIncludeList
55
userId
66
partyUuid
7+
emailVerificationStatus
8+
smsVerificationStatus
9+
needsConfirmation
710
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
query verifiedAddresses {
2+
verifiedAddresses {
3+
value
4+
addressType
5+
}
6+
}

packages/bff/src/graphql/functions/profile.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import config from '../../config.ts';
55
import { GroupRepository, PartyRepository, ProfileRepository } from '../../db.ts';
66
import { Group, Party, ProfileTable } from '../../entities.ts';
77
import type { PreselectedPartyOperationType } from '../types/mutation.ts';
8-
import type { NotificationSettingsInputData } from '../types/profile.ts';
8+
import type { NotificationSettingsInputData, VerifyAddressInputData } from '../types/profile.ts';
99

1010
const { platformBaseURL } = config;
1111

@@ -454,6 +454,56 @@ export const updateProfileSettingPreference = async (context: Context, shouldSho
454454
}
455455
};
456456

457+
export const verifyAddress = async (data: VerifyAddressInputData, context: Context) => {
458+
const newToken = await exchangeToken(context);
459+
try {
460+
await axios.post(
461+
`${platformProfileAPI_url}users/current/verification/verify`,
462+
{ value: data.value, type: data.type, verificationCode: data.verificationCode },
463+
{
464+
timeout: 30000,
465+
headers: {
466+
Authorization: `Bearer ${newToken}`,
467+
'Content-Type': 'application/json',
468+
Accept: 'application/json',
469+
},
470+
},
471+
);
472+
return { success: true };
473+
} catch (error) {
474+
if (typeof error === 'object' && error !== null && 'response' in error) {
475+
const axiosError = error as { response?: { status?: number } };
476+
if (axiosError.response?.status === 422) {
477+
return { success: false, message: 'invalid_code' };
478+
}
479+
}
480+
logger.error(error, 'Error verifying address:');
481+
throw error;
482+
}
483+
};
484+
485+
export const getVerifiedAddresses = async (context: Context) => {
486+
const newToken = await exchangeToken(context);
487+
if (!newToken) {
488+
logger.error('No new token received');
489+
throw new Error('Unable to exchange token');
490+
}
491+
try {
492+
const response = await axios.get(`${platformProfileAPI_url}users/current/verification/verified-addresses`, {
493+
timeout: 30000,
494+
headers: {
495+
Authorization: `Bearer ${newToken}`,
496+
'Content-Type': 'application/json',
497+
Accept: 'application/json',
498+
},
499+
});
500+
return response.data ?? [];
501+
} catch (error) {
502+
logger.error(error, 'Error fetching verified addresses for user:');
503+
return [];
504+
}
505+
};
506+
457507
export const updateLanguage = async (pid: string, language: string) => {
458508
const currentProfile = await ProfileRepository!.findOne({
459509
where: { pid },

packages/bff/src/graphql/types/mutation.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import {
1111
updateLanguage,
1212
updateNotificationsSetting,
1313
updateProfileSettingPreference,
14+
verifyAddress,
1415
} from '../functions/profile.ts';
1516
import { createSavedSearch, deleteSavedSearch, updateSavedSearch } from '../functions/savedsearch.ts';
1617
import { languageCodes, updateAltinnPersistentContextValue } from './cookie.js';
17-
import { NotificationSettingsInput, Response, SavedSearchInput, SavedSearches } from './index.ts';
18+
import { NotificationSettingsInput, Response, SavedSearchInput, SavedSearches, VerifyAddressInput } from './index.ts';
1819

1920
export const Mutation = extendType({
2021
type: 'Mutation',
@@ -256,6 +257,27 @@ export const UpdateProfileSettingPreference = extendType({
256257
},
257258
});
258259

260+
export const VerifyAddress = extendType({
261+
type: 'Mutation',
262+
definition(t) {
263+
t.field('verifyAddress', {
264+
type: Response,
265+
args: {
266+
data: VerifyAddressInput,
267+
},
268+
resolve: async (_, { data }, ctx) => {
269+
try {
270+
const result = await verifyAddress(data, ctx);
271+
return result;
272+
} catch (error) {
273+
logger.error(error, 'Failed to verify address:');
274+
return { success: false, message: 'Failed to verify address' };
275+
}
276+
},
277+
});
278+
},
279+
});
280+
259281
export type PreselectedPartyOperationType = 'set' | 'unset';
260282
export const SetPreSelectedParty = extendType({
261283
type: 'Mutation',

packages/bff/src/graphql/types/profile.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,18 @@ export const NotificationSettingsResponse = objectType({
249249
description: 'ID of the party',
250250
resolve: (obj) => obj.partyUuid,
251251
});
252+
t.nullable.string('emailVerificationStatus', {
253+
description: 'Verification status for the email address (Unverified, Verified, Legacy)',
254+
resolve: (obj) => obj.emailVerificationStatus,
255+
});
256+
t.nullable.string('smsVerificationStatus', {
257+
description: 'Verification status for the SMS/phone number (Unverified, Verified, Legacy)',
258+
resolve: (obj) => obj.smsVerificationStatus,
259+
});
260+
t.nullable.boolean('needsConfirmation', {
261+
description: 'Indicates whether the notification settings need confirmation',
262+
resolve: (obj) => obj.needsConfirmation,
263+
});
252264
},
253265
});
254266

@@ -260,6 +272,7 @@ export const NotificationSettingsInput = inputObjectType({
260272
t.string('emailAddress');
261273
t.string('phoneNumber');
262274
t.list.string('resourceIncludeList');
275+
t.nullable.boolean('generateVerificationCode');
263276
},
264277
});
265278

@@ -269,6 +282,22 @@ export interface NotificationSettingsInputData {
269282
emailAddress?: string;
270283
phoneNumber?: string;
271284
resourceIncludeList?: string[];
285+
generateVerificationCode?: boolean;
286+
}
287+
288+
export const VerifyAddressInput = inputObjectType({
289+
name: 'VerifyAddressInput',
290+
definition(t) {
291+
t.string('value');
292+
t.string('type');
293+
t.string('verificationCode');
294+
},
295+
});
296+
297+
export interface VerifyAddressInputData {
298+
value: string;
299+
type: 'Email' | 'Sms';
300+
verificationCode: string;
272301
}
273302

274303
export const ProfileSettingPreference = objectType({
@@ -284,6 +313,14 @@ export const ProfileSettingPreference = objectType({
284313
},
285314
});
286315

316+
export const VerifiedAddressResponse = objectType({
317+
name: 'VerifiedAddressResponse',
318+
definition(t) {
319+
t.nullable.string('value');
320+
t.nullable.string('addressType', { resolve: (obj) => obj.type ?? null });
321+
},
322+
});
323+
287324
export const UserProfile = objectType({
288325
name: 'UserProfile',
289326
definition(t) {

packages/bff/src/graphql/types/query.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getNotificationsettingsForCurrentUser,
99
getOrCreateProfile,
1010
getUserFromCore,
11+
getVerifiedAddresses,
1112
} from '../functions/profile.ts';
1213
import { getLanguageFromAltinnContext, languageCodes, updateAltinnPersistentContextValue } from './cookie.js';
1314
import { getOrganizationsFromRedis } from './organization.ts';
@@ -94,6 +95,13 @@ export const Query = objectType({
9495
},
9596
});
9697

98+
t.field('verifiedAddresses', {
99+
type: list('VerifiedAddressResponse'),
100+
resolve: async (_source, _args, ctx) => {
101+
return (await getVerifiedAddresses(ctx)) ?? [];
102+
},
103+
});
104+
97105
t.field('getNotificationAddressByOrgNumber', {
98106
type: OrganizationResponse,
99107
args: {

packages/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"i18n:sort": "tsx ./src/i18n/check.ts --sort"
2323
},
2424
"dependencies": {
25-
"@altinn/altinn-components": "0.56.23",
25+
"@altinn/altinn-components": "0.56.24",
2626
"@microsoft/applicationinsights-react-js": "19.3.7",
2727
"@microsoft/applicationinsights-web": "3.3.9",
2828
"@navikt/aksel-icons": "^7.37.0",

packages/frontend/src/api/queries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {
2121
type UpdateProfileSettingPreferenceMutation,
2222
type UpdateSavedSearchMutation,
2323
type UpdateSystemLabelMutation,
24+
type VerifiedAddressesQuery,
25+
type VerifyAddressMutation,
26+
type VerifyAddressMutationVariables,
2427
getSdk,
2528
} from 'bff-types-generated';
2629
import { ClientError, GraphQLClient, type RequestMiddleware, type ResponseMiddleware } from 'graphql-request';
@@ -237,3 +240,8 @@ export const updateProfileSettingPreference = (
237240
shouldShowDeletedEntities: boolean,
238241
): Promise<UpdateProfileSettingPreferenceMutation> =>
239242
graphQLSDK.UpdateProfileSettingPreference({ shouldShowDeletedEntities });
243+
244+
export const getVerifiedAddresses = (): Promise<VerifiedAddressesQuery> => graphQLSDK.verifiedAddresses();
245+
246+
export const verifyAddress = (data: VerifyAddressMutationVariables['data']): Promise<VerifyAddressMutation> =>
247+
graphQLSDK.verifyAddress({ data });

packages/frontend/src/constants/queryKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export const QUERY_KEYS = {
2929
ALTINN_COOKIE: 'altinnCookie',
3030
UPDATED_LANGUAGE: 'updatedLanguage',
3131
SHOW_DELETED_ENTITIES: 'showDeletedEntities',
32+
VERIFIED_ADDRESSES: 'verifiedAddresses',
3233
};

0 commit comments

Comments
 (0)