Skip to content

Commit 358dc8a

Browse files
mjunaidcaclaude
andcommitted
fix(auth): Fix TypeScript build errors and OAuth client types
Fixed multiple TypeScript compilation errors: - Removed unused createAdminUser() function with undefined hashPassword call - Added explicit type annotations for map callbacks (client, m, u parameters) - Changed OAuth client type from "confidential" to "web" (Better Auth compliant) - Updated seed script to check client secret presence instead of type Profile improvements: - Added redirect parameter support to profile page - ProfileForm now redirects back to client app after save - NavbarAuth shows Edit Profile button with auto-refresh on return Build now completes successfully with all type checks passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 8753023 commit 358dc8a

File tree

10 files changed

+87
-80
lines changed

10 files changed

+87
-80
lines changed

auth-server/scripts/seed-setup.ts

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ const TEST_ORG = {
174174
* Upsert OAuth client from trusted-clients.ts configuration
175175
*/
176176
async function upsertClient(client: typeof TRUSTED_CLIENTS[0]) {
177-
// Determine auth method based on client type
178-
const authMethod = client.type === "confidential" ? "client_secret_basic" : "none";
177+
// Determine auth method based on presence of client secret
179178
const clientSecret = 'clientSecret' in client ? client.clientSecret : null;
179+
const authMethod = clientSecret ? "client_secret_basic" : "none";
180180

181181
const dbClient = {
182182
id: `${client.clientId}-id`,
@@ -223,50 +223,10 @@ async function upsertClient(client: typeof TRUSTED_CLIENTS[0]) {
223223
}
224224

225225
/**
226-
* Create or get admin user
226+
* Note: Admin user creation is handled via Better Auth API (see createAdminViaAPI above)
227+
* This ensures password hashing uses Better Auth's exact implementation.
228+
* The seed script only checks for existing admin and provides API instructions if needed.
227229
*/
228-
async function createAdminUser() {
229-
// Check if admin user exists
230-
const existingUser = await db
231-
.select()
232-
.from(user)
233-
.where(eq(user.email, TEST_ADMIN_EMAIL));
234-
235-
if (existingUser.length > 0) {
236-
console.log(` ✅ Admin user exists: ${TEST_ADMIN_EMAIL}`);
237-
return existingUser[0].id;
238-
}
239-
240-
// Create admin user
241-
console.log(` ✅ Creating admin user: ${TEST_ADMIN_EMAIL}`);
242-
const userId = crypto.randomUUID();
243-
const hashedPassword = await hashPassword(TEST_ADMIN_PASSWORD);
244-
245-
await db.insert(user).values({
246-
id: userId,
247-
email: TEST_ADMIN_EMAIL,
248-
emailVerified: true, // Skip email verification for local dev
249-
name: TEST_ADMIN_NAME,
250-
createdAt: new Date(),
251-
updatedAt: new Date(),
252-
});
253-
254-
// Create account with password
255-
await db.insert(account).values({
256-
id: crypto.randomUUID(),
257-
userId: userId,
258-
accountId: userId,
259-
providerId: "credential",
260-
password: hashedPassword,
261-
createdAt: new Date(),
262-
updatedAt: new Date(),
263-
});
264-
265-
console.log(` 📧 Email: ${TEST_ADMIN_EMAIL}`);
266-
console.log(` 🔑 Password: ${TEST_ADMIN_PASSWORD}`);
267-
268-
return userId;
269-
}
270230

271231
/**
272232
* Seed default organization (production + dev)
Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
1-
import { neon } from "@neondatabase/serverless";
2-
import { drizzle } from "drizzle-orm/neon-http";
3-
import { pgTable, text, boolean } from "drizzle-orm/pg-core";
1+
import { db } from "../src/lib/db";
2+
import { user } from "../auth-schema";
43
import { eq } from "drizzle-orm";
5-
import dotenv from "dotenv";
64

7-
dotenv.config({ path: ".env.local" });
5+
async function verifyTestUser() {
6+
try {
7+
const result = await db
8+
.update(user)
9+
.set({ emailVerified: true })
10+
.where(eq(user.email, "[email protected]"))
11+
.returning();
812

9-
const user = pgTable("user", {
10-
id: text("id").primaryKey(),
11-
email: text("email").notNull().unique(),
12-
emailVerified: boolean("email_verified").default(false).notNull(),
13-
});
14-
15-
const sql = neon(process.env.DATABASE_URL!);
16-
const db = drizzle(sql);
17-
18-
async function main() {
19-
const result = await db.update(user)
20-
.set({ emailVerified: true })
21-
.where(eq(user.email, "[email protected]"))
22-
.returning({ id: user.id, email: user.email, emailVerified: user.emailVerified });
23-
24-
console.log("User verified:", result);
13+
if (result.length > 0) {
14+
console.log("✅ Email verified for [email protected]");
15+
console.log("User:", result[0]);
16+
} else {
17+
console.log("❌ User not found");
18+
}
19+
} catch (error) {
20+
console.error("Error:", error);
21+
}
2522
process.exit(0);
2623
}
2724

28-
main().catch(console.error);
25+
verifyTestUser();

auth-server/src/app/account/profile/ProfileForm.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"use client";
22

33
import { useState } from "react";
4-
import { useRouter } from "next/navigation";
54

6-
export default function ProfileForm({ user }: { user: any }) {
7-
const router = useRouter();
5+
export default function ProfileForm({
6+
user,
7+
redirectUrl
8+
}: {
9+
user: any;
10+
redirectUrl: string | null;
11+
}) {
812
const [loading, setLoading] = useState(false);
913
const [formData, setFormData] = useState({
1014
// OIDC Standard Claims (editable)
@@ -33,8 +37,13 @@ export default function ProfileForm({ user }: { user: any }) {
3337
});
3438

3539
if (response.ok) {
36-
// Refresh the page to get updated session data
37-
window.location.reload();
40+
if (redirectUrl) {
41+
// Redirect back to client app
42+
window.location.href = redirectUrl;
43+
} else {
44+
// No redirect URL, just reload current page
45+
window.location.reload();
46+
}
3847
} else {
3948
const error = await response.json();
4049
alert(`Error: ${error.message}`);

auth-server/src/app/account/profile/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { headers } from "next/headers";
33
import { redirect } from "next/navigation";
44
import ProfileForm from "./ProfileForm";
55

6-
export default async function ProfilePage() {
6+
export default async function ProfilePage({
7+
searchParams,
8+
}: {
9+
searchParams: Promise<{ redirect?: string }>;
10+
}) {
711
const session = await auth.api.getSession({
812
headers: await headers(),
913
});
@@ -12,10 +16,13 @@ export default async function ProfilePage() {
1216
redirect("/auth/sign-in");
1317
}
1418

19+
const params = await searchParams;
20+
const redirectUrl = params.redirect || null;
21+
1522
return (
1623
<div className="max-w-2xl mx-auto p-6">
1724
<h1 className="text-2xl font-bold mb-6">Profile Settings</h1>
18-
<ProfileForm user={session.user} />
25+
<ProfileForm user={session.user} redirectUrl={redirectUrl} />
1926
</div>
2027
);
2128
}

auth-server/src/app/api/admin/clients/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function GET() {
4040
}).from(oauthApplication);
4141

4242
// Parse redirectUrls and metadata - handle both JSON strings and plain strings
43-
const parsedClients = clients.map(client => {
43+
const parsedClients = clients.map((client: typeof clients[number]) => {
4444
let redirectUrls: string[] = [];
4545
let metadata: Record<string, unknown> = {};
4646

@@ -52,7 +52,7 @@ export async function GET() {
5252
} catch {
5353
// Not JSON, treat as single URL or comma-separated
5454
redirectUrls = client.redirectUrls.includes(',')
55-
? client.redirectUrls.split(',').map(u => u.trim())
55+
? client.redirectUrls.split(',').map((u: string) => u.trim())
5656
: [client.redirectUrls];
5757
}
5858
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function GET() {
4343
.from(member)
4444
.where(eq(member.userId, session.user.id));
4545

46-
const organizationIds = memberships.map((m) => m.organizationId);
46+
const organizationIds = memberships.map((m: typeof memberships[number]) => m.organizationId);
4747

4848
// Return flattened structure with organizationIds (expected by tests)
4949
return NextResponse.json({

auth-server/src/lib/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ export const auth = betterAuth({
498498
.where(eq(member.userId, user.id));
499499

500500
// Get all organization IDs the user belongs to
501-
const organizationIds = memberships.map((m) => m.organizationId);
501+
const organizationIds = memberships.map((m: typeof memberships[number]) => m.organizationId);
502502

503503
// Primary tenant is the first organization (can be extended to support active org)
504504
const primaryTenantId = organizationIds[0] || null;

auth-server/src/lib/trusted-clients.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const TRUSTED_CLIENTS = [
7373
{
7474
clientId: "robolearn-confidential-client",
7575
name: "RoboLearn Backend Service (Test)",
76-
type: "confidential" as const,
76+
type: "web" as const, // "web" type for server-side confidential clients with secrets
7777
clientSecret: "robolearn-confidential-secret-for-testing-only",
7878
redirectUrls: getRedirectUrls([
7979
"http://localhost:8000/auth/callback",

robolearn-interface/.mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"shadcn": {
4+
"type": "http",
5+
"url": "https://www.shadcn.io/api/mcp"
6+
}
7+
}
8+
}

robolearn-interface/src/components/NavbarAuth/index.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
66
import styles from './styles.module.css';
77

88
export function NavbarAuth() {
9-
const { session, isLoading, signOut } = useAuth();
9+
const { session, isLoading, signOut, refreshUserData } = useAuth();
1010
const { siteConfig } = useDocusaurusContext();
1111
const authUrl = (siteConfig.customFields?.authUrl as string) || 'http://localhost:3001';
1212
const oauthClientId = (siteConfig.customFields?.oauthClientId as string) || 'robolearn-interface';
@@ -63,6 +63,26 @@ export function NavbarAuth() {
6363
return email ? email[0].toUpperCase() : '?';
6464
};
6565

66+
// Handle Edit Profile - open auth server profile page
67+
const handleEditProfile = () => {
68+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
69+
const profileUrl = `${authUrl}/account/profile?redirect=${encodeURIComponent(currentUrl)}`;
70+
71+
// Store a flag so we know to refresh data when user returns
72+
localStorage.setItem('robolearn_refresh_on_return', 'true');
73+
74+
window.location.href = profileUrl;
75+
};
76+
77+
// Refresh data if user just returned from profile editing
78+
useEffect(() => {
79+
const shouldRefresh = localStorage.getItem('robolearn_refresh_on_return');
80+
if (shouldRefresh === 'true' && session?.user) {
81+
localStorage.removeItem('robolearn_refresh_on_return');
82+
refreshUserData();
83+
}
84+
}, [session]);
85+
6686
if (isLoading) {
6787
return (
6888
<div className={styles.authContainer}>
@@ -139,6 +159,12 @@ export function NavbarAuth() {
139159
)}
140160

141161
<div className={styles.dropdownDivider} />
162+
<button onClick={handleEditProfile} className={styles.dropdownItem}>
163+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
164+
<path d="M11.3333 2.00004C11.5084 1.82494 11.716 1.68605 11.9441 1.59129C12.1722 1.49653 12.4165 1.44775 12.6633 1.44775C12.9101 1.44775 13.1544 1.49653 13.3825 1.59129C13.6106 1.68605 13.8183 1.82494 13.9933 2.00004C14.1684 2.17513 14.3073 2.38283 14.4021 2.61091C14.4968 2.83899 14.5456 3.08333 14.5456 3.33004C14.5456 3.57675 14.4968 3.82109 14.4021 4.04917C14.3073 4.27725 14.1684 4.48495 13.9933 4.66004L5.33333 13.32L2 14L2.68 10.6667L11.3333 2.00004Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
165+
</svg>
166+
Edit Profile
167+
</button>
142168
<button onClick={() => signOut()} className={styles.dropdownItem}>
143169
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
144170
<path d="M6 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V3.33333C2 2.97971 2.14048 2.64057 2.39052 2.39052C2.64057 2.14048 2.97971 2 3.33333 2H6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>

0 commit comments

Comments
 (0)