Skip to content

Commit 8d547b6

Browse files
authored
Merge pull request #21 from codegasms/auth-setup
[Partial] Auth setup part 3/n
2 parents 4a914d3 + 38fa9d3 commit 8d547b6

File tree

13 files changed

+917
-7
lines changed

13 files changed

+917
-7
lines changed

app/api/auth/login/route.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { NextResponse } from "next/server";
2+
import { loginSchema } from "@/lib/validations";
3+
import { db } from "@/db/drizzle";
4+
import { users } from "@/db/schema";
5+
import { eq } from "drizzle-orm";
6+
import { generateSessionToken, createSession } from "@/lib/server/session";
7+
import { setSessionTokenCookie } from "@/lib/server/cookies";
8+
import { Argon2id } from "oslo/password";
9+
10+
export async function POST(request: Request) {
11+
try {
12+
const body = await request.json();
13+
const validatedData = loginSchema.parse(body);
14+
15+
const user = await db.query.users.findFirst({
16+
where: eq(users.email, validatedData.email),
17+
});
18+
19+
if (!user) {
20+
return NextResponse.json(
21+
{ error: "Invalid credentials" },
22+
{ status: 401 },
23+
);
24+
}
25+
26+
const argon2id = new Argon2id();
27+
const isValidPassword = await argon2id.verify(
28+
user.hashedPassword,
29+
validatedData.password,
30+
);
31+
32+
if (!isValidPassword) {
33+
return NextResponse.json(
34+
{ error: "Invalid credentials" },
35+
{ status: 401 },
36+
);
37+
}
38+
39+
const token = generateSessionToken();
40+
const session = await createSession(token, user.id);
41+
await setSessionTokenCookie(token, session.expiresAt);
42+
43+
return NextResponse.json({
44+
_id: user.id,
45+
email: user.email,
46+
fullName: user.name,
47+
// role: user.role,
48+
});
49+
} catch (error) {
50+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
51+
}
52+
}

app/api/auth/logout/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NextResponse } from "next/server";
2+
import { getCurrentSession } from "@/lib/server/session";
3+
import { deleteSessionTokenCookie } from "@/lib/server/cookies";
4+
import { invalidateSession } from "@/lib/server/session";
5+
6+
export async function DELETE() {
7+
try {
8+
const { session } = await getCurrentSession();
9+
10+
if (session) {
11+
await invalidateSession(session.id);
12+
}
13+
14+
await deleteSessionTokenCookie();
15+
return NextResponse.json({ success: true });
16+
} catch (error) {
17+
return NextResponse.json({ error: "Failed to logout" }, { status: 500 });
18+
}
19+
}

app/api/auth/register/route.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { NextResponse } from "next/server";
2+
import { registerSchema } from "@/lib/validations";
3+
import { db } from "@/db/drizzle";
4+
import { users } from "@/db/schema";
5+
import { eq } from "drizzle-orm";
6+
import { generateSessionToken, createSession } from "@/lib/server/session";
7+
import { setSessionTokenCookie } from "@/lib/server/cookies";
8+
import { Argon2id } from "oslo/password";
9+
10+
export async function POST(request: Request) {
11+
try {
12+
const body = await request.json();
13+
const validatedData = registerSchema.parse(body);
14+
15+
const existingUser = await db.query.users.findFirst({
16+
where: eq(users.email, validatedData.email),
17+
});
18+
19+
if (existingUser) {
20+
return NextResponse.json(
21+
{ error: "Email already exists" },
22+
{ status: 400 },
23+
);
24+
}
25+
26+
const argon2id = new Argon2id();
27+
const hashedPassword = await argon2id.hash(validatedData.password);
28+
29+
const [user] = await db
30+
.insert(users)
31+
.values({
32+
email: validatedData.email,
33+
hashedPassword: hashedPassword,
34+
name: validatedData.fullName,
35+
})
36+
.returning();
37+
38+
const token = generateSessionToken();
39+
const session = await createSession(token, user.id);
40+
await setSessionTokenCookie(token, session.expiresAt);
41+
42+
return NextResponse.json({
43+
_id: user.id,
44+
email: user.email,
45+
fullName: user.name,
46+
// role: user.role,
47+
});
48+
} catch (error) {
49+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
50+
}
51+
}

app/unauth/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { UnauthorizedPage } from "@/mint/unauthorized";
2+
3+
export default function Page() {
4+
return <UnauthorizedPage />;
5+
}

