Refactor authentication flow; improve error handling, implement token creation and verification, and enhance user session management. Fixed login issue. Currently working on user authorisation for creating teams.

This commit is contained in:
bobbert committed 2025-03-12 18:11:33 +00:00
1 parent ce962162cb
commit b039af238f
5 files changed
+459 -162

No files matched your search

+53 -19
View File
@@ -1,4 +1,4 @@
"use client"; // Mark as a Client Component "use client";
import { useState } from "react"; import { useState } from "react";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -16,30 +16,66 @@ export default function Login() {
setIsLoading(true); setIsLoading(true);
setError(""); setError("");
try {
console.log("Attempting to sign in with:", username);
// First try using the NextAuth signIn method
try { try {
const result = await signIn("credentials", { const result = await signIn("credentials", {
redirect: false, redirect: false,
username, username,
password, password,
callbackUrl: "/team-selection",
}); });
if (result?.error) { if (result?.error) {
setError("Invalid username or password"); throw new Error(result.error);
setIsLoading(false);
} else {
// Fetch the user role to redirect appropriately
const response = await fetch("/api/user");
const userData = await response.json();
if (userData.role === "admin") {
router.push("/admin");
} else {
router.push("/team-selection");
} }
// Success case - redirect
router.push("/team-selection");
return;
} catch (signInError) {
console.error("Error with signIn method:", signInError);
console.log("Falling back to direct API call");
// Fall back to direct fetch approach with explicit JSON content-type
const loginResponse = await fetch("/api/auth/callback/credentials", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
callbackUrl: "/team-selection",
}),
credentials: "include", // Important for cookie handling
});
if (!loginResponse.ok) {
const errorData = await loginResponse
.json()
.catch(() => ({ error: "Failed to parse error response" }));
throw new Error(
errorData.error ||
`Login failed with status ${loginResponse.status}`
);
}
// On successful login
const userData = await loginResponse
.json()
.catch(() => ({ url: "/team-selection" }));
// Allow time for cookie to be set
setTimeout(() => {
router.push(userData.url || "/team-selection");
}, 100);
} }
} catch (err) { } catch (err) {
setError("An unexpected error occurred"); setError(err.message || "An unexpected error occurred");
console.error(err); console.error("Login error:", err);
setIsLoading(false); setIsLoading(false);
} }
}; };
@@ -54,6 +90,7 @@ export default function Login() {
<div className="auth-container"> <div className="auth-container">
<div className="auth-card"> <div className="auth-card">
<form onSubmit={handleLogin}>
<div className="input-container"> <div className="input-container">
<input <input
type="text" type="text"
@@ -75,13 +112,10 @@ export default function Login() {
/> />
</div> </div>
{error && <div className="error-message">{error}</div>} {error && <div className="error-message">{error}</div>}
<button <button type="submit" className="auth-button" disabled={isLoading}>
onClick={handleLogin}
className="auth-button"
disabled={isLoading}
>
{isLoading ? "Logging in..." : "Log In"} {isLoading ? "Logging in..." : "Log In"}
</button> </button>
</form>
<Link href="/register"> <Link href="/register">
<button className="auth-button">Register</button> <button className="auth-button">Register</button>
</Link> </Link>
+341 -135
View File
@@ -1,7 +1,8 @@
export const runtime = "nodejs"; export const runtime = "nodejs";
// Remove problematic import of NextAuth
import { validateCredentials } from "@/actions/auth"; import { validateCredentials } from "@/actions/auth";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
// Define hardcoded values for NextAuth // Define hardcoded values for NextAuth
const NEXTAUTH_SECRET = "banana"; const NEXTAUTH_SECRET = "banana";
@@ -11,45 +12,356 @@ const NEXTAUTH_URL = "http://localhost:3000";
console.log("Using NextAuth Secret: banana"); console.log("Using NextAuth Secret: banana");
console.log(`Using NextAuth URL: ${NEXTAUTH_URL}`); console.log(`Using NextAuth URL: ${NEXTAUTH_URL}`);
// Custom credentials provider function that doesn't rely on the import // Function to create a signed token
const createCredentialsProvider = (options) => { function createToken(payload) {
return { const encodedData = Buffer.from(JSON.stringify(payload)).toString("base64");
const signature = Buffer.from(`${encodedData}.${NEXTAUTH_SECRET}`).toString(
"base64"
);
return `${encodedData}.${signature}`;
}
// Function to verify a token
function verifyToken(token) {
try {
if (!token) return null;
const [encodedData, signature] = token.split(".");
const expectedSignature = Buffer.from(
`${encodedData}.${NEXTAUTH_SECRET}`
).toString("base64");
if (signature !== expectedSignature) return null;
const data = JSON.parse(
Buffer.from(encodedData, "base64").toString("utf-8")
);
// Check if token is expired
if (data.exp && data.exp < Math.floor(Date.now() / 1000)) return null;
return data;
} catch (error) {
console.error("Token verification error:", error);
return null;
}
}
// Create the handler functions directly without calling NextAuth function
export async function GET(request) {
try {
// Safely parse URL with error handling
let pathname = "/";
let searchParams = new URLSearchParams();
try {
if (request && request.url) {
const url = new URL(request.url);
pathname = url.pathname;
searchParams = url.searchParams;
} else {
console.warn("Request or request.url is undefined");
}
} catch (urlError) {
console.error("Failed to parse URL:", urlError);
pathname = request.nextUrl?.pathname || "/api/auth/session";
}
console.log("Auth GET request path:", pathname);
// Handle session requests
if (pathname.includes("/session")) {
try {
const cookieStore = await cookies();
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
if (!sessionToken) {
return Response.json({ user: null });
}
const userData = verifyToken(sessionToken);
return Response.json({
user: userData
? {
id: userData.id,
name: userData.username,
email: `${userData.username}@example.com`, // Next-auth expects an email
image: null,
role: userData.role,
teamId: userData.teamId,
}
: null,
});
} catch (cookieError) {
console.error("Error accessing cookies:", cookieError);
return Response.json({ user: null });
}
}
// For CSRF token requests - IMPORTANT: Return JSON, don't redirect
if (pathname.includes("/csrf")) {
const csrfToken = Buffer.from(
`csrf-${Date.now()}-${NEXTAUTH_SECRET}`
).toString("base64");
return new Response(JSON.stringify({ csrfToken }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
// Check if this is a signin request
if (pathname.includes("/signin")) {
// Redirect to the login page with callbackUrl
const callbackUrl = searchParams.get("callbackUrl") || "/";
return Response.redirect(
`${NEXTAUTH_URL}/login?callbackUrl=${encodeURIComponent(callbackUrl)}`
);
}
// Handle providers request
if (pathname.includes("/providers")) {
return Response.json({
credentials: {
id: "credentials", id: "credentials",
name: "Credentials", name: "Credentials",
type: "credentials", type: "credentials",
credentials: { signinUrl: `${NEXTAUTH_URL}/api/auth/signin/credentials`,
username: { label: "Username", type: "text" }, callbackUrl: `${NEXTAUTH_URL}/api/auth/callback/credentials`,
password: { label: "Password", type: "password" },
}, },
async authorize(credentials) { });
}
// Default: redirect to login
return Response.redirect(`${NEXTAUTH_URL}/login`);
} catch (error) {
console.error("Auth GET error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
export async function POST(request) {
try { try {
if (!credentials?.username || !credentials?.password) return null; // Debug the actual URL value before trying to parse it
const user = await validateCredentials( console.log("Raw request URL:", request.url);
credentials.username,
credentials.password // More robust URL parsing
let pathname = "";
let requestUrl = null;
try {
// Add protocol and host if the URL doesn't have them
const urlString = request.url;
// Only try to parse if we have a string
if (typeof urlString === "string" && urlString) {
// Check if URL has proper protocol
const hasProtocol =
urlString.startsWith("http://") || urlString.startsWith("https://");
if (hasProtocol) {
requestUrl = new URL(urlString);
} else {
// Add protocol and host to handle relative URLs
requestUrl = new URL(urlString, "http://localhost:3000");
}
pathname = requestUrl.pathname;
console.log("Successfully parsed URL:", pathname);
} else {
console.log("URL is not valid, using fallback methods");
}
} catch (urlError) {
console.error("Invalid URL in request:", urlError);
}
// Use nextUrl as fallback if available
if (!pathname && request.nextUrl?.pathname) {
pathname = request.nextUrl.pathname;
console.log("Using nextUrl pathname:", pathname);
}
const contentType = request.headers.get("content-type") || "";
console.log("Content-Type:", contentType);
// Check for callback/credentials without relying on URL parsing
const isCallbackRequest =
pathname.includes("/callback/credentials") ||
(request.url && request.url.includes("/callback/credentials"));
if (isCallbackRequest) {
// Extract credentials from the appropriate source based on content type
let username, password, callbackUrl;
if (contentType.includes("application/json")) {
// Handle JSON request
const jsonData = await request.json();
username = jsonData.username;
password = jsonData.password;
callbackUrl = jsonData.callbackUrl || "/team-selection";
console.log("Parsed JSON credentials from callback request");
} else if (
contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("multipart/form-data")
) {
// Handle form data
const formData = await request.formData();
username = formData.get("username");
password = formData.get("password");
callbackUrl = formData.get("callbackUrl") || "/team-selection";
console.log("Parsed form credentials from callback request");
} else {
return Response.json(
{ error: "Unsupported content type for authentication" },
{ status: 400 }
); );
return user }
? {
console.log(`Credentials callback for user: ${username || "(unknown)"}`);
// Validate credentials
const user = await validateCredentials(username, password);
if (!user) {
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
// Create token with user data
const token = {
id: user.id, id: user.id,
username: user.username, username: user.username,
role: user.role, role: user.role,
teamId: user.teamId, teamId: user.teamId,
} iat: Math.floor(Date.now() / 1000),
: null; exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days
} catch (error) {
console.error("NextAuth authorize error:", error);
return null;
}
},
...options,
}; };
};
// Create JWT-like token
const jwtToken = createToken(token);
// Calculate expiration date for cookie
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Return success with cookie and redirect URL
return new Response(JSON.stringify({ url: callbackUrl }), {
status: 200,
headers: {
"Content-Type": "application/json",
"Set-Cookie": `next-auth.session-token=${jwtToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${expiryDate.toUTCString()}`,
},
});
}
// Handle regular form submission
let username, password, callbackUrl;
// Handle form data (application/x-www-form-urlencoded)
if (contentType.includes("application/x-www-form-urlencoded")) {
// Clone request to read the body
const clonedRequest = request.clone();
const formText = await clonedRequest.text();
console.log("Form data:", formText);
// Parse form data manually
const params = new URLSearchParams(formText);
username = params.get("username");
password = params.get("password");
callbackUrl = params.get("callbackUrl") || "/team-selection";
console.log("Parsed credentials:", {
username,
password: password ? "[REDACTED]" : undefined,
});
}
// Handle JSON
else if (contentType.includes("application/json")) {
const jsonData = await request.json();
username = jsonData.username;
password = jsonData.password;
callbackUrl = jsonData.callbackUrl || "/team-selection";
}
// Try form data as a last resort
else {
try {
const formData = await request.formData();
username = formData.get("username");
password = formData.get("password");
callbackUrl = formData.get("callbackUrl") || "/team-selection";
} catch (formError) {
console.error("Form data parsing error:", formError);
// Instead of returning an error, log and continue - the request might be something else
}
}
if (!username || !password) {
// If no credentials were found, this might be a different kind of request
// Log it and return an appropriate error
console.error("Missing credentials in request");
return Response.json(
{ error: "Username and password required" },
{ status: 400 }
);
}
// Validate credentials
const user = await validateCredentials(username, password);
if (!user) {
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
// Create token with user data
const token = {
id: user.id,
username: user.username,
role: user.role,
teamId: user.teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, // 30 days
};
// Create JWT-like token
const jwtToken = createToken(token);
// Calculate expiration date for cookie
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 30);
// Return success with cookie and redirect URL
return new Response(
JSON.stringify({
ok: true,
url: callbackUrl,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
"Set-Cookie": `next-auth.session-token=${jwtToken}; Path=/; HttpOnly; SameSite=Lax; Expires=${expiryDate.toUTCString()}`,
},
}
);
} catch (error) {
console.error("Auth POST error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
// Export authOptions for compatibility with other code using getServerSession
export const authOptions = { export const authOptions = {
debug: process.env.NODE_ENV === "development",
providers: [ providers: [
// Using our custom provider creator instead of the imported module {
createCredentialsProvider({}), id: "credentials",
name: "Credentials",
type: "credentials",
authorize: async (credentials) => {
if (!credentials?.username || !credentials?.password) return null;
return await validateCredentials(
credentials.username,
credentials.password
);
},
},
], ],
callbacks: { callbacks: {
async jwt({ token, user }) { async jwt({ token, user }) {
@@ -73,114 +385,8 @@ export const authOptions = {
}, },
}, },
session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60 }, session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60 },
secret: NEXTAUTH_SECRET, // Use the hardcoded secret secret: NEXTAUTH_SECRET,
url: NEXTAUTH_URL, // Use the hardcoded URL
}; };
// Create the handler functions directly without calling NextAuth function // Export the token functions directly for use in other files
export async function GET(request) { export { createToken, verifyToken };
// Implement the GET handler for auth
try {
// Extract the necessary info from the request
const { searchParams } = new URL(request.url);
const callbackUrl = searchParams.get("callbackUrl") || "/";
const error = searchParams.get("error") || "";
// For sign-in requests
if (searchParams.get("csrf")) {
// Generate a simple CSRF token using the secret
const csrfToken = Buffer.from(`${Date.now()}-${NEXTAUTH_SECRET}`).toString('base64');
return new Response(JSON.stringify({ csrfToken }), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}
// For session requests
if (request.headers.get("x-auth-return-session")) {
// Return the session info - would need more implementation
return new Response(JSON.stringify({ user: null }), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}
// Default response for GET - normally this would redirect to the sign-in page
return new Response(JSON.stringify({ url: "/login", error }), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
console.error("Auth GET error:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}
export async function POST(request) {
// Implement the POST handler for auth
try {
const body = await request.json();
const { username, password } = body;
// Validate the credentials
const user = await validateCredentials(username, password);
if (!user) {
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
status: 401,
headers: {
"Content-Type": "application/json",
},
});
}
// Create a session token
const token = {
id: user.id,
username: user.username,
role: user.role,
teamId: user.teamId,
// Add timestamp for token expiration checks
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60), // 30 days
};
// Create a simple JWT-like token by encoding and signing with our secret
const encodedToken = Buffer.from(JSON.stringify(token)).toString('base64');
const signature = Buffer.from(`${encodedToken}-${NEXTAUTH_SECRET}`).toString('base64');
const jwtToken = `${encodedToken}.${signature}`;
return new Response(
JSON.stringify({
user: token,
url: body.callbackUrl || "/dashboard",
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
"Set-Cookie": `next-auth.session-token=${jwtToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`,
},
}
);
} catch (error) {
console.error("Auth POST error:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
}
+29 -8
View File
@@ -1,17 +1,38 @@
// app/api/user/route.js // app/api/user/route.js
import { getServerSession } from 'next-auth/next'; import { cookies } from "next/headers";
import { authOptions } from '../auth/[...nextauth]/route';
export async function GET() { export async function GET() {
const session = await getServerSession(authOptions); try {
// Get the token from cookies - Fix: await the cookies() call
const cookieStore = await cookies();
const sessionToken = cookieStore.get("next-auth.session-token")?.value;
if (!session) { if (!sessionToken) {
return Response.json({ error: 'Unauthorized' }, { status: 401 }); return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Import the token verification function - using a direct import
const { verifyToken } = await import("../auth/[...nextauth]/route");
try {
const userData = verifyToken(sessionToken);
if (!userData) {
return Response.json({ error: "Invalid session" }, { status: 401 });
} }
return Response.json({ return Response.json({
id: session.user.id, id: userData.id,
username: session.user.username, username: userData.username,
role: session.user.role role: userData.role,
teamId: userData.teamId,
}); });
} catch (tokenError) {
console.error("Token verification error:", tokenError);
return Response.json({ error: "Invalid session token" }, { status: 401 });
}
} catch (error) {
console.error("User API error:", error);
return Response.json({ error: "Server error" }, { status: 500 });
}
} }
+36
View File
@@ -0,0 +1,36 @@
const NEXTAUTH_SECRET = "banana";
// Function to create a signed token
export function createToken(payload) {
const encodedData = Buffer.from(JSON.stringify(payload)).toString("base64");
const signature = Buffer.from(`${encodedData}.${NEXTAUTH_SECRET}`).toString(
"base64"
);
return `${encodedData}.${signature}`;
}
// Function to verify a token
export function verifyToken(token) {
try {
if (!token) return null;
const [encodedData, signature] = token.split(".");
const expectedSignature = Buffer.from(
`${encodedData}.${NEXTAUTH_SECRET}`
).toString("base64");
if (signature !== expectedSignature) return null;
const data = JSON.parse(
Buffer.from(encodedData, "base64").toString("utf-8")
);
// Check if token is expired
if (data.exp && data.exp < Math.floor(Date.now() / 1000)) return null;
return data;
} catch (error) {
console.error("Token verification error:", error);
return null;
}
}
BIN
View File
Binary file not shown.