@internetderdinge/api 1.229.2 → 1.229.4
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/dist/src/accounts/accounts.controller.js +7 -5
- package/dist/src/accounts/accounts.service.js +4 -4
- package/dist/src/accounts/accounts.validation.js +4 -12
- package/dist/src/accounts/auth0.service.js +25 -25
- package/dist/src/devices/devices.route.js +12 -2
- package/dist/src/devicesNotifications/devicesNotifications.controller.js +1 -0
- package/dist/src/middlewares/auth.js +2 -0
- package/dist/src/middlewares/validateAdmin.js +7 -5
- package/dist/src/users/users.route.js +0 -2
- package/dist/src/users/users.service.js +2 -0
- package/dist/src/users/users.validation.js +10 -16
- package/dist/src/utils/buildRouterAndDocs.js +0 -1
- package/dist/src/utils/userName.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/accounts/accounts.controller.ts +8 -5
- package/src/accounts/accounts.service.ts +4 -4
- package/src/accounts/accounts.validation.ts +19 -24
- package/src/accounts/auth0.service.ts +20 -16
- package/src/devices/devices.route.ts +11 -0
- package/src/devicesNotifications/devicesNotifications.controller.ts +2 -0
- package/src/middlewares/auth.ts +3 -1
- package/src/middlewares/validateAdmin.ts +18 -7
- package/src/users/users.route.ts +0 -2
- package/src/users/users.service.ts +14 -0
- package/src/users/users.validation.ts +10 -17
- package/src/utils/buildRouterAndDocs.ts +0 -2
- package/src/utils/userName.ts +1 -1
package/package.json
CHANGED
|
@@ -36,7 +36,7 @@ const getAccountById = catchAsync(
|
|
|
36
36
|
throw new ApiError(httpStatus.NOT_FOUND, "Account not found");
|
|
37
37
|
}
|
|
38
38
|
res.send({
|
|
39
|
-
...account
|
|
39
|
+
...account,
|
|
40
40
|
notification: entryDeviceNotifications?.settings,
|
|
41
41
|
});
|
|
42
42
|
},
|
|
@@ -46,7 +46,7 @@ const setDeviceToken = catchAsync(
|
|
|
46
46
|
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
|
47
47
|
const account = await accountsService.getAccountById(req.auth.sub);
|
|
48
48
|
|
|
49
|
-
const devices: Device[] = account.
|
|
49
|
+
const devices: Device[] = account.app_metadata.devices || [];
|
|
50
50
|
const alreadyExisting = devices.find((d) => d.fck === req.body.token);
|
|
51
51
|
if (!alreadyExisting) {
|
|
52
52
|
devices.push({ fck: req.body.token });
|
|
@@ -74,7 +74,9 @@ const updateEntry = catchAsync(
|
|
|
74
74
|
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
|
75
75
|
const account = await accountsService.getAccountById(req.auth.sub);
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
console.log("account", account);
|
|
78
|
+
|
|
79
|
+
const { isSocial } = account.identities[0];
|
|
78
80
|
|
|
79
81
|
const {
|
|
80
82
|
given_name,
|
|
@@ -93,7 +95,7 @@ const updateEntry = catchAsync(
|
|
|
93
95
|
|
|
94
96
|
const update = isSocial
|
|
95
97
|
? {
|
|
96
|
-
...account.
|
|
98
|
+
...account.app_metadata,
|
|
97
99
|
...updateBody,
|
|
98
100
|
...(hasGivenName ? { first_name: trimmedGivenName } : {}),
|
|
99
101
|
...(hasFamilyName ? { last_name: trimmedFamilyName } : {}),
|
|
@@ -136,7 +138,8 @@ const deleteCurrent = catchAsync(
|
|
|
136
138
|
const current = catchAsync(
|
|
137
139
|
async (req: AuthenticatedRequest, res: Response): Promise<void> => {
|
|
138
140
|
const user = await accountsService.getAccountById(req.auth.sub);
|
|
139
|
-
|
|
141
|
+
console.log("user", user);
|
|
142
|
+
res.send(user);
|
|
140
143
|
},
|
|
141
144
|
);
|
|
142
145
|
|
|
@@ -10,7 +10,7 @@ type Stock = any; // Replace with the actual Stock type if available
|
|
|
10
10
|
* @returns {Promise<Stock>}
|
|
11
11
|
*/
|
|
12
12
|
export const getAccountById = async (id: ObjectId): Promise<Stock> => {
|
|
13
|
-
return auth0.users.get(
|
|
13
|
+
return auth0.users.get(id);
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -54,7 +54,7 @@ export const updateMetaDataById = async (
|
|
|
54
54
|
updateBody: Record<string, any>,
|
|
55
55
|
): Promise<Stock> => {
|
|
56
56
|
// now use the generic update and pass app_metadata
|
|
57
|
-
return auth0.users.update(
|
|
57
|
+
return auth0.users.update(id, { app_metadata: updateBody });
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -65,14 +65,14 @@ export const updateUserById = async (
|
|
|
65
65
|
updateBody: Record<string, any>,
|
|
66
66
|
): Promise<Stock> => {
|
|
67
67
|
// switch to the v3 ManagementClient users.update
|
|
68
|
-
return auth0.users.update(
|
|
68
|
+
return auth0.users.update(id, updateBody);
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Delete user by id
|
|
73
73
|
*/
|
|
74
74
|
export const deleteById = async (userId: ObjectId): Promise<Stock> => {
|
|
75
|
-
return auth0.users.delete(
|
|
75
|
+
return auth0.users.delete(userId);
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export default {
|
|
@@ -35,25 +35,21 @@ export const getUsersSchema = zPagination;
|
|
|
35
35
|
|
|
36
36
|
export const getAccountSchema = {
|
|
37
37
|
params: z.object({
|
|
38
|
-
accountId: z
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
description: "Auth Account ID",
|
|
44
|
-
}),
|
|
38
|
+
accountId: z.string().openapi({
|
|
39
|
+
example:
|
|
40
|
+
process.env.SCHEMA_EXAMPLE_ACCOUNT_ID || "auth0|60452f4c0dc85b0062326",
|
|
41
|
+
description: "Auth Account ID",
|
|
42
|
+
}),
|
|
45
43
|
}),
|
|
46
44
|
};
|
|
47
45
|
|
|
48
46
|
export const updateAccountSchema = {
|
|
49
47
|
params: z.object({
|
|
50
|
-
accountId: z
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
description: "Auth Account ID",
|
|
56
|
-
}),
|
|
48
|
+
accountId: z.string().openapi({
|
|
49
|
+
example:
|
|
50
|
+
process.env.SCHEMA_EXAMPLE_ACCOUNT_ID || "auth0|60452f4c0dc85b0062326",
|
|
51
|
+
description: "Auth Account ID",
|
|
52
|
+
}),
|
|
57
53
|
}),
|
|
58
54
|
body: zPatchBody({
|
|
59
55
|
language: z
|
|
@@ -67,9 +63,10 @@ export const updateAccountSchema = {
|
|
|
67
63
|
.optional()
|
|
68
64
|
.openapi({ example: "female", description: "Gender" }),
|
|
69
65
|
email: z
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
.preprocess(
|
|
67
|
+
(value) => (value === "" ? undefined : value),
|
|
68
|
+
z.string().email().optional(),
|
|
69
|
+
)
|
|
73
70
|
.openapi({ description: "User email address" }),
|
|
74
71
|
given_name: z
|
|
75
72
|
.string()
|
|
@@ -90,13 +87,11 @@ export const updateAccountSchema = {
|
|
|
90
87
|
|
|
91
88
|
export const deleteEntrySchema = {
|
|
92
89
|
params: z.object({
|
|
93
|
-
accountId: z
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
description: "Auth Account ID",
|
|
99
|
-
}),
|
|
90
|
+
accountId: z.string().openapi({
|
|
91
|
+
example:
|
|
92
|
+
process.env.SCHEMA_EXAMPLE_ACCOUNT_ID || "auth0|60452f4c0dc85b0062326",
|
|
93
|
+
description: "Auth Account ID",
|
|
94
|
+
}),
|
|
100
95
|
}),
|
|
101
96
|
};
|
|
102
97
|
|
|
@@ -21,6 +21,7 @@ let tokenManagementClient: TokenManagementClient = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
//if (config.env !== 'production') {
|
|
24
|
+
/*
|
|
24
25
|
try {
|
|
25
26
|
console.warn("Auth0 client: use local token from cache try");
|
|
26
27
|
const token = readFileSync("./token.txt", "utf8");
|
|
@@ -28,7 +29,7 @@ try {
|
|
|
28
29
|
tokenManagementClient = { token };
|
|
29
30
|
} catch (error) {
|
|
30
31
|
console.log("Auth0 Client: use new token");
|
|
31
|
-
}
|
|
32
|
+
} */
|
|
32
33
|
//}
|
|
33
34
|
|
|
34
35
|
// IoT Api
|
|
@@ -123,12 +124,12 @@ export const getAuth0Token = async (): Promise<string> => {
|
|
|
123
124
|
pendingTokenPromise = null;
|
|
124
125
|
return cachedToken;
|
|
125
126
|
}
|
|
126
|
-
//}
|
|
127
127
|
|
|
128
|
+
console.warn("Auth0 Client: Requesting new token from Auth0");
|
|
128
129
|
const tokenResponse =
|
|
129
130
|
await auth0AuthClient.oauth.clientCredentialsGrant(grantOpts);
|
|
130
|
-
const expiresIn = tokenResponse.
|
|
131
|
-
cachedToken = tokenResponse.
|
|
131
|
+
const expiresIn = tokenResponse.expires_in || 3600;
|
|
132
|
+
cachedToken = tokenResponse.access_token;
|
|
132
133
|
tokenExpiresAt = now + expiresIn;
|
|
133
134
|
|
|
134
135
|
// if (process.env.NODE_ENV !== 'production') {
|
|
@@ -159,6 +160,7 @@ export const getAuth0ManagementToken = async (): Promise<string> => {
|
|
|
159
160
|
|
|
160
161
|
pendingManagementTokenPromise = (async () => {
|
|
161
162
|
const fileToken = await loadTokenFromFile(MANAGEMENT_TOKEN_FILE_PATH);
|
|
163
|
+
|
|
162
164
|
if (fileToken && fileToken.expiresAt > now + TOKEN_BUFFER_SECONDS) {
|
|
163
165
|
cachedManagementToken = fileToken.token;
|
|
164
166
|
managementTokenExpiresAt = fileToken.expiresAt;
|
|
@@ -166,8 +168,10 @@ export const getAuth0ManagementToken = async (): Promise<string> => {
|
|
|
166
168
|
return cachedManagementToken;
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
console.warn("Auth0 Management Client: Requesting new token from Auth0");
|
|
169
172
|
const tokenResponse =
|
|
170
173
|
await auth0AuthClient.oauth.clientCredentialsGrant(grantOpts);
|
|
174
|
+
|
|
171
175
|
const expiresIn = tokenResponse.data.expires_in || 3600;
|
|
172
176
|
cachedManagementToken = tokenResponse.data.access_token;
|
|
173
177
|
managementTokenExpiresAt = now + expiresIn;
|
|
@@ -192,27 +196,26 @@ export const auth0 = new ManagementClient({
|
|
|
192
196
|
});
|
|
193
197
|
|
|
194
198
|
export const getUserIdByEmail = async (email: string): Promise<User[]> => {
|
|
195
|
-
|
|
196
|
-
return auth0.usersByEmail.getByEmail({ email });
|
|
199
|
+
return auth0.users.listUsersByEmail({ email });
|
|
197
200
|
};
|
|
198
201
|
|
|
199
202
|
export const sendVerificationEmail = async (userID: string): Promise<any> => {
|
|
200
|
-
return auth0.
|
|
203
|
+
return auth0.tickets.verifyEmail({ user_id: userID });
|
|
201
204
|
};
|
|
202
205
|
|
|
203
206
|
export const getUserById = async (userId: string): Promise<User> => {
|
|
204
|
-
return auth0.users.get(
|
|
207
|
+
return auth0.users.get(userId);
|
|
205
208
|
};
|
|
206
209
|
|
|
207
210
|
export const avatar = async (userId: string): Promise<User> => {
|
|
208
|
-
return auth0.users.get(
|
|
211
|
+
return auth0.users.get(userId);
|
|
209
212
|
};
|
|
210
213
|
|
|
211
214
|
export const mfaEnrollAccount = async (
|
|
212
215
|
userId: string,
|
|
213
216
|
mfaToken: string,
|
|
214
217
|
): Promise<any> => {
|
|
215
|
-
const ticketResponse = await auth0.guardian.
|
|
218
|
+
const ticketResponse = await auth0.guardian.enrollments.createTicket({
|
|
216
219
|
user_id: userId,
|
|
217
220
|
send_mail: false,
|
|
218
221
|
});
|
|
@@ -221,15 +224,15 @@ export const mfaEnrollAccount = async (
|
|
|
221
224
|
};
|
|
222
225
|
|
|
223
226
|
export const mfaDisableAccount = async (userId: string): Promise<any> => {
|
|
224
|
-
await auth0.users.
|
|
227
|
+
await auth0.users.authenticationMethods.deleteAll(userId);
|
|
225
228
|
return { success: true };
|
|
226
229
|
};
|
|
227
230
|
|
|
228
231
|
export const getUsersByIds = async (postIDs: string[]): Promise<User[]> => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
232
|
+
const userIds = postIDs.filter(Boolean);
|
|
233
|
+
if (!userIds.length) return [];
|
|
234
|
+
|
|
235
|
+
const q = userIds.map((id) => `user_id:"${id}"`).join(" OR ");
|
|
233
236
|
|
|
234
237
|
const params = {
|
|
235
238
|
search_engine: "v3",
|
|
@@ -238,7 +241,8 @@ export const getUsersByIds = async (postIDs: string[]): Promise<User[]> => {
|
|
|
238
241
|
page: 0,
|
|
239
242
|
};
|
|
240
243
|
|
|
241
|
-
|
|
244
|
+
const page = await auth0.users.list(params);
|
|
245
|
+
return page || [];
|
|
242
246
|
};
|
|
243
247
|
|
|
244
248
|
export default {
|
|
@@ -94,6 +94,17 @@ export const devicesRouteSpecs: RouteSpec[] = [
|
|
|
94
94
|
{
|
|
95
95
|
method: "post",
|
|
96
96
|
path: "/:deviceId",
|
|
97
|
+
validate: [auth("manageUsers"), validateDevice, validateOrganizationUpdate],
|
|
98
|
+
requestSchema: updateDeviceSchema,
|
|
99
|
+
responseSchema: deviceResponseSchema,
|
|
100
|
+
handler: devicesController.updateEntry,
|
|
101
|
+
summary: "Update a device",
|
|
102
|
+
description:
|
|
103
|
+
"Modify the properties of an existing device identified by its ID.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
method: "delete",
|
|
107
|
+
path: "/:deviceId",
|
|
97
108
|
handler: devicesController.deleteEntry,
|
|
98
109
|
summary: "Delete a device",
|
|
99
110
|
description: "Remove the specified device from the system.",
|
package/src/middlewares/auth.ts
CHANGED
|
@@ -36,17 +36,18 @@ const verifyCallback: VerifyCallback =
|
|
|
36
36
|
return reject(new ApiError(httpStatus.FORBIDDEN, "Forbidden"));
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
|
|
40
39
|
resolve();
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
const auth = function authFactory(...requiredRights: string[]) {
|
|
43
|
+
console.log("Creating auth middleware with required rights:", requiredRights);
|
|
44
44
|
return async function authMiddleware(
|
|
45
45
|
req: AuthRequest,
|
|
46
46
|
res: Response,
|
|
47
47
|
next: NextFunction,
|
|
48
48
|
): Promise<void> {
|
|
49
49
|
try {
|
|
50
|
+
console.log("Authenticating request:");
|
|
50
51
|
// Check for custom token in X-API-Key header
|
|
51
52
|
const apiKey = req.headers["x-api-key"] as string | undefined;
|
|
52
53
|
if (apiKey && apiKey.length === 64) {
|
|
@@ -93,6 +94,7 @@ const auth = function authFactory(...requiredRights: string[]) {
|
|
|
93
94
|
err.message || "Sorry we were unable to process your request.";
|
|
94
95
|
return res.status(status).send({ message });
|
|
95
96
|
}
|
|
97
|
+
|
|
96
98
|
next();
|
|
97
99
|
});
|
|
98
100
|
} catch (error) {
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import httpStatus from
|
|
2
|
-
import ApiError from
|
|
3
|
-
import type { Request, Response, NextFunction } from
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import ApiError from "../utils/ApiError";
|
|
3
|
+
import type { Request, Response, NextFunction } from "express";
|
|
4
4
|
|
|
5
5
|
const isAdmin = (user: Record<string, any> | undefined): boolean => {
|
|
6
|
-
|
|
6
|
+
return false;
|
|
7
7
|
if (!user) return false;
|
|
8
8
|
|
|
9
9
|
// return false; // TODO: Remove this line when the user object is properly defined
|
|
10
|
-
return user[
|
|
10
|
+
return user["https://memo.wirewire.de/roles"]
|
|
11
|
+
? user["https://memo.wirewire.de/roles"].includes("admin")
|
|
12
|
+
: false;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
const validateAdmin = async (
|
|
15
|
+
const validateAdmin = async (
|
|
16
|
+
req: Request,
|
|
17
|
+
res: Response,
|
|
18
|
+
next: NextFunction,
|
|
19
|
+
): Promise<void> => {
|
|
14
20
|
if (isAdmin(req.auth)) {
|
|
15
21
|
next();
|
|
16
22
|
} else {
|
|
17
|
-
next(
|
|
23
|
+
next(
|
|
24
|
+
new ApiError(
|
|
25
|
+
httpStatus.FORBIDDEN,
|
|
26
|
+
"User is not part of the admin group (validateAdmin)",
|
|
27
|
+
),
|
|
28
|
+
);
|
|
18
29
|
}
|
|
19
30
|
};
|
|
20
31
|
|
package/src/users/users.route.ts
CHANGED
|
@@ -138,7 +138,6 @@ export const userRouteSpecs: RouteSpec[] = [
|
|
|
138
138
|
handler: userController.updateUser,
|
|
139
139
|
summary: "Update a user by ID",
|
|
140
140
|
description: "Replaces a user’s full record with the provided data.",
|
|
141
|
-
memoOnly: true,
|
|
142
141
|
},
|
|
143
142
|
{
|
|
144
143
|
method: "patch",
|
|
@@ -149,7 +148,6 @@ export const userRouteSpecs: RouteSpec[] = [
|
|
|
149
148
|
handler: userController.updateUser,
|
|
150
149
|
summary: "Partially update a user by ID",
|
|
151
150
|
description: "Applies partial updates to a user’s record by ID.",
|
|
152
|
-
memoOnly: true,
|
|
153
151
|
},
|
|
154
152
|
{
|
|
155
153
|
method: "delete",
|
|
@@ -149,7 +149,21 @@ export const getUserByOwner = async (
|
|
|
149
149
|
): Promise<any | null> => {
|
|
150
150
|
const user = await User.findOne({ owner, organization });
|
|
151
151
|
if (!user) return null;
|
|
152
|
+
|
|
153
|
+
console.log(
|
|
154
|
+
"Found user for owner and organization:",
|
|
155
|
+
owner,
|
|
156
|
+
organization,
|
|
157
|
+
user,
|
|
158
|
+
);
|
|
152
159
|
const auth0User = await populateAuth0User(user);
|
|
160
|
+
|
|
161
|
+
console.log(
|
|
162
|
+
"Found user for owner and organizationddddddd:",
|
|
163
|
+
owner,
|
|
164
|
+
organization,
|
|
165
|
+
user,
|
|
166
|
+
);
|
|
153
167
|
const json = user.toJSON();
|
|
154
168
|
json.auth0User = auth0User;
|
|
155
169
|
return json;
|
|
@@ -71,21 +71,6 @@ export const getCurrentUserSchema = {
|
|
|
71
71
|
export const updateUserSchema = {
|
|
72
72
|
...zUpdate("userId"),
|
|
73
73
|
body: zPatchBody({
|
|
74
|
-
password: z
|
|
75
|
-
.string()
|
|
76
|
-
.refine(
|
|
77
|
-
(val) => {
|
|
78
|
-
try {
|
|
79
|
-
password(val);
|
|
80
|
-
return true;
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
{ message: "Invalid password format" },
|
|
86
|
-
)
|
|
87
|
-
.optional()
|
|
88
|
-
.openapi({ description: "New user password" }),
|
|
89
74
|
name: z.string().optional().openapi({ description: "User full name" }),
|
|
90
75
|
timezone: z.string().optional().openapi({ description: "IANA timezone" }),
|
|
91
76
|
avatar: z.string().optional().openapi({ description: "Avatar URL" }),
|
|
@@ -107,7 +92,11 @@ export const updateUserSchema = {
|
|
|
107
92
|
.enum(["user", "admin", "patient", "onlyself"])
|
|
108
93
|
.optional()
|
|
109
94
|
.openapi({ description: "User role" }),
|
|
110
|
-
inviteCode: z
|
|
95
|
+
inviteCode: z
|
|
96
|
+
.string()
|
|
97
|
+
.nullable()
|
|
98
|
+
.optional()
|
|
99
|
+
.openapi({ description: "Invite code" }),
|
|
111
100
|
organization: zObjectId
|
|
112
101
|
.optional()
|
|
113
102
|
.openapi({ description: "Organization ObjectId" }),
|
|
@@ -128,7 +117,11 @@ export const updateInviteSchema = {
|
|
|
128
117
|
body: z.object({
|
|
129
118
|
organization: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
130
119
|
status: z.enum(["accepted"]).openapi({ description: "Invite status" }),
|
|
131
|
-
inviteCode: z
|
|
120
|
+
inviteCode: z
|
|
121
|
+
.string()
|
|
122
|
+
.nullable()
|
|
123
|
+
.optional()
|
|
124
|
+
.openapi({ description: "Invite code" }),
|
|
132
125
|
}),
|
|
133
126
|
};
|
|
134
127
|
|
package/src/utils/userName.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Patient } from "../types/patient"; // Assuming a `Patient` type ex
|
|
|
5
5
|
const generateUserName = async (patient: Patient): Promise<string | null> => {
|
|
6
6
|
if (patient.owner) {
|
|
7
7
|
const auth0UserById = await getUserById(patient.owner);
|
|
8
|
-
patient.auth0User = auth0UserById
|
|
8
|
+
patient.auth0User = auth0UserById;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
if (patient?.auth0User?.app_metadata?.first_name) {
|