forked from jayrosen-design/NWR-DataConnect
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclaimStudentProfile.js
More file actions
277 lines (235 loc) · 8.25 KB
/
Copy pathclaimStudentProfile.js
File metadata and controls
277 lines (235 loc) · 8.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
* Firebase Cloud Function: claimStudentProfile
*
* Purpose: Link a game account to a New Worlds Reading student profile via access code
*
* This is a TEMPLATE for the actual Cloud Function to be deployed to Firebase.
* When ready to deploy, move this file to your Firebase Functions directory.
*
* Deployment Steps:
* 1. Initialize Firebase Functions: firebase init functions
* 2. Copy this file to functions/index.js (or create as a separate function)
* 3. Install dependencies: npm install axios
* 4. Deploy: firebase deploy --only functions:claimStudentProfile
*/
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');
// Initialize Firebase Admin SDK (only once)
// admin.initializeApp();
/**
* Claim Student Profile - Link game account to NWR student ID
*
* @param {Object} data - Request data from Unity client
* @param {string} data.code - 6-digit access code
* @param {string} data.logicalAccountId - Firebase account ID to link
* @param {Object} context - Firebase Functions context (contains auth info)
*
* @returns {Object} Result with student information or error
*/
exports.claimStudentProfile = functions.https.onCall(async (data, context) => {
// --- VALIDATION ---
// Check authentication (require user to be signed in)
if (!context.auth) {
throw new functions.https.HttpsError(
'unauthenticated',
'User must be authenticated to link account'
);
}
const { code, logicalAccountId } = data;
const authUid = context.auth.uid;
// Validate input
if (!code || typeof code !== 'string' || code.length !== 6) {
throw new functions.https.HttpsError(
'invalid-argument',
'Access code must be exactly 6 digits'
);
}
if (!logicalAccountId || typeof logicalAccountId !== 'string') {
throw new functions.https.HttpsError(
'invalid-argument',
'Logical account ID is required'
);
}
// --- RATE LIMITING ---
const db = admin.firestore();
const rateLimitRef = db.collection('rate_limits').doc(authUid);
try {
const rateLimitDoc = await rateLimitRef.get();
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute window
const maxAttempts = 5; // Max 5 attempts per minute
if (rateLimitDoc.exists) {
const data = rateLimitDoc.data();
const attempts = data.attempts || [];
// Filter attempts within the time window
const recentAttempts = attempts.filter(timestamp => now - timestamp < windowMs);
if (recentAttempts.length >= maxAttempts) {
throw new functions.https.HttpsError(
'resource-exhausted',
'Too many verification attempts. Please wait a minute and try again.'
);
}
// Update rate limit document
await rateLimitRef.update({
attempts: [...recentAttempts, now]
});
} else {
// Create rate limit document
await rateLimitRef.set({
attempts: [now],
userId: authUid
});
}
} catch (error) {
if (error instanceof functions.https.HttpsError) {
throw error; // Re-throw rate limit errors
}
// Log other errors but don't fail the request
console.error('Rate limiting error:', error);
}
// --- CHECK IF ALREADY LINKED ---
const userProfileRef = db.collection('users').doc(authUid)
.collection('profiles').doc(logicalAccountId);
const profileDoc = await userProfileRef.get();
if (profileDoc.exists && profileDoc.data().linkedStudentId) {
throw new functions.https.HttpsError(
'already-exists',
'This account is already linked to a student profile'
);
}
// --- VERIFY CODE WITH EXTERNAL API ---
// Determine which API to call based on environment
const environment = functions.config().nwr?.environment || 'dev';
let apiUrl, apiKey;
if (environment === 'dev' || environment === 'development') {
// Development mode - use mock server
apiUrl = functions.config().nwr?.mock_api_url || 'http://localhost:3001/api/verify-code';
apiKey = 'dev-key'; // No real auth for mock server
console.log('[DEV MODE] Using mock server:', apiUrl);
} else {
// Production mode - use Azure Data Lagoon
apiUrl = functions.config().nwr?.azure_api_url;
apiKey = functions.config().nwr?.azure_api_key;
if (!apiUrl || !apiKey) {
throw new functions.https.HttpsError(
'failed-precondition',
'Azure API credentials not configured'
);
}
console.log('[PROD MODE] Using Azure Data Lagoon');
}
let verificationResult;
try {
const response = await axios.post(apiUrl, {
code: code,
logicalAccountId: logicalAccountId
}, {
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey
},
timeout: 10000 // 10 second timeout
});
verificationResult = response.data;
// Check if verification was successful
if (!verificationResult.success || !verificationResult.valid) {
throw new functions.https.HttpsError(
'not-found',
verificationResult.error || 'Invalid or expired access code'
);
}
} catch (error) {
if (error instanceof functions.https.HttpsError) {
throw error;
}
// Handle axios errors
if (error.response) {
// Server responded with error status
const errorData = error.response.data;
throw new functions.https.HttpsError(
'internal',
errorData.error || 'Failed to verify access code',
{ statusCode: error.response.status }
);
} else if (error.request) {
// Request was made but no response
throw new functions.https.HttpsError(
'unavailable',
'Could not reach verification service. Please try again later.'
);
} else {
// Other errors
throw new functions.https.HttpsError(
'internal',
'An unexpected error occurred during verification'
);
}
}
// --- UPDATE FIRESTORE ---
const studentId = verificationResult.studentId;
const gradeLevel = verificationResult.gradeLevel;
const studentName = verificationResult.studentName;
try {
// Update user profile with linked student ID
await userProfileRef.set({
linkedStudentId: studentId,
linkedStudentName: studentName, // Optional: for display purposes
gradeLevel: gradeLevel,
linkedDate: admin.firestore.FieldValue.serverTimestamp(),
logicalAccountId: logicalAccountId,
authUid: authUid
}, { merge: true });
// Also update the main user document
await db.collection('users').doc(authUid).set({
lastLinkedProfile: logicalAccountId,
lastLinkedStudent: studentId,
lastUpdateDate: admin.firestore.FieldValue.serverTimestamp()
}, { merge: true });
console.log(`Successfully linked account ${logicalAccountId} to student ${studentId}`);
} catch (error) {
console.error('Firestore update error:', error);
throw new functions.https.HttpsError(
'internal',
'Failed to save link information. Please try again.'
);
}
// --- LOG TO ANALYTICS ---
// Note: Firebase Analytics events are automatically logged for Cloud Functions
// You can also manually log custom events if needed
// --- RETURN SUCCESS ---
return {
success: true,
studentId: studentId,
gradeLevel: gradeLevel,
studentName: studentName,
linkedDate: new Date().toISOString()
};
});
/**
* Configuration Required:
*
* Run these commands to set configuration:
*
* Development:
* firebase functions:config:set nwr.environment="dev"
* firebase functions:config:set nwr.mock_api_url="http://localhost:3001/api/verify-code"
*
* Production:
* firebase functions:config:set nwr.environment="prod"
* firebase functions:config:set nwr.azure_api_url="https://your-azure-endpoint/api/verify-code"
* firebase functions:config:set nwr.azure_api_key="your-secure-api-key"
*
* View current config:
* firebase functions:config:get
*/
/**
* Testing this function locally:
*
* 1. Install Firebase CLI: npm install -g firebase-tools
* 2. Login: firebase login
* 3. Initialize functions: firebase init functions
* 4. Copy this file to functions/ directory
* 5. Run locally: firebase emulators:start
* 6. Test from Unity with emulator URL
*/