Skip to content

Commit ecfa193

Browse files
authored
Add Approve User Function (#739)
* Add Approve User Function * Fix Indexes
1 parent d4e617c commit ecfa193

File tree

9 files changed

+398
-23
lines changed

9 files changed

+398
-23
lines changed

common/functions/approveUser.d.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,33 @@
1-
import { condition, status } from '../types/Copy';
2-
31
export default interface approveUserData {
42
/**
53
* Firestore id of the library
64
*/
75
libraryID: string;
86
/**
9-
* Firestore id of the book
7+
* The Firebase ID of the user to approve
8+
*/
9+
uid: string;
10+
/**
11+
* The user's first name
1012
*/
11-
bookID: string;
13+
firstName: string;
1214
/**
13-
* Firestore id of the copy
15+
* The user's last name
1416
*/
15-
copyID: string;
17+
lastName: string;
1618
/**
17-
* Condition of the book when it was checked in
19+
* The user's email address
1820
*/
19-
condition: condition;
21+
email: string;
2022
/**
21-
* The status of the copy to set
23+
* The expiration date of the user's account
2224
*/
23-
status: status;
25+
expiration: number;
2426
}
2527

2628
export interface approveUserResult {
27-
/**
28-
* Firestore id of the checkout
29-
*/
30-
checkoutID: string;
3129
/**
3230
* Firestore id of the user
3331
*/
3432
userID: string;
35-
/**
36-
* The difference in condition between the copy when it was checked in and when it was checked out. (conditionIn - conditionOut)
37-
*/
38-
conditionDiff: number;
3933
}

common/types/User.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { Timestamp } from 'firebase-admin/firestore';
44
* Firestore Location: `libraries/{Library}/users/{User}`
55
*/
66
export default interface User {
7+
/**
8+
* Whether or not the user is active within the library
9+
*/
10+
active: boolean;
711
/**
812
* An array of active checkout IDs
913
*/

firestore.indexes.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,48 @@
1818
}
1919
]
2020
},
21+
{
22+
"collectionGroup": "joinRequests",
23+
"queryScope": "COLLECTION",
24+
"fields": [
25+
{
26+
"fieldPath": "approved",
27+
"order": "ASCENDING"
28+
},
29+
{
30+
"fieldPath": "firstName",
31+
"order": "ASCENDING"
32+
}
33+
]
34+
},
35+
{
36+
"collectionGroup": "joinRequests",
37+
"queryScope": "COLLECTION",
38+
"fields": [
39+
{
40+
"fieldPath": "approved",
41+
"order": "ASCENDING"
42+
},
43+
{
44+
"fieldPath": "lastName",
45+
"order": "ASCENDING"
46+
}
47+
]
48+
},
49+
{
50+
"collectionGroup": "joinRequests",
51+
"queryScope": "COLLECTION",
52+
"fields": [
53+
{
54+
"fieldPath": "approved",
55+
"order": "ASCENDING"
56+
},
57+
{
58+
"fieldPath": "email",
59+
"order": "ASCENDING"
60+
}
61+
]
62+
},
2163
{
2264
"collectionGroup": "checkouts",
2365
"queryScope": "COLLECTION",

functions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export { default as renewCheckout } from './checkouts/renewCheckout.js';
1313
export { default as updatePermissionsClaims } from './libraries/updatePermissionsClaims.js';
1414

1515
export { default as userStatistics } from './users/userStatistics.js';
16+
export { default as approveUser } from './users/approveUser.js';

functions/src/users/approveUser.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import functions from 'firebase-functions';
2+
import { getFirestore, Timestamp, FieldValue } from 'firebase-admin/firestore';
3+
4+
import approveUserData, {
5+
approveUserResult,
6+
} from '@common/functions/approveUser';
7+
8+
import User from '@common/types/User';
9+
import { getAuth } from 'firebase-admin/auth';
10+
11+
const approveUser = functions
12+
.region('us-west2')
13+
.https.onCall(async (data: approveUserData, context) => {
14+
const firestore = getFirestore();
15+
16+
// App Check Verification
17+
if (!context.app && process.env.NODE_ENV === 'production') {
18+
throw new functions.https.HttpsError(
19+
'unauthenticated',
20+
'The function must be called from an App Check verified app.'
21+
);
22+
}
23+
24+
// Auth Verification
25+
if (!context.auth) {
26+
throw new functions.https.HttpsError(
27+
'unauthenticated',
28+
'The function must be called while authenticated.'
29+
);
30+
}
31+
32+
if (
33+
!context.auth.token?.permissions?.MANAGE_USERS?.includes(
34+
data.libraryID
35+
) &&
36+
!context.auth.token?.librariesOwned?.includes(data.libraryID)
37+
) {
38+
throw new functions.https.HttpsError(
39+
'permission-denied',
40+
"The user calling the function must have the 'MANAGE_USERS' permission."
41+
);
42+
}
43+
44+
console.log(data, typeof data.expiration);
45+
46+
// Type Verification
47+
if (
48+
typeof data.firstName !== 'string' ||
49+
typeof data.lastName !== 'string' ||
50+
typeof data.email !== 'string' ||
51+
typeof data.uid !== 'string' ||
52+
typeof data.libraryID !== 'string' ||
53+
typeof data.expiration !== 'number'
54+
) {
55+
throw new functions.https.HttpsError(
56+
'invalid-argument',
57+
'The function must be called with the appropriate arguments.'
58+
);
59+
}
60+
61+
const libraryDoc = await firestore
62+
.collection('libraries')
63+
.doc(data.libraryID)
64+
.get();
65+
66+
if (!libraryDoc.exists) {
67+
throw new functions.https.HttpsError(
68+
'not-found',
69+
'The specified library could not be found.'
70+
);
71+
}
72+
73+
const user = await getAuth().getUser(data.uid);
74+
75+
if (!user) {
76+
throw new functions.https.HttpsError(
77+
'not-found',
78+
'The specified user could not be found.'
79+
);
80+
}
81+
82+
// Create a batched update
83+
const batch = firestore.batch();
84+
85+
const userDoc: User = {
86+
active: true,
87+
activeCheckouts: [],
88+
checkoutGroup: 'default',
89+
firstName: data.firstName,
90+
approvedAt: FieldValue.serverTimestamp() as Timestamp,
91+
approvedBy: context.auth.uid,
92+
lastName: data.lastName,
93+
email: data.email,
94+
phoneNumber: null,
95+
identifiers: [],
96+
expiration: Timestamp.fromDate(new Date(data.expiration)),
97+
uid: data.uid,
98+
updatedBy: context.auth.uid,
99+
updatedAt: FieldValue.serverTimestamp() as Timestamp,
100+
};
101+
102+
const userRef = firestore
103+
.collection(`libraries/${data.libraryID}/users`)
104+
.doc(data.uid);
105+
106+
const joinRequestRef = firestore
107+
.collection(`libraries/${data.libraryID}/joinRequests`)
108+
.doc(data.uid);
109+
110+
batch.set(userRef, userDoc);
111+
batch.update(joinRequestRef, { approved: true });
112+
113+
await batch
114+
.commit()
115+
.then(async () => {
116+
const userClaims = user.customClaims;
117+
118+
const newLibrariesJoined = [];
119+
if (userClaims && Array.isArray(userClaims?.librariesJoined)) {
120+
newLibrariesJoined.push(userClaims.librariesJoined);
121+
newLibrariesJoined.push(data.libraryID);
122+
} else {
123+
newLibrariesJoined.push(data.libraryID);
124+
}
125+
126+
getAuth()
127+
.setCustomUserClaims(data.uid, {
128+
...userClaims,
129+
librariesJoined: newLibrariesJoined,
130+
})
131+
.catch(console.error);
132+
})
133+
.catch((err) => {
134+
functions.logger.error(context, data, err);
135+
throw new functions.https.HttpsError(
136+
'internal',
137+
'There was an error approving this user.'
138+
);
139+
});
140+
141+
const result: approveUserResult = {
142+
userID: data.uid,
143+
};
144+
145+
return result;
146+
});
147+
148+
export default approveUser;

react/public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<body>
6363
<noscript>
6464
<h1>BASIS Scottsdale Library</h1>
65-
<h2>Opening 8/8/2022!</h2>
65+
<h2>Now Open in Room 123!</h2>
6666
You need to enable JavaScript to run this app. There may be a problem with
6767
your browser.
6868
</noscript>

react/src/pages/LibraryHome/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ const Home = () => {
131131
color="inherit"
132132
style={{ fontWeight: 'bold' }}
133133
>
134-
Opening 8/8/2022!
134+
Now Open in Room 123!
135135
</Typography>
136136
{!user ? (
137137
<Button
@@ -179,6 +179,15 @@ const Home = () => {
179179
For more information, click <Link to="/contribute">here.</Link>
180180
</h6>
181181
<br />
182+
<h2>When will the Library be open?</h2>
183+
<h6>
184+
The BASIS Scottsdale Library will typically be open from{' '}
185+
<strong>
186+
3:45-4:30 Monday-Friday, and 7:00-8:15 Wednesday-Friday
187+
</strong>
188+
. Check the Bulldog Blast for any updates to our hours for the week.
189+
</h6>
190+
<br />
182191
<h2>I have more questions!</h2>
183192
<h6>
184193
There has been a huge interest in the library and we are so excited to

0 commit comments

Comments
 (0)