Skip to content

Commit 41a5b29

Browse files
marcodejonghclaude
andauthored
Add next-auth.js for user management (#232)
* Add next-auth.js for user management Would be handy to have users in Boardsesh, so this is a first step to adding it. Only have ran through the google login setup so far. Have yet to do apple * Remove .vscode from git tracking and add to .gitignore 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix lint issues and update migration to use conditional statements - Remove unused imports and variables - Update migration 0016 to use CREATE TABLE IF NOT EXISTS - Add conditional foreign key constraints to handle existing tables - Build and lint now pass successfully 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d503b3a commit 41a5b29

File tree

24 files changed

+4887
-73
lines changed

24 files changed

+4887
-73
lines changed

.env.local

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ POSTGRES_PORT=5432
1010
POSTGRES_USER=postgres
1111
POSTGRES_PASSWORD=password
1212
POSTGRES_DATABASE=main
13-
IRON_SESSION_PASSWORD={ "1": "68cJgCDE39gaXwi8LTVW4WioyhGxwcAd" }
13+
IRON_SESSION_PASSWORD={ "1": "68cJgCDE39gaXwi8LTVW4WioyhGxwcAd" }
14+
15+
# NextAuth.js configuration
16+
NEXTAUTH_SECRET=68cJgCDE39gaXwi8LTVW4WioyhGxwcAd
17+
NEXTAUTH_URL=http://localhost:3000

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,8 @@ kilter.db
4040
tension.db
4141
db/tmp
4242

43+
# editor
44+
.vscode
45+
4346
# board controller
4447
board-controller/board_controller.db

.vscode/launch.json

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import NextAuth from "next-auth";
2+
import { DrizzleAdapter } from "@auth/drizzle-adapter";
3+
import GoogleProvider from "next-auth/providers/google";
4+
import CredentialsProvider from "next-auth/providers/credentials";
5+
import { getDb } from "@/app/lib/db/db";
6+
import * as schema from "@/app/lib/db/schema";
7+
8+
const handler = NextAuth({
9+
adapter: DrizzleAdapter(getDb(), {
10+
usersTable: schema.users,
11+
accountsTable: schema.accounts,
12+
sessionsTable: schema.sessions,
13+
verificationTokensTable: schema.verificationTokens,
14+
}),
15+
providers: [
16+
GoogleProvider({
17+
clientId: process.env.GOOGLE_CLIENT_ID!,
18+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
19+
}),
20+
CredentialsProvider({
21+
name: "Email",
22+
credentials: {
23+
email: { label: "Email", type: "email", placeholder: "your@email.com" },
24+
password: { label: "Password", type: "password" }
25+
},
26+
async authorize(credentials) {
27+
// TODO: Add proper password hashing and database lookup
28+
// For now, this is a simple demo implementation
29+
if (credentials?.email && credentials?.password) {
30+
// In production, you'd:
31+
// 1. Look up user in database by email
32+
// 2. Verify password hash
33+
// 3. Return user object or null
34+
return {
35+
id: crypto.randomUUID(),
36+
email: credentials.email,
37+
name: credentials.email.split('@')[0],
38+
};
39+
}
40+
return null;
41+
}
42+
}),
43+
],
44+
session: {
45+
strategy: "jwt", // Required for credentials provider
46+
},
47+
callbacks: {
48+
async session({ session, token }) {
49+
// Include user ID in session from JWT
50+
if (session?.user && token?.sub) {
51+
session.user.id = token.sub;
52+
}
53+
return session;
54+
},
55+
async jwt({ token, user }) {
56+
// Persist the OAuth access_token and user id to the token right after signin
57+
if (user) {
58+
token.id = user.id;
59+
}
60+
return token;
61+
},
62+
},
63+
});
64+
65+
export { handler as GET, handler as POST };
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getServerSession } from "next-auth/next";
2+
import { NextRequest, NextResponse } from "next/server";
3+
import { createUserBoardMapping, getUserBoardMappings } from "@/app/lib/auth/user-board-mappings";
4+
import { BoardName } from "@/app/lib/types";
5+
6+
export async function POST(request: NextRequest) {
7+
try {
8+
const session = await getServerSession();
9+
10+
if (!session?.user?.id) {
11+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12+
}
13+
14+
const body = await request.json();
15+
const { boardType, boardUserId, boardUsername } = body;
16+
17+
if (!boardType || !boardUserId) {
18+
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
19+
}
20+
21+
await createUserBoardMapping(
22+
session.user.id,
23+
boardType as BoardName,
24+
parseInt(boardUserId),
25+
boardUsername
26+
);
27+
28+
return NextResponse.json({ success: true });
29+
} catch (error) {
30+
console.error("Failed to create board mapping:", error);
31+
return NextResponse.json(
32+
{ error: "Failed to create board mapping" },
33+
{ status: 500 }
34+
);
35+
}
36+
}
37+
38+
export async function GET() {
39+
try {
40+
const session = await getServerSession();
41+
42+
if (!session?.user?.id) {
43+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
44+
}
45+
46+
const mappings = await getUserBoardMappings(session.user.id);
47+
return NextResponse.json({ mappings });
48+
} catch (error) {
49+
console.error("Failed to get board mappings:", error);
50+
return NextResponse.json(
51+
{ error: "Failed to get board mappings" },
52+
{ status: 500 }
53+
);
54+
}
55+
}