contexts/auth-context.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client";
2+
3+
import { createContext, useState, ReactNode, useEffect } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { fetchApi } from "@/lib/client/fetch";
6+
7+
interface Org {
8+
id: number;
9+
name: string;
10+
nameId: string;
11+
role: string;
12+
}
13+
14+
interface User {
15+
_id: number;
16+
email: string;
17+
name: string;
18+
orgs: Org[];
19+
}
20+
21+
interface AuthContextType {
22+
user: User | null;
23+
login: (email: string, password: string) => Promise<User>;
24+
signup: (email: string, password: string, fullName: string) => Promise<User>;
25+
logout: () => Promise<void>;
26+
isAuthenticated: boolean;
27+
setIsAuthenticated: (status: boolean) => void;
28+
}
29+
30+
const defaultContext: AuthContextType = {
31+
user: null,
32+
login: async () => Promise.reject("Not implemented"),
33+
signup: async () => Promise.reject("Not implemented"),
34+
logout: async () => Promise.reject("Not implemented"),
35+
isAuthenticated: false,
36+
setIsAuthenticated: () => {},
37+
};
38+
39+
export const AuthContext = createContext<AuthContextType>(defaultContext);
40+
41+
export const AuthProvider: React.FC<{ children: ReactNode }> = ({
42+
children,
43+
}) => {
44+
const [user, setUser] = useState<User | null>(null);
45+
const [isAuthenticated, setIsAuthenticated] = useState(false);
46+
const router = useRouter();
47+
48+
useEffect(() => {
49+
const fetchUser = async () => {
50+
try {
51+
const data = await fetchApi<User>("/auth/me");
52+
if (data.email) {
53+
setIsAuthenticated(true);
54+
setUser(data);
55+
}
56+
} catch (error) {
57+
console.error("User not authorized");
58+
}
59+
};
60+
61+
fetchUser();
62+
}, []);
63+
64+
const login = async (email: string, password: string): Promise<User> => {
65+
try {
66+
const data = await fetchApi<User>("/auth/login", {
67+
method: "POST",
68+
body: JSON.stringify({ email, password }),
69+
});
70+
71+
if (!data.email) {
72+
throw new Error("Login failed: Invalid response");
73+
}
74+
75+
setUser(data);
76+
setIsAuthenticated(true);
77+
router.push("/admin");
78+
return data;
79+
} catch (error) {
80+
console.error("Login failed:", error);
81+
throw new Error("Invalid credentials");
82+
}
83+
};
84+
85+
const register = async (
86+
email: string,
87+
password: string,
88+
fullName: string,
89+
): Promise<User> => {
90+
try {
91+
const data = await fetchApi<User>("/auth/register", {
92+
method: "POST",
93+
body: JSON.stringify({ email, password, fullName }),
94+
});
95+
96+
setUser(data);
97+
setIsAuthenticated(true);
98+
router.push("/admin");
99+
return data;
100+
} catch (error) {
101+
console.error("Signup failed:", error);
102+
throw new Error("Registration failed");
103+
}
104+
};
105+
106+
const logout = async (): Promise<void> => {
107+
try {
108+
await fetchApi("/auth/logout", { method: "DELETE" });
109+
setUser(null);
110+
setIsAuthenticated(false);
111+
router.push("/auth/login");
112+
} catch (error) {
113+
console.error("Logout failed:", error);
114+
throw error;
115+
}
116+
};
117+
118+
return (
119+
<AuthContext.Provider
120+
value={{
121+
user,
122+
login,
123+
signup: register,
124+
logout,
125+
isAuthenticated,
126+
setIsAuthenticated,
127+
}}
128+
>
129+
{children}
130+
</AuthContext.Provider>
131+
);
132+
};

db/schema.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@ import {
1010
uniqueIndex,
1111
} from "drizzle-orm/pg-core";
1212

13-
export const userEmails = pgTable("user_emails", {
14-
email: text("email").primaryKey(),
15-
userId: integer("user_id").references(() => users.id),
16-
});
17-
1813
export const users = pgTable("users", {
1914
id: serial("id").primaryKey(),
15+
email: varchar("email").notNull().unique(),
2016

2117
nameId: text("name_id").notNull().unique(),
2218
name: text("name").notNull(),

lib/client/fetch.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export async function fetchApi<T>(
2+
endpoint: string,
3+
options: RequestInit = {},
4+
): Promise<T> {
5+
const response = await fetch(`/api/${endpoint}`, {
6+
...options,
7+
credentials: "include",
8+
headers: {
9+
"Content-Type": "application/json",
10+
...options.headers,
11+
},
12+
});
13+
14+
if (!response.ok) {
15+
throw new Error(`API call failed: ${response.statusText}`);
16+
}
17+
18+
return response.json();
19+
}

lib/server/cookies.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { cookies } from "next/headers";
2+
3+
export async function setSessionTokenCookie(
4+
token: string,
5+
expiresAt: Date,
6+
): Promise<void> {
7+
const cookieStore = cookies();
8+
cookieStore.set("session", token, {
9+
httpOnly: true,
10+
sameSite: "lax",
11+
secure: process.env.NODE_ENV === "production",
12+
expires: expiresAt,
13+
path: "/",
14+
});
15+
}
16+
17+
export async function deleteSessionTokenCookie(): Promise<void> {
18+
const cookieStore = cookies();
19+
cookieStore.set("session", "", {
20+
httpOnly: true,
21+
sameSite: "lax",
22+
secure: process.env.NODE_ENV === "production",
23+
maxAge: 0,
24+
path: "/",
25+
});
26+
}

lib/validations.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,17 @@ export const createTestCaseSchema = z.object({
8383
export const createParticipantSchema = z.object({
8484
userId: z.number().int().positive(),
8585
});
86+
87+
export const loginSchema = z.object({
88+
email: z.string().email(),
89+
password: z.string().min(6),
90+
});
91+
92+
export const registerSchema = z.object({
93+
email: z.string().email(),
94+
password: z.string().min(6),
95+
fullName: z.string().min(2),
96+
});
97+
98+
export type LoginInput = z.infer<typeof loginSchema>;
99+
export type RegisterInput = z.infer<typeof registerSchema>;

mint/books/listing.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ export default function LibraryBookListing() {
157157
return (
158158
<>
159159
<div className="container mx-auto p-4">
160-
<h1 className="text-2xl font-bold mb-4">Library Book Listing</h1>
161160
<div className="flex gap-4 mb-4">
162161
<div className="relative flex-grow">
163162
<Input
@@ -222,7 +221,11 @@ export default function LibraryBookListing() {
222221
<TableCell>{book.author}</TableCell>
223222
<TableCell>{book.pages}</TableCell>
224223
<TableCell>
225-
{new Date(book.publicationDate).toLocaleDateString()}
224+
{new Date(book.publicationDate).toLocaleDateString("en-US", {
225+
year: "numeric",
226+
month: "2-digit",
227+
day: "2-digit",
228+
})}
226229
</TableCell>
227230
<TableCell>{book.copiesAvailable}</TableCell>
228231
<TableCell>

0 commit comments

Comments
 (0)