Skip to content

Commit 38e5ce4

Browse files
authored
Merge Add hardware tier to signup and improve auth UX pull request #9 from mjunaidca/feat/signup-profile-ux-improvements
feat: Add hardware tier to signup and improve auth UX
2 parents ec64379 + 39f2791 commit 38e5ce4

File tree

16 files changed

+933
-280
lines changed

16 files changed

+933
-280
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "user_profile" ADD COLUMN "hardware_tier" text;

auth-server/src/app/api/profile/route.ts

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
22
import { auth } from "@/lib/auth";
33
import { headers } from "next/headers";
44
import { db } from "@/lib/db";
5-
import { userProfile, SoftwareBackground } from "@/lib/db/schema";
5+
import { userProfile, user, SoftwareBackground, HardwareTier } from "@/lib/db/schema";
66
import { eq } from "drizzle-orm";
77
import { randomUUID } from "crypto";
88

@@ -35,6 +35,7 @@ export async function GET() {
3535
profile: profile
3636
? {
3737
softwareBackground: profile.softwareBackground,
38+
hardwareTier: profile.hardwareTier,
3839
createdAt: profile.createdAt,
3940
updatedAt: profile.updatedAt,
4041
}
@@ -52,19 +53,52 @@ export async function GET() {
5253
// POST /api/profile - Create user profile (called after signup)
5354
export async function POST(request: NextRequest) {
5455
try {
55-
const session = await auth.api.getSession({
56-
headers: await headers(),
57-
});
58-
59-
if (!session) {
60-
return NextResponse.json(
61-
{ error: "Unauthorized" },
62-
{ status: 401 }
63-
);
64-
}
65-
6656
const body = await request.json();
67-
const { softwareBackground } = body;
57+
const { softwareBackground, hardwareTier, userId } = body;
58+
59+
let targetUserId: string;
60+
61+
// Allow creating profile during signup with userId (user not verified yet)
62+
// OR with authenticated session (user is signed in)
63+
if (userId) {
64+
// Validate user exists and was created recently (within 5 minutes)
65+
// This prevents abuse while allowing signup-time profile creation
66+
const userRecord = await db.query.user.findFirst({
67+
where: eq(user.id, userId),
68+
});
69+
70+
if (!userRecord) {
71+
return NextResponse.json(
72+
{ error: "User not found" },
73+
{ status: 404 }
74+
);
75+
}
76+
77+
// Check if user was created recently (within 5 minutes)
78+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
79+
if (userRecord.createdAt < fiveMinutesAgo) {
80+
return NextResponse.json(
81+
{ error: "Profile creation window expired. Please sign in and update your profile." },
82+
{ status: 403 }
83+
);
84+
}
85+
86+
targetUserId = userId;
87+
} else {
88+
// Fallback to session-based authentication
89+
const session = await auth.api.getSession({
90+
headers: await headers(),
91+
});
92+
93+
if (!session) {
94+
return NextResponse.json(
95+
{ error: "Unauthorized. Provide userId for signup or sign in first." },
96+
{ status: 401 }
97+
);
98+
}
99+
100+
targetUserId = session.user.id;
101+
}
68102

69103
// Validate software background
70104
const validBackgrounds: SoftwareBackground[] = ["beginner", "intermediate", "advanced"];
@@ -75,9 +109,18 @@ export async function POST(request: NextRequest) {
75109
);
76110
}
77111

112+
// Validate hardware tier
113+
const validTiers: HardwareTier[] = ["tier1", "tier2", "tier3", "tier4"];
114+
if (!hardwareTier || !validTiers.includes(hardwareTier)) {
115+
return NextResponse.json(
116+
{ error: "Invalid hardware tier. Must be: tier1, tier2, tier3, or tier4" },
117+
{ status: 400 }
118+
);
119+
}
120+
78121
// Check if profile already exists
79122
const existingProfile = await db.query.userProfile.findFirst({
80-
where: eq(userProfile.userId, session.user.id),
123+
where: eq(userProfile.userId, targetUserId),
81124
});
82125

83126
if (existingProfile) {
@@ -92,14 +135,16 @@ export async function POST(request: NextRequest) {
92135
.insert(userProfile)
93136
.values({
94137
id: randomUUID(),
95-
userId: session.user.id,
138+
userId: targetUserId,
96139
softwareBackground,
140+
hardwareTier,
97141
})
98142
.returning();
99143

100144
return NextResponse.json({
101145
profile: {
102146
softwareBackground: newProfile[0].softwareBackground,
147+
hardwareTier: newProfile[0].hardwareTier,
103148
createdAt: newProfile[0].createdAt,
104149
updatedAt: newProfile[0].updatedAt,
105150
},
@@ -128,24 +173,41 @@ export async function PUT(request: NextRequest) {
128173
}
129174

130175
const body = await request.json();
131-
const { softwareBackground } = body;
176+
const { softwareBackground, hardwareTier } = body;
177+
178+
// Validate software background (if provided)
179+
if (softwareBackground) {
180+
const validBackgrounds: SoftwareBackground[] = ["beginner", "intermediate", "advanced"];
181+
if (!validBackgrounds.includes(softwareBackground)) {
182+
return NextResponse.json(
183+
{ error: "Invalid software background. Must be: beginner, intermediate, or advanced" },
184+
{ status: 400 }
185+
);
186+
}
187+
}
132188

133-
// Validate software background
134-
const validBackgrounds: SoftwareBackground[] = ["beginner", "intermediate", "advanced"];
135-
if (!validBackgrounds.includes(softwareBackground)) {
136-
return NextResponse.json(
137-
{ error: "Invalid software background. Must be: beginner, intermediate, or advanced" },
138-
{ status: 400 }
139-
);
189+
// Validate hardware tier (if provided)
190+
if (hardwareTier) {
191+
const validTiers: HardwareTier[] = ["tier1", "tier2", "tier3", "tier4"];
192+
if (!validTiers.includes(hardwareTier)) {
193+
return NextResponse.json(
194+
{ error: "Invalid hardware tier. Must be: tier1, tier2, tier3, or tier4" },
195+
{ status: 400 }
196+
);
197+
}
140198
}
141199

200+
// Build update object with only provided fields
201+
const updateData: { softwareBackground?: SoftwareBackground; hardwareTier?: HardwareTier; updatedAt: Date } = {
202+
updatedAt: new Date(),
203+
};
204+
if (softwareBackground) updateData.softwareBackground = softwareBackground;
205+
if (hardwareTier) updateData.hardwareTier = hardwareTier;
206+
142207
// Update profile
143208
const updatedProfile = await db
144209
.update(userProfile)
145-
.set({
146-
softwareBackground,
147-
updatedAt: new Date(),
148-
})
210+
.set(updateData)
149211
.where(eq(userProfile.userId, session.user.id))
150212
.returning();
151213

@@ -159,6 +221,7 @@ export async function PUT(request: NextRequest) {
159221
return NextResponse.json({
160222
profile: {
161223
softwareBackground: updatedProfile[0].softwareBackground,
224+
hardwareTier: updatedProfile[0].hardwareTier,
162225
createdAt: updatedProfile[0].createdAt,
163226
updatedAt: updatedProfile[0].updatedAt,
164227
},

auth-server/src/app/auth/layout.tsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,50 @@ export default function AuthLayout({
44
children: React.ReactNode;
55
}) {
66
return (
7-
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
8-
<div className="max-w-md w-full">
9-
<div className="text-center mb-8">
10-
<h1 className="text-3xl font-bold text-gray-900">RoboLearn</h1>
11-
<p className="mt-2 text-sm text-gray-600">
7+
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative overflow-hidden">
8+
{/* Animated background mesh */}
9+
<div className="absolute inset-0 gradient-mesh opacity-60" />
10+
11+
{/* Subtle grid pattern */}
12+
<div
13+
className="absolute inset-0 opacity-[0.03]"
14+
style={{
15+
backgroundImage: `
16+
linear-gradient(to right, #0f172a 1px, transparent 1px),
17+
linear-gradient(to bottom, #0f172a 1px, transparent 1px)
18+
`,
19+
backgroundSize: '48px 48px'
20+
}}
21+
/>
22+
23+
<div className="max-w-sm lg:max-w-md w-full relative z-10">
24+
{/* Logo/Brand */}
25+
<div className="text-center mb-10 animate-in slide-in-from-top">
26+
<h1 className="text-4xl font-bold mb-2">
27+
<span className="text-gradient">RoboLearn</span>
28+
</h1>
29+
<p className="text-sm text-slate-600 font-medium tracking-wide uppercase">
1230
Physical AI & Humanoid Robotics
1331
</p>
32+
<div className="mt-3 flex items-center justify-center gap-2">
33+
<div className="h-px w-12 bg-gradient-to-r from-transparent via-slate-300 to-transparent" />
34+
<div className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
35+
<div className="h-px w-12 bg-gradient-to-r from-transparent via-slate-300 to-transparent" />
36+
</div>
1437
</div>
15-
<div className="bg-white py-8 px-6 shadow-lg rounded-xl">
38+
39+
{/* Form container with glass effect */}
40+
<div className="glass-effect rounded-2xl shadow-2xl shadow-indigo-500/10 p-8 md:p-10 animate-in scale-in">
1641
{children}
1742
</div>
43+
44+
{/* Footer */}
45+
<div className="mt-8 text-center animate-in slide-in-from-bottom">
46+
<p className="text-xs text-slate-500">
47+
Secure authentication powered by{' '}
48+
<span className="font-medium text-slate-700">Better Auth</span>
49+
</p>
50+
</div>
1851
</div>
1952
</div>
2053
);
Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import { SignInForm } from "@/components/sign-in-form";
22
import { Suspense } from "react";
3+
import { redirect } from "next/navigation";
4+
import { auth } from "@/lib/auth";
5+
import { headers } from "next/headers";
6+
7+
export default async function SignInPage() {
8+
// Redirect authenticated users away from sign-in page
9+
const session = await auth.api.getSession({
10+
headers: await headers(),
11+
});
12+
13+
if (session) {
14+
redirect("/");
15+
}
316

4-
export default function SignInPage() {
517
return (
6-
<div>
7-
<h2 className="text-2xl font-semibold text-gray-900 text-center mb-6">
8-
Welcome back
9-
</h2>
10-
<Suspense
11-
fallback={
12-
<div className="flex items-center justify-center py-8">
13-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
14-
</div>
15-
}
16-
>
17-
<SignInForm />
18-
</Suspense>
19-
</div>
18+
<Suspense
19+
fallback={
20+
<div className="flex items-center justify-center py-8">
21+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
22+
</div>
23+
}
24+
>
25+
<SignInForm />
26+
</Suspense>
2027
);
2128
}
Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import { SignUpForm } from "@/components/sign-up-form";
22
import { Suspense } from "react";
3+
import { redirect } from "next/navigation";
4+
import { auth } from "@/lib/auth";
5+
import { headers } from "next/headers";
6+
7+
export default async function SignUpPage() {
8+
// Redirect authenticated users away from sign-up page
9+
const session = await auth.api.getSession({
10+
headers: await headers(),
11+
});
12+
13+
if (session) {
14+
redirect("/");
15+
}
316

4-
export default function SignUpPage() {
517
return (
6-
<div>
7-
<h2 className="text-2xl font-semibold text-gray-900 text-center mb-6">
8-
Create your account
9-
</h2>
10-
<Suspense
11-
fallback={
12-
<div className="flex items-center justify-center py-8">
13-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
14-
</div>
15-
}
16-
>
17-
<SignUpForm />
18-
</Suspense>
19-
</div>
18+
<Suspense
19+
fallback={
20+
<div className="flex items-center justify-center py-8">
21+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
22+
</div>
23+
}
24+
>
25+
<SignUpForm />
26+
</Suspense>
2027
);
2128
}

0 commit comments

Comments
 (0)