app/components/board-page/header.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
'use client';
22
import React from 'react';
3-
import { Flex } from 'antd';
3+
import { Flex, Button, Dropdown, MenuProps, Grid } from 'antd';
44
import { Header } from 'antd/es/layout/layout';
55
import Title from 'antd/es/typography/Title';
66
import Link from 'next/link';
7+
import { useSession, signIn, signOut } from 'next-auth/react';
78
import SearchButton from '../search-drawer/search-button';
89
import SearchClimbNameInput from '../search-drawer/search-climb-name-input';
910
import { UISearchParamsProvider } from '../queue-control/ui-searchparams-provider';
1011
import SendClimbToBoardButton from '../board-bluetooth-control/send-climb-to-board-button';
1112
import { BoardDetails } from '@/app/lib/types';
1213
import { ShareBoardButton } from './share-button';
14+
import { useBoardProvider } from '../board-provider/board-provider-context';
15+
import { UserOutlined, LogoutOutlined, LoginOutlined } from '@ant-design/icons';
1316
import AngleSelector from './angle-selector';
1417
import styles from './header.module.css';
1518

@@ -18,6 +21,24 @@ type BoardSeshHeaderProps = {
1821
angle?: number;
1922
};
2023
export default function BoardSeshHeader({ boardDetails, angle }: BoardSeshHeaderProps) {
24+
const { useBreakpoint } = Grid;
25+
const screens = useBreakpoint();
26+
const { data: session } = useSession();
27+
const { logout } = useBoardProvider();
28+
29+
const handleSignOut = () => {
30+
signOut();
31+
logout(); // Also logout from board provider
32+
};
33+
34+
const userMenuItems: MenuProps['items'] = [
35+
{
36+
key: 'logout',
37+
icon: <LogoutOutlined />,
38+
label: 'Logout',
39+
onClick: handleSignOut,
40+
},
41+
];
2142
return (
2243
<Header
2344
style={{
@@ -53,6 +74,22 @@ export default function BoardSeshHeader({ boardDetails, angle }: BoardSeshHeader
5374
{angle !== undefined && <AngleSelector boardName={boardDetails.board_name} currentAngle={angle} />}
5475
<ShareBoardButton />
5576
<SendClimbToBoardButton boardDetails={boardDetails} />
77+
{session?.user ? (
78+
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
79+
<Button icon={<UserOutlined />} type="text">
80+
{session.user.name || session.user.email}
81+
</Button>
82+
</Dropdown>
83+
) : (
84+
<Button
85+
icon={<LoginOutlined />}
86+
type="text"
87+
onClick={() => signIn()}
88+
size={screens.xs ? 'small' : 'middle'}
89+
>
90+
{screens.xs ? '' : 'Login'}
91+
</Button>
92+
)}
5693
</Flex>
5794
</Flex>
5895
</UISearchParamsProvider>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { SessionProvider } from 'next-auth/react';
5+
import { ReactNode } from 'react';
6+
7+
interface SessionProviderWrapperProps {
8+
children: ReactNode;
9+
}
10+
11+
export default function SessionProviderWrapper({ children }: SessionProviderWrapperProps) {
12+
return <SessionProvider>{children}</SessionProvider>;
13+
}

app/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AntdRegistry } from '@ant-design/nextjs-registry';
44
import { App } from 'antd';
55
import { Analytics } from '@vercel/analytics/react';
66
import { SpeedInsights } from '@vercel/speed-insights/next';
7+
import SessionProviderWrapper from './components/providers/session-provider';
78
import WebBluetoothWarning from './components/board-bluetooth-control/web-bluetooth-warning';
89

910
export default function RootLayout({ children }: { children: React.ReactNode }) {
@@ -14,12 +15,14 @@ export default function RootLayout({ children }: { children: React.ReactNode })
1415
</head>
1516
<body style={{ margin: 0 }}>
1617
<Analytics />
17-
<AntdRegistry>
18-
<App>
19-
<WebBluetoothWarning />
20-
{children}
21-
</App>
22-
</AntdRegistry>
18+
<SessionProviderWrapper>
19+
<AntdRegistry>
20+
<App>
21+
<WebBluetoothWarning />
22+
{children}
23+
</App>
24+
</AntdRegistry>
25+
</SessionProviderWrapper>
2326
<SpeedInsights />
2427
</body>
2528
</html>

app/lib/api-wrappers/aurora/getGyms.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

app/lib/auth/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { DefaultSession } from "next-auth";
2+
3+
declare module "next-auth" {
4+
interface Session {
5+
user: {
6+
id: string;
7+
} & DefaultSession["user"];
8+
}
9+
}

0 commit comments

Comments
 (0)