Skip to content

Commit 131b766

Browse files
authored
Merge pull request #22 from MisbahAN/misbah/ft/p1-02-04-auth-db-users
authentication, user persistence, and onboarding (issues #2, #3, #4)
2 parents 86b3675 + ca3cac0 commit 131b766

25 files changed

Lines changed: 5231 additions & 2234 deletions

.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ FIREBASE_AUTH_DOMAIN=
55
FIREBASE_APP_ID=
66
FIREBASE_STORAGE_BUCKET=
77
FIREBASE_MESSAGING_SENDER_ID=
8-
FIREBASE_MEASUREMENT_ID=
8+
FIREBASE_MEASUREMENT_ID=G-
9+
10+
# Vite (Firebase variables prefixed with VITE_)
11+
VITE_FIREBASE_PROJECT_ID=
12+
VITE_FIREBASE_API_KEY=
13+
VITE_FIREBASE_AUTH_DOMAIN=
14+
VITE_FIREBASE_APP_ID=
15+
VITE_FIREBASE_STORAGE_BUCKET=
16+
VITE_FIREBASE_MESSAGING_SENDER_ID=
17+
VITE_FIREBASE_MEASUREMENT_ID=
918

1019
# MongoDB
1120
MONGODB_URI=

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ dist-ssr
2323
*.sln
2424
*.sw?
2525
.env
26-
.claude
26+
.claude
27+
.vercel

api/onboarding.ts

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ type VercelResponse = {
1717
json: (data: unknown) => void;
1818
send: (data: unknown) => void;
1919
};
20-
import { generateAdaptiveQuestions } from "../src/lib/services/gemini";
21-
import { getCollection } from "../src/lib/mongodb";
20+
import { generateAdaptiveQuestions } from "../src/lib/services/gemini.js";
21+
import { getCollection } from "../src/lib/mongodb.js";
2222

2323
export default async function handler(
2424
req: VercelRequest,
@@ -29,23 +29,62 @@ export default async function handler(
2929
}
3030

3131
try {
32-
interface OnboardingRequestBody {
32+
type OnboardingAction = "generate" | "complete";
33+
interface BaseOnboardingBody {
34+
action?: OnboardingAction;
3335
firebaseUid?: string;
36+
}
37+
interface GenerateBody extends BaseOnboardingBody {
3438
ageRange?: string;
3539
experience?: string;
3640
monthlyIncome?: string;
3741
riskTolerance?: number;
3842
learningGoals?: string[];
3943
}
40-
const body = req.body as OnboardingRequestBody;
44+
interface CompleteBody extends BaseOnboardingBody {
45+
action: "complete";
46+
answers?: Record<string, unknown>;
47+
startingLesson?: number;
48+
}
49+
50+
const body = req.body as GenerateBody | CompleteBody;
51+
const action: OnboardingAction = body.action || "generate";
52+
53+
if (action === "complete") {
54+
const { firebaseUid, answers, startingLesson } = body as CompleteBody;
55+
56+
if (!firebaseUid || !answers) {
57+
return res.status(400).json({ error: "Missing required completion data" });
58+
}
59+
60+
const usersCollection = await getCollection("users");
61+
const result = await usersCollection.updateOne(
62+
{ firebaseUid },
63+
{
64+
$set: {
65+
onboardingCompleted: true,
66+
onboardingAnswers: answers,
67+
currentLesson: typeof startingLesson === "number" ? startingLesson : 1,
68+
updatedAt: new Date(),
69+
},
70+
}
71+
);
72+
73+
if (result.matchedCount === 0) {
74+
return res.status(404).json({ error: "User not found" });
75+
}
76+
77+
return res.status(200).json({ success: true });
78+
}
79+
4180
const {
4281
firebaseUid,
4382
ageRange,
4483
experience,
4584
monthlyIncome,
4685
riskTolerance,
4786
learningGoals,
48-
} = body;
87+
} = body as GenerateBody;
4988

5089
// Validate required fields
5190
if (!firebaseUid || !ageRange || !experience || !monthlyIncome || riskTolerance === undefined || !learningGoals) {
@@ -61,28 +100,6 @@ export default async function handler(
61100
learningGoals
62101
);
63102

64-
// Save to MongoDB
65-
const usersCollection = await getCollection("users");
66-
67-
await usersCollection.updateOne(
68-
{ firebaseUid },
69-
{
70-
$set: {
71-
onboardingCompleted: true,
72-
onboardingAnswers: {
73-
ageRange,
74-
investmentExperience: experience,
75-
monthlyIncome,
76-
riskTolerance,
77-
goals: learningGoals,
78-
},
79-
currentLesson: geminiResponse.startingLesson,
80-
updatedAt: new Date(),
81-
},
82-
},
83-
{ upsert: true }
84-
);
85-
86103
return res.status(200).json({
87104
adaptiveQuestions: geminiResponse.adaptiveQuestions,
88105
startingLesson: geminiResponse.startingLesson,
@@ -94,4 +111,4 @@ export default async function handler(
94111
message: error instanceof Error ? error.message : "Unknown error"
95112
});
96113
}
97-
}
114+
}

api/users/[uid].ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* GET /api/users/[uid] - Get user by Firebase UID
3+
* PUT /api/users/[uid] - Update user profile
4+
*/
5+
6+
import type { VercelRequest, VercelResponse } from "@vercel/node";
7+
import { MongoClient } from "mongodb";
8+
9+
const MONGODB_URI = process.env.MONGODB_URI!;
10+
const DB_NAME = "finwise";
11+
12+
let cachedClient: MongoClient | null = null;
13+
14+
async function connectToDatabase() {
15+
if (cachedClient) {
16+
return cachedClient.db(DB_NAME);
17+
}
18+
19+
const client = await MongoClient.connect(MONGODB_URI, {
20+
maxPoolSize: 10,
21+
minPoolSize: 2,
22+
maxIdleTimeMS: 60000,
23+
});
24+
25+
cachedClient = client;
26+
return client.db(DB_NAME);
27+
}
28+
29+
export default async function handler(
30+
req: VercelRequest,
31+
res: VercelResponse,
32+
) {
33+
const { uid } = req.query;
34+
35+
if (!uid || typeof uid !== "string") {
36+
return res.status(400).json({ error: "Missing or invalid uid parameter" });
37+
}
38+
39+
try {
40+
const db = await connectToDatabase();
41+
const usersCollection = db.collection("users");
42+
43+
// GET - Fetch user data
44+
if (req.method === "GET") {
45+
const user = await usersCollection.findOne({ firebaseUid: uid });
46+
47+
if (!user) {
48+
return res.status(404).json({ error: "User not found" });
49+
}
50+
51+
// Update lastLoginAt
52+
await usersCollection.updateOne(
53+
{ firebaseUid: uid },
54+
{ $set: { lastLoginAt: new Date() } },
55+
);
56+
57+
return res.status(200).json({
58+
success: true,
59+
user,
60+
});
61+
}
62+
63+
// PUT - Update user profile
64+
if (req.method === "PUT") {
65+
const updateData = req.body;
66+
67+
// Remove fields that shouldn't be updated directly
68+
delete updateData._id;
69+
delete updateData.firebaseUid;
70+
delete updateData.createdAt;
71+
72+
const result = await usersCollection.updateOne(
73+
{ firebaseUid: uid },
74+
{ $set: updateData },
75+
);
76+
77+
if (result.matchedCount === 0) {
78+
return res.status(404).json({ error: "User not found" });
79+
}
80+
81+
// Fetch updated user
82+
const updatedUser = await usersCollection.findOne({ firebaseUid: uid });
83+
84+
return res.status(200).json({
85+
success: true,
86+
user: updatedUser,
87+
});
88+
}
89+
90+
// Method not allowed
91+
return res.status(405).json({ error: "Method not allowed" });
92+
} catch (error) {
93+
console.error("Error handling user request:", error);
94+
return res.status(500).json({
95+
error: "Internal server error",
96+
message: error instanceof Error ? error.message : "Unknown error",
97+
});
98+
}
99+
}

api/users/index.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* POST /api/users
3+
* Create a new user in MongoDB after Firebase auth
4+
*/
5+
6+
import type { VercelRequest, VercelResponse } from "@vercel/node";
7+
import { MongoClient } from "mongodb";
8+
9+
const MONGODB_URI = process.env.MONGODB_URI!;
10+
const DB_NAME = "finwise";
11+
12+
let cachedClient: MongoClient | null = null;
13+
14+
async function connectToDatabase() {
15+
if (cachedClient) {
16+
return cachedClient.db(DB_NAME);
17+
}
18+
19+
const client = await MongoClient.connect(MONGODB_URI, {
20+
maxPoolSize: 10,
21+
minPoolSize: 2,
22+
maxIdleTimeMS: 60000,
23+
});
24+
25+
cachedClient = client;
26+
return client.db(DB_NAME);
27+
}
28+
29+
interface CreateUserRequest {
30+
firebaseUid: string;
31+
email: string;
32+
displayName: string;
33+
photoURL?: string;
34+
}
35+
36+
export default async function handler(
37+
req: VercelRequest,
38+
res: VercelResponse,
39+
) {
40+
// Only allow POST requests
41+
if (req.method !== "POST") {
42+
return res.status(405).json({ error: "Method not allowed" });
43+
}
44+
45+
try {
46+
const { firebaseUid, email, displayName, photoURL } =
47+
req.body as CreateUserRequest;
48+
49+
// Validate required fields
50+
if (!firebaseUid || !email || !displayName) {
51+
return res.status(400).json({
52+
error: "Missing required fields: firebaseUid, email, displayName",
53+
});
54+
}
55+
56+
const db = await connectToDatabase();
57+
const usersCollection = db.collection("users");
58+
59+
const now = new Date();
60+
61+
const defaults = {
62+
firebaseUid,
63+
email,
64+
displayName,
65+
photoURL: photoURL || "",
66+
createdAt: now,
67+
lastLoginAt: now,
68+
69+
// Onboarding
70+
onboardingCompleted: false,
71+
onboardingAnswers: {},
72+
73+
// Current Progress
74+
currentLesson: 1,
75+
totalXP: 0,
76+
currentStreak: 0,
77+
longestStreak: 0,
78+
lastLessonDate: null,
79+
80+
// Social
81+
friends: [],
82+
friendRequestsSent: [],
83+
friendRequestsReceived: [],
84+
85+
// Settings
86+
settings: {
87+
notificationsEnabled: true,
88+
emailNotifications: true,
89+
lessonReminders: true,
90+
voiceMode: false,
91+
},
92+
};
93+
94+
const result = await usersCollection.updateOne(
95+
{ firebaseUid },
96+
{
97+
$setOnInsert: defaults,
98+
$set: {
99+
email,
100+
displayName,
101+
photoURL: photoURL || "",
102+
lastLoginAt: now,
103+
},
104+
},
105+
{ upsert: true },
106+
);
107+
108+
const user = await usersCollection.findOne({ firebaseUid });
109+
if (!user) {
110+
throw new Error("Failed to create or fetch user");
111+
}
112+
113+
const wasExisting = result.upsertedCount === 0;
114+
115+
return res.status(wasExisting ? 200 : 201).json({
116+
success: true,
117+
user,
118+
});
119+
} catch (error) {
120+
console.error("Error creating user:", error);
121+
return res.status(500).json({
122+
error: "Internal server error",
123+
message: error instanceof Error ? error.message : "Unknown error",
124+
});
125+
}
126+
}

0 commit comments

Comments
 (0